From f0bca8cbdb82b3526c98d8c7b80bc4773c6dabb5 Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Sun, 19 Jul 2026 18:16:53 +0200 Subject: [PATCH] refactor: group headers and specs into sub-domain folders Reorganise include/morph/ into per-sub-domain subdirectories (core, journal, offline, session, forms, util) mirroring the namespaces, and mirror the same folders under docs/spec/. Pure file-move + include-path change: no namespace, symbol, or behaviour change. All in-repo includes, CMake header sets, Doxygen @file comments, ARCHITECTURE.md header map, and inter-doc links updated. Co-Authored-By: Claude Opus 4.8 (1M context) --- CMakeLists.txt | 44 ++--- README.md | 4 +- docs/ARCHITECTURE.md | 159 ++++++++++++------ docs/planned/backend_changed_dispatch.md | 8 +- docs/planned/connection_scoped_cleanup.md | 16 +- docs/planned/drift_guard.md | 14 +- docs/planned/durable_offline_queue_impl.md | 24 +-- docs/planned/durable_queue.md | 6 +- docs/planned/graceful_shutdown.md | 18 +- docs/planned/gui_collections_views.md | 42 ++--- docs/planned/gui_computed_fields.md | 50 +++--- docs/planned/gui_cross_field_rules.md | 42 ++--- docs/planned/gui_dependent_choices.md | 48 +++--- docs/planned/gui_field_metadata.md | 18 +- docs/planned/gui_i18n.md | 26 +-- docs/planned/gui_layout_grouping.md | 10 +- docs/planned/gui_overview.md | 24 +-- docs/planned/gui_renderer_toolkit.md | 20 +-- docs/planned/gui_widget_hints.md | 20 +-- docs/planned/gui_workflows_navigation.md | 34 ++-- docs/planned/instance_authorization.md | 6 +- docs/planned/journal_evolution.md | 18 +- docs/planned/non_qt_transport.md | 28 +-- docs/planned/observability.md | 32 ++-- docs/planned/outbox.md | 10 +- docs/planned/protocol_versioning.md | 20 +-- docs/planned/testing_strategy.md | 22 +-- docs/planned/tls_peer_verification.md | 6 +- docs/planned/transport_limits.md | 12 +- docs/planned/validation.md | 10 +- docs/spec/concurrency_and_lifetimes.md | 18 +- docs/spec/{ => core}/backend.md | 0 docs/spec/{ => core}/bridge.md | 8 +- docs/spec/{ => core}/completion.md | 2 +- docs/spec/{ => core}/executor.md | 4 +- docs/spec/{ => core}/logger.md | 0 docs/spec/{ => core}/registry.md | 6 +- docs/spec/{ => core}/wire.md | 0 docs/spec/error_handling.md | 22 +-- docs/spec/{ => forms}/choice.md | 62 +------ docs/spec/{ => forms}/forms.md | 14 +- docs/spec/{ => journal}/journal.md | 0 docs/spec/{ => offline}/offline.md | 2 +- docs/spec/security.md | 16 +- docs/spec/{ => session}/session.md | 32 ++-- docs/spec/{ => util}/datetime.md | 0 docs/spec/{ => util}/quantity_type.md | 6 +- docs/spec/{ => util}/rational.md | 4 +- examples/bank/gui/BankClient.cpp | 4 +- examples/bank/gui/BankClient.hpp | 4 +- .../gui/controllers/AccountController.hpp | 2 +- .../bank/gui/controllers/AppController.hpp | 2 +- .../bank/gui/controllers/CardController.hpp | 2 +- .../bank/gui/controllers/LoanController.hpp | 2 +- .../bank/gui/controllers/PayeeController.hpp | 2 +- .../gui/controllers/TransactionController.hpp | 2 +- .../include/bank/models/account_model.hpp | 4 +- .../include/bank/models/auth_model.hpp | 4 +- .../include/bank/models/card_model.hpp | 4 +- .../include/bank/models/loan_model.hpp | 4 +- .../include/bank/models/payee_model.hpp | 4 +- .../include/bank/models/payment_model.hpp | 4 +- .../include/bank/models/transaction_model.hpp | 4 +- examples/bank/include/bank/app/app.hpp | 4 +- examples/bank/include/bank/core/principal.hpp | 2 +- .../include/bank/models/account_model.hpp | 4 +- .../bank/include/bank/models/auth_model.hpp | 4 +- .../bank/include/bank/models/budget_model.hpp | 4 +- .../bank/include/bank/models/card_model.hpp | 4 +- .../bank/include/bank/models/loan_model.hpp | 4 +- .../bank/models/notification_model.hpp | 4 +- .../bank/include/bank/models/payee_model.hpp | 4 +- .../include/bank/models/payment_model.hpp | 4 +- .../include/bank/models/statement_model.hpp | 4 +- .../include/bank/models/transaction_model.hpp | 4 +- examples/bank/src/app/app.cpp | 4 +- examples/bank/src/cli/main.cpp | 14 +- examples/bank/src/models/account_model.cpp | 2 +- examples/bank/tests/bank_test_support.hpp | 4 +- examples/bank/tests/test_account.cpp | 2 +- examples/bank/tests/test_auth.cpp | 2 +- examples/bank/tests/test_budget.cpp | 2 +- examples/bank/tests/test_card.cpp | 2 +- examples/bank/tests/test_loan.cpp | 2 +- examples/bank/tests/test_notification.cpp | 2 +- examples/bank/tests/test_offline.cpp | 8 +- examples/bank/tests/test_payee.cpp | 2 +- examples/bank/tests/test_payment.cpp | 2 +- examples/bank/tests/test_relations.cpp | 2 +- examples/bank/tests/test_remote.cpp | 10 +- examples/bank/tests/test_statement.cpp | 2 +- examples/bank/tests/test_transaction.cpp | 2 +- examples/forms/gui_qml/FormsController.cpp | 2 +- examples/forms/gui_qml/FormsController.hpp | 4 +- examples/forms/lab_model.hpp | 10 +- examples/forms/lab_units.hpp | 2 +- examples/forms/main.cpp | 2 +- include/morph/{ => core}/backend.hpp | 2 +- include/morph/{ => core}/bridge.hpp | 4 +- include/morph/{ => core}/completion.hpp | 0 include/morph/{ => core}/executor.hpp | 0 include/morph/{ => core}/logger.hpp | 0 include/morph/{ => core}/model.hpp | 4 +- include/morph/{ => core}/registry.hpp | 0 include/morph/{ => core}/remote.hpp | 4 +- include/morph/{ => core}/strand.hpp | 0 include/morph/{ => core}/wire.hpp | 2 +- include/morph/detail/quantity_equation.hpp | 2 +- include/morph/{ => forms}/choice.hpp | 4 +- include/morph/{ => forms}/forms.hpp | 4 +- include/morph/{ => journal}/action_log.hpp | 0 .../morph/{ => journal}/file_action_log.hpp | 2 +- include/morph/{ => journal}/journal.hpp | 4 +- .../morph/{ => offline}/network_monitor.hpp | 0 include/morph/{ => offline}/offline_queue.hpp | 0 .../{ => offline}/reconnect_coordinator.hpp | 2 +- include/morph/{ => offline}/sync_worker.hpp | 2 +- include/morph/qt/qt_executor.hpp | 2 +- include/morph/qt/qt_websocket_backend.hpp | 4 +- include/morph/qt/qt_websocket_server.hpp | 2 +- include/morph/{ => session}/session.hpp | 0 include/morph/{ => session}/session_auth.hpp | 2 +- include/morph/{ => util}/datetime.hpp | 2 +- include/morph/{ => util}/quantity.hpp | 6 +- include/morph/{ => util}/rational.hpp | 2 +- src/main.cpp | 8 +- src/qt/qt_websocket_backend.cpp | 2 +- tests/qt/qt_test_client_main.cpp | 4 +- tests/qt/qt_test_models.hpp | 4 +- tests/qt/qt_test_server_main.cpp | 4 +- tests/qt/test_qt_websocket.cpp | 10 +- tests/test_action_log.cpp | 18 +- tests/test_action_log_phase2.cpp | 18 +- tests/test_backend_extra.cpp | 8 +- tests/test_bridge_execute_json.cpp | 8 +- tests/test_bridge_fixes.cpp | 14 +- tests/test_bridge_lifetime.cpp | 8 +- tests/test_bridge_local.cpp | 6 +- tests/test_bridge_remote.cpp | 8 +- tests/test_completion.cpp | 2 +- tests/test_completion_branches.cpp | 2 +- tests/test_completion_extra.cpp | 4 +- tests/test_concurrency_invariants.cpp | 20 +-- tests/test_conflict_resolution.cpp | 10 +- tests/test_coverage_extra.cpp | 6 +- tests/test_coverage_gaps.cpp | 16 +- tests/test_coverage_push95.cpp | 24 +-- tests/test_datetime.cpp | 2 +- tests/test_dispatch_di.cpp | 2 +- tests/test_example.cpp | 6 +- tests/test_executor.cpp | 2 +- tests/test_executor_extra.cpp | 2 +- tests/test_handler_binding.cpp | 8 +- tests/test_issues.cpp | 24 +-- tests/test_logger.cpp | 2 +- tests/test_model.cpp | 2 +- tests/test_network_monitor.cpp | 2 +- tests/test_offline_integration.cpp | 14 +- tests/test_offline_queue.cpp | 2 +- tests/test_policy_hardening.cpp | 22 +-- tests/test_quantity.cpp | 2 +- tests/test_quantity_forms.cpp | 14 +- tests/test_rational.cpp | 2 +- tests/test_reconnect_coordinator.cpp | 4 +- tests/test_registry_extra.cpp | 2 +- tests/test_remote_extra.cpp | 14 +- tests/test_security_fixes.cpp | 12 +- tests/test_server_limits.cpp | 12 +- tests/test_session_auth.cpp | 2 +- tests/test_strand.cpp | 2 +- tests/test_strand_extra.cpp | 2 +- tests/test_strand_race.cpp | 4 +- tests/test_subscription.cpp | 10 +- tests/test_support.hpp | 4 +- tests/test_switch_backend.cpp | 12 +- tests/test_sync_worker.cpp | 4 +- tests/test_wire_hardening.cpp | 2 +- 177 files changed, 808 insertions(+), 811 deletions(-) rename docs/spec/{ => core}/backend.md (100%) rename docs/spec/{ => core}/bridge.md (99%) rename docs/spec/{ => core}/completion.md (99%) rename docs/spec/{ => core}/executor.md (99%) rename docs/spec/{ => core}/logger.md (100%) rename docs/spec/{ => core}/registry.md (99%) rename docs/spec/{ => core}/wire.md (100%) rename docs/spec/{ => forms}/choice.md (80%) rename docs/spec/{ => forms}/forms.md (96%) rename docs/spec/{ => journal}/journal.md (100%) rename docs/spec/{ => offline}/offline.md (99%) rename docs/spec/{ => session}/session.md (94%) rename docs/spec/{ => util}/datetime.md (100%) rename docs/spec/{ => util}/quantity_type.md (99%) rename docs/spec/{ => util}/rational.md (97%) rename include/morph/{ => core}/backend.hpp (99%) rename include/morph/{ => core}/bridge.hpp (99%) rename include/morph/{ => core}/completion.hpp (100%) rename include/morph/{ => core}/executor.hpp (100%) rename include/morph/{ => core}/logger.hpp (100%) rename include/morph/{ => core}/model.hpp (99%) rename include/morph/{ => core}/registry.hpp (100%) rename include/morph/{ => core}/remote.hpp (99%) rename include/morph/{ => core}/strand.hpp (100%) rename include/morph/{ => core}/wire.hpp (99%) rename include/morph/{ => forms}/choice.hpp (98%) rename include/morph/{ => forms}/forms.hpp (99%) rename include/morph/{ => journal}/action_log.hpp (100%) rename include/morph/{ => journal}/file_action_log.hpp (99%) rename include/morph/{ => journal}/journal.hpp (99%) rename include/morph/{ => offline}/network_monitor.hpp (100%) rename include/morph/{ => offline}/offline_queue.hpp (100%) rename include/morph/{ => offline}/reconnect_coordinator.hpp (99%) rename include/morph/{ => offline}/sync_worker.hpp (99%) rename include/morph/{ => session}/session.hpp (100%) rename include/morph/{ => session}/session_auth.hpp (99%) rename include/morph/{ => util}/datetime.hpp (99%) rename include/morph/{ => util}/quantity.hpp (99%) rename include/morph/{ => util}/rational.hpp (99%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 55fc5bf..7f531a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,28 +96,28 @@ target_sources(morph FILE_SET HEADERS BASE_DIRS include FILES - include/morph/logger.hpp - include/morph/executor.hpp - include/morph/strand.hpp - include/morph/completion.hpp - include/morph/model.hpp - include/morph/action_log.hpp - include/morph/journal.hpp - include/morph/file_action_log.hpp - include/morph/rational.hpp - include/morph/quantity.hpp - include/morph/datetime.hpp - include/morph/choice.hpp - include/morph/forms.hpp - include/morph/registry.hpp - include/morph/backend.hpp - include/morph/remote.hpp - include/morph/bridge.hpp - include/morph/network_monitor.hpp - include/morph/offline_queue.hpp - include/morph/sync_worker.hpp - include/morph/session.hpp - include/morph/wire.hpp + include/morph/core/logger.hpp + include/morph/core/executor.hpp + include/morph/core/strand.hpp + include/morph/core/completion.hpp + include/morph/core/model.hpp + include/morph/core/registry.hpp + include/morph/core/backend.hpp + include/morph/core/remote.hpp + include/morph/core/bridge.hpp + include/morph/core/wire.hpp + include/morph/journal/action_log.hpp + include/morph/journal/journal.hpp + include/morph/journal/file_action_log.hpp + include/morph/offline/network_monitor.hpp + include/morph/offline/offline_queue.hpp + include/morph/offline/sync_worker.hpp + include/morph/session/session.hpp + include/morph/forms/forms.hpp + include/morph/forms/choice.hpp + include/morph/util/rational.hpp + include/morph/util/quantity.hpp + include/morph/util/datetime.hpp ) # ── Demo executable ────────────────────────────────────────────────────────── diff --git a/README.md b/README.md index 003b795..7c65ccd 100644 --- a/README.md +++ b/README.md @@ -312,7 +312,7 @@ with `checkpoint()` coalescing and `undoLast()`, which reconstructs state by *replaying* the remaining actions. This model is exact for pure, deterministic, in-memory models; a model that owns an external store (SQL, network) needs care, because replay re-executes actions rather than re-applying stored results — see -the design notes in [`docs/spec/journal.md`](docs/spec/journal.md). +the design notes in [`docs/spec/journal/journal.md`](docs/spec/journal/journal.md). ## Offline support @@ -344,7 +344,7 @@ the `Context` is client-supplied data with no built-in integrity, the default authorizer allows everything, and the local backend does not authorize at all. Treat authentication as the transport's job (e.g. TLS + a token your authorizer validates) and enforce security-critical checks inside the model. See -[`docs/spec/session.md`](docs/spec/session.md). +[`docs/spec/session/session.md`](docs/spec/session/session.md). ## Building & dependencies diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 972302e..57e2123 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -11,9 +11,9 @@ The framework is header-only (C++23, namespace `morph`), depends on Glaze for JS > subsystem, capturing invariants, API surface, and the reasoning behind each > design. Consult the matching spec before changing a public type: e.g. > `docs/spec/security.md` (authenticated sessions and the trust model), -> `docs/spec/completion.md`, `docs/spec/executor.md`, `docs/spec/bridge.md`, -> `docs/spec/journal.md`, `docs/spec/offline.md`, `docs/spec/session.md`, -> `docs/spec/wire.md`. Where this document and a spec disagree, the spec wins. +> `docs/spec/core/completion.md`, `docs/spec/core/executor.md`, `docs/spec/core/bridge.md`, +> `docs/spec/journal/journal.md`, `docs/spec/offline/offline.md`, `docs/spec/session/session.md`, +> `docs/spec/core/wire.md`. Where this document and a spec disagree, the spec wins. ## Namespace map @@ -67,44 +67,6 @@ Every nested `detail` namespace under those topics holds implementation symbols. └─────────────────────────────────────────────────────────────────┘ ``` -## Header map - -### Core headers (`include/morph/`) - -| Header | Responsibility | -|---|---| -| `logger.hpp` | `LogLevel`, log configuration and level helpers; internals in `morph::log::detail` | -| `executor.hpp` | `IExecutor`, `ThreadPoolExecutor`, `MainThreadExecutor` (`morph::exec::`) | -| `strand.hpp` | `ModelId`, `ModelIdHash`, `StrandExecutor` — serialises tasks per model (`morph::exec::detail::`) | -| `completion.hpp` | `CompletionState` (detail) + `Completion` (public) — result handle | -| `model.hpp` | `IModelHolder`, `ModelHolder`, `ModelFactory`, `IBackendChangedSink`, `BackendChangedNotifiable` — type-erased model storage; `IModelHolder::attachActionLog`/`hasActionLog`/`recordIfAttached` (`morph::model::detail::`) | -| `registry.hpp` | `ModelTraits<>`, `ActionTraits<>`, `ActionValidator<>`, `ActionLogPolicy<>`, `Loggable` (public) + `ActionDispatcher` (now also tracking each action's `coalesce` policy), `ModelRegistryFactory`, `defaultDispatcher()`, `defaultRegistry()`, `ParseError`, `registerModelOnce`, `registerActionOnce`, `actionLoggable()` (detail). Registration macros `BRIDGE_REGISTER_MODEL`, `BRIDGE_REGISTER_ACTION` (optional 4th `Loggable` argument), `BRIDGE_REGISTER_VALIDATOR` are defined here at file scope. | -| `action_log.hpp` | `LogEntry`, `IActionLog`, `InMemoryActionLog`, `toJson`/`fromJson`, `SerializationError`, `setActionLog`, `defaultActionLog`, `ScopedActionLog` (`morph::journal::`) — the durable-sink interface and the process-wide default sink, with zero dependency on `model.hpp`/`registry.hpp` | -| `journal.hpp` | `SessionLog`, `replay()` (`morph::journal::`) — full-fidelity session log with `checkpoint()` coalescing and `undoLast()`, built on `action_log.hpp` + the existing `ActionDispatcher`/`ModelRegistryFactory` | -| `file_action_log.hpp` | `FileActionLog` (`morph::journal::`) — append-only NDJSON `IActionLog`, `flush()` fsyncs | -| `rational.hpp` | `Rational`, `DecimalPlaces`, `RationalError` (`morph::math::`) — exact int64 rational arithmetic with a decimal-precision tag; Glaze wire codec (`{"num","den","dp"}`, canonicalised on read) and `std::formatter` | -| `quantity.hpp` | `Quantity`, `UnitMeta`, `UnitTraits` (`morph::units::`) — unit-tagged optional value over `Rational`; units are application enum NTTPs, schemas get `ExtUnits` automatically. See `docs/spec/quantity_type.md` for the full design. | -| `datetime.hpp` | `DateTime`, `Timestamp` (`morph::time::`) — UTC instant (ms precision) with a strict ISO-8601 wire codec (malformed input is a read *error*) and the optionally-empty field wrapper; schemas carry `"format": "date-time"` | -| `choice.hpp` | `Choice`, `FixedString`, `isChoice` (`morph::forms::`) — a field whose options are the result of executing the named action; surfaces as `x-optionsAction`/`x-optionValue`/`x-optionLabel` in schemas, renders as a combo box | -| `forms.hpp` | `schemaJson()`, `allRequiredEngaged()`, `EmptyCapableField` (`morph::forms::`) — JSON Schema per action with derived `required`, `x-decimalPlaces`, `x-order`, `x-options*` | -| `backend.hpp` | `LocalBackend` (public) + `ActionCall`, `IBackend` (detail), including the non-breaking `registerModelWithContext()` default method | -| `remote.hpp` | `RemoteServer` (now with `setLogProvider()`), `SimulatedRemoteBackend` (`morph::backend::`) | -| `bridge.hpp` | `Bridge`, `BridgeHandler` (public) + `HandlerBinding` (now carrying `contextKey`), `MemberPointerTraits` (detail) | -| `network_monitor.hpp` | `NetworkMonitorConfig`, `NetworkMonitor` — background probe thread, online/offline state machine | -| `offline_queue.hpp` | `IOfflineQueue`, `QueueItem`, `InMemoryOfflineQueue` — durable write queue abstraction | -| `sync_worker.hpp` | `SyncWorker`, `SyncResult` — drains offline queue on reconnect via caller-supplied replay | -| `reconnect_coordinator.hpp` | `ReconnectCoordinator`, `ReconnectOutcome`, `ReconnectCoordinatorConfig` — sequences reconnect → activate → bind → replay on connectivity return; all side effects injected via `Deps` | -| `session.hpp` | `Context`, `IAuthorizer`, `AllowAllAuthorizer`, `allowAllAuthorizer`, `current` (`morph::session::`) — per-call session bag carried through the bridge to the model, plus the server-side authorization seam; internals in `morph::session::detail` (`ScopedContext`, thread-local current context) | -| `session_auth.hpp` | `SessionToken`, `TokenIssuer`, `TokenVerifier`, `SigningAuthorizer`, `MacFunction`, `hmacSha256`, `AuthError` (`morph::session::`) — opt-in signed bearer tokens and a verifying `IAuthorizer`; self-contained reference HMAC-SHA256 in `morph::session::detail`. See `docs/spec/security.md`. | - -### Qt integration headers (`include/morph/qt/`) - -| Header | Responsibility | -|---|---| -| `qt_executor.hpp` | `QtExecutor` — posts callables via `QMetaObject::invokeMethod` | -| `qt_websocket_backend.hpp` | `QtWebSocketBackend` — WebSocket client; implements `IBackend` | -| `qt_websocket_server.hpp` | `QtWebSocketServer` — QObject server; forwards messages to `RemoteServer` | - ## Deployment topologies **Local mode** — model lives in the same process: @@ -130,13 +92,14 @@ GUI thread └─ serialize result → Completion::then → GUI executor ``` -**Qt WebSocket mode** — model lives in a separate process: +**Qt WebSocket mode** — model lives in a separate process. Three classes with distinct responsibilities collaborate here: `qt::QtWebSocketBackend` is the **network client** (owns the `QWebSocket`, correlates replies to pending `Completion`s, handles reconnect); `qt::QtWebSocketServer` is the **network server** (pure transport — receives text frames and forwards them verbatim to `RemoteServer::handle()`, sends the reply back); `backend::RemoteServer` is the **protocol + dispatch** layer (parses envelopes, authorizes, routes to models) and is transport-agnostic — the same class also serves the simulated-remote topology above. ``` GUI thread (Qt process) Server process + qt::QtWebSocketServer (transport) └─ bridge::BridgeHandler::execute(action) └─ bridge::Bridge::executeVia - └─ qt::QtWebSocketBackend::execute + └─ qt::QtWebSocketBackend::execute (network client) └─ assign callId, send JSON ──► qt::QtWebSocketServer::handle └─ backend::RemoteServer::handle (JSON wire envelope) └─ ActionDispatcher → StrandExecutor → Model::execute @@ -147,6 +110,25 @@ GUI thread (Qt process) Server process The GUI sees the same `morph::async::Completion` in all three modes. +**Offline / sync mode** — how the `morph::offline` primitives compose. These four classes do not form a backend of their own; they wrap whichever backend is active and drive `Bridge::switchBackend` + replay when connectivity flaps. The framework ships the primitives and the sequencing; the application wires them together (the probe, the queue's persistence, and the replay function are all caller-supplied): + +``` +morph::offline::NetworkMonitor (detects connectivity; probe is caller-supplied) + │ onOffline ──► app calls Bridge::switchBackend(local fallback) + │ app/enqueuer writes pending actions into IOfflineQueue + │ (InMemoryOfflineQueue ships with the framework; + │ a durable SQL-backed queue is the app's to provide) + │ onOnline ──► morph::offline::ReconnectCoordinator (sequences:) + │ 1. tryReconnect() → probe the primary backend + │ 2. activatePrimary() → Bridge::switchBackend(primary) + │ 3. bindContext() → rebind handlers + │ 4. replay() → morph::offline::SyncWorker::run() + │ drains IOfflineQueue via a + │ caller-supplied ReplayFunction +``` + +`NetworkMonitor` fires the transitions; `ReconnectCoordinator` owns the ordering and retry loop; `SyncWorker` owns the queue drain; `IOfflineQueue` owns the pending writes. Each is usable independently of the others. + ## Wire protocol `RemoteServer::handle` accepts JSON envelopes (`morph::wire::Envelope`). The `kind` @@ -163,8 +145,8 @@ field is the discriminator: All envelopes round-trip through Glaze JSON, so the protocol is self-describing, escaping-safe, and easy to extend (add a field, ignore unknowns). -The `session` field is a `morph::session::Context`, which now carries a -verified bearer `token` alongside the (untrusted) client-asserted `principal`. +The `session` field is a `morph::session::Context` carrying a verified bearer +`token` alongside the (untrusted) client-asserted `principal`. The server runs every incoming `execute` envelope through its configured `IAuthorizer`; a `false` return causes the server to reply with `err|unauthorized` (callId echoed). The default authorizer permits everything. @@ -213,7 +195,7 @@ If a `Completion` is destroyed with an unhandled error (no `.onError` was attach ### Registry & type erasure `morph::model::ModelTraits` and `morph::model::ActionTraits` are specialised by registration macros. They provide: -- String type IDs for protocol routing. +- String type IDs for protocol routing — the string literals passed as the `NAME` argument to `BRIDGE_REGISTER_MODEL(M, "MyModel")` / `BRIDGE_REGISTER_ACTION(M, A, "MyAction")` in the `.cpp` that owns the model. These strings travel in the wire envelope's `modelType`/`actionType` fields and key the server's dispatch tables. - Glaze-based `toJson` / `fromJson` for actions and results. `morph::model::detail::ActionDispatcher` maps `(modelTypeId, actionTypeId)` → a runner lambda that downcasts `IModelHolder`, calls `model.execute(action)`, and returns the result as JSON. @@ -237,7 +219,7 @@ Bridge ──holds──► weak_ptr (does NOT kee **RAII lifetime:** When `BridgeHandler` goes out of scope, its destructor calls `Bridge::deregisterHandler`, which tells the backend to destroy the model instance and removes the stale `weak_ptr` from the Bridge's list. No manual cleanup is required from application code. -**Destruction order is now safe either way:** the `BridgeHandler` also holds a `weak_ptr` liveness token published by the `Bridge` (`Bridge::liveness()`; the token is destroyed with the bridge). The destructor deregisters only if that token is still live — if the `Bridge` was destroyed first, the token has expired and the handler skips deregistration instead of dereferencing a dangling `Bridge&`. Destroying a bridge before its handlers is still discouraged, but is defined behaviour rather than a use-after-free. +**Destruction order is safe either way:** the `BridgeHandler` also holds a `weak_ptr` liveness token published by the `Bridge` (`Bridge::liveness()`; the token is destroyed with the bridge). The destructor deregisters only if that token is still live — if the `Bridge` was destroyed first, the token has expired and the handler skips deregistration instead of dereferencing a dangling `Bridge&`. Destroying a bridge before its handlers is still discouraged, but is defined behaviour rather than a use-after-free. **`atomic currentId`:** Every call to `executeVia` reads `binding->currentId` to find out which backend-assigned ID to use. The value is an atomic so it can be updated during a backend switch without holding the bridge mutex for the duration of every execute call. A value of `0` is the sentinel for "not bound" — `executeVia` returns an immediate error in that case. @@ -302,11 +284,11 @@ morph::offline::NetworkMonitor monitor{ | `drain()` | Return all pending items in enqueue order; does **not** remove them | | `markDone(id)` | Remove item by id; no-op if unknown | -`drain()` returning items without removing them is deliberate — items survive a crash between `drain()` and `markDone()`. A SQL-backed implementation (not in this repository) can persist items across process restarts by storing them in a table with a UNIQUE constraint on the payload. +`drain()` returning items without removing them is deliberate — items survive a crash between `drain()` and `markDone()`. A SQL-backed implementation can persist items across process restarts by storing them in a table with a UNIQUE constraint on the payload. `InMemoryOfflineQueue` implements the interface with a `std::deque` protected by a mutex. It does not deduplicate. -### Action log (issue #3) — ordered, coalescing, identity-aware execution history +### Action log — ordered, coalescing, identity-aware execution history `morph::journal` records executed actions as an ordered, replayable log, distinct in purpose from `IOfflineQueue` above: `IOfflineQueue` holds pending writes awaiting retry and deletes them once delivered; the action log is a permanent audit/replay trail — entries are never removed by the framework. @@ -314,7 +296,6 @@ morph::offline::NetworkMonitor monitor{ **Set the sink once, in `main()` — every model uses it automatically.** `morph::journal::setActionLog(log)` installs a process-wide default. `ModelFactory::create()` — the factory behind every ordinary model registration, local *or* remote — attaches that default to each new instance automatically (empty `entityKey`). No per-model, per-handler, or per-backend wiring is required; `RemoteServer`-owned instances get it exactly the same way, since they're constructed through the same factory. `defaultActionLog()` reads the current sink back; `ScopedActionLog` (RAII, mirrors `morph::log::ScopedLoggerOverride`) installs one temporarily and restores the previous one on scope exit — the tool tests use it to avoid leaking a sink across test cases. - Application code that needs a specific instance identity (e.g. per-account auditing) can still call `IModelHolder::attachActionLog(log, contextKey)` explicitly on that instance — an explicit call always overrides the default, and is the seam `HandlerBinding::contextKey`/`RemoteServer::setLogProvider` (below) build on for the remote case. Recording itself happens at the two call sites that are the *only* two places `Model::execute()` is ever invoked in the whole codebase: | Site | Topology | @@ -330,11 +311,11 @@ Because these are mutually exclusive per topology, recording is automatically se **`SessionLog`** (`journal.hpp`) is where coalescing actually happens. It keeps full, uncoalesced history in memory (the raw material for `undoLast()`), and `checkpoint(durableSink)` reduces everything appended since the last checkpoint by `(modelType, entityKey, actionType)` — keeping only the latest entry where `coalesce == true`, every entry otherwise — before forwarding the reduced set to the real sink. `undoLast()` needs no inverse operations: it drops the most recent entry and calls `journal::replay()` over what remains, reusing the same `ActionDispatcher`/`ModelRegistryFactory` `RemoteServer` already relies on for dispatch. This is not a workaround — a model's entire state genuinely is "initial state plus its ordered actions replayed," so reconstructing it by replay is the direct statement of that fact, not a special case. -**Replay/undo never records into the live default log.** `journal::replay()` builds the reconstructed holder through `ModelRegistryFactory::create`, which (like every factory-built model) auto-attaches the process-wide default action log. Before replaying, `replay()` immediately **detaches** it (`attachActionLog(nullptr, {})`), so re-running the recorded actions does not re-record each one into the live sink — which would corrupt the very audit trail being read from. The reconstructed holder is a detached, throwaway artifact. +**Replay/undo never records into the live default log.** `journal::replay()` builds the reconstructed holder through `ModelRegistryFactory::create`, which (like every factory-built model) auto-attaches the process-wide default action log. Before replaying, `replay()` immediately **detaches** it (`attachActionLog(nullptr, {})`), so re-running the recorded actions does not re-record each one into the live sink — which would corrupt the very audit trail being read from. The suppression is scoped to the replay pass only: once replay finishes, the reconstructed instance becomes the live model, and any new action executed on it goes through the normal dispatch path (with the log attached) and is recorded as a new entry appended after the surviving entries — e.g. journal `[A, B, C]`, undo `C`, execute `D` leaves `[A, B, D]`. **Remote-mode per-instance identity** (`RemoteServer::setLogProvider`) is the advanced escape hatch for when the global default isn't granular enough: `RemoteServer` owns the actual model instances behind any remote/simulated-remote client, so it is the only place able to attach a *different* log (or a specific `entityKey`) to a *specific* instance. `HandlerBinding::contextKey` (client-side) travels through the `register` wire envelope's `contextKey` field; if a `LogProvider` is installed, `RemoteServer` calls it with `(modelType, contextKey)` and attaches whatever `IActionLog` it returns (or nothing, if it returns `nullptr` or no `contextKey` was sent) before the instance ever executes an action — overriding whatever the global default would have attached. -**Not yet built** (see the design note linked from issue #3): the outbox pattern an integration against a model that also owns its own durable store would need (to avoid the log and the store's committed state silently diverging) — see `examples/bank`, which demonstrates `setActionLog` end to end against SQLite-backed models but writes to its own DB and the audit log as two independent steps, not one atomic outbox write. A Kafka-backed sink (dropped for now; the `IActionLog` interface is designed so one can be added later without touching call sites) and any read-model built on top of it are noted as future work. +**Not yet built** : the outbox pattern an integration against a model that also owns its own durable store would need (to avoid the log and the store's committed state silently diverging) — see `examples/bank`, which demonstrates `setActionLog` end to end against SQLite-backed models but writes to its own DB and the audit log as two independent steps, not one atomic outbox write. ### SyncWorker @@ -478,7 +459,7 @@ handler.reset(); ## Exact values, units, and schema-driven forms Three headers extend actions from "any aggregate" to *self-describing* -aggregates a client can build its GUI from at runtime: `rational.hpp` (exact +aggregates a client can build its GUI from a runtime: `rational.hpp` (exact numbers), `quantity.hpp` (unit-tagged, optionally-empty values) and `forms.hpp` (JSON Schema generation). Nothing else in the framework includes them — they are an opt-in layer that composes with registration, validators, @@ -594,6 +575,76 @@ Two further field types follow the same one-kind-of-empty pattern: cannot fetch); the QML client fetches them live over the same in-process wire it submits on. +## Header map + +The headers are grouped into per-sub-domain subdirectories that mirror the +namespaces. Design specs live under `docs/spec//` with the same +folder names. + +### Library headers (`include/morph/`) + +#### `core/` — async core, registry, bridge, backends, wire + +| Header | Responsibility | +|---|---| +| `core/logger.hpp` | `LogLevel`, log configuration and level helpers; internals in `morph::log::detail` | +| `core/executor.hpp` | `IExecutor`, `ThreadPoolExecutor`, `MainThreadExecutor` (`morph::exec::`) | +| `core/strand.hpp` | `ModelId`, `ModelIdHash`, `StrandExecutor` — serialises tasks per model (`morph::exec::detail::`) | +| `core/completion.hpp` | `CompletionState` (detail) + `Completion` (public) — result handle | +| `core/model.hpp` | `IModelHolder`, `ModelHolder`, `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()` (detail). Registration macros `BRIDGE_REGISTER_MODEL`, `BRIDGE_REGISTER_ACTION` (optional 4th `Loggable` argument), `BRIDGE_REGISTER_VALIDATOR` are defined here at file scope. | +| `core/backend.hpp` | `LocalBackend` (public) + `ActionCall`, `IBackend` (detail), including the non-breaking `registerModelWithContext()` default method | +| `core/remote.hpp` | `RemoteServer` (with `setLogProvider()`), `SimulatedRemoteBackend` (`morph::backend::`) | +| `core/bridge.hpp` | `Bridge`, `BridgeHandler` (public) + `HandlerBinding` (carrying `contextKey`), `MemberPointerTraits` (detail) | +| `core/wire.hpp` | `Envelope`, `encode`/`decode`, `kMaxEnvelopeBytes` (`morph::wire::`) — the JSON wire envelope and length-bounded framing between any client and `RemoteServer` | + +#### `journal/` — ordered, replayable action log + +| Header | Responsibility | +|---|---| +| `journal/action_log.hpp` | `LogEntry`, `IActionLog`, `InMemoryActionLog`, `toJson`/`fromJson`, `SerializationError`, `setActionLog`, `defaultActionLog`, `ScopedActionLog` (`morph::journal::`) — the durable-sink interface and the process-wide default sink, with zero dependency on `core/model.hpp`/`core/registry.hpp` | +| `journal/journal.hpp` | `SessionLog`, `replay()` (`morph::journal::`) — full-fidelity session log with `checkpoint()` coalescing and `undoLast()`, built on `action_log.hpp` + the existing `ActionDispatcher`/`ModelRegistryFactory` | +| `journal/file_action_log.hpp` | `FileActionLog` (`morph::journal::`) — append-only NDJSON `IActionLog`, `flush()` fsyncs | + +#### `offline/` — connectivity + replay + +| Header | Responsibility | +|---|---| +| `offline/network_monitor.hpp` | `NetworkMonitorConfig`, `NetworkMonitor` — background probe thread, online/offline state machine | +| `offline/offline_queue.hpp` | `IOfflineQueue`, `QueueItem`, `InMemoryOfflineQueue` — durable write queue abstraction | +| `offline/sync_worker.hpp` | `SyncWorker`, `SyncResult` — drains offline queue on reconnect via caller-supplied replay | +| `offline/reconnect_coordinator.hpp` | `ReconnectCoordinator`, `ReconnectOutcome`, `ReconnectCoordinatorConfig` — sequences reconnect → activate → bind → replay on connectivity return; all side effects injected via `Deps` | + +#### `session/` — per-call context + authentication + +| Header | Responsibility | +|---|---| +| `session/session.hpp` | `Context`, `IAuthorizer`, `AllowAllAuthorizer`, `allowAllAuthorizer`, `current` (`morph::session::`) — per-call session bag carried through the bridge to the model, plus the server-side authorization seam; internals in `morph::session::detail` (`ScopedContext`, thread-local current context) | +| `session/session_auth.hpp` | `SessionToken`, `TokenIssuer`, `TokenVerifier`, `SigningAuthorizer`, `MacFunction`, `hmacSha256`, `AuthError` (`morph::session::`) — opt-in signed bearer tokens and a verifying `IAuthorizer`; self-contained reference HMAC-SHA256 in `morph::session::detail`. See `docs/spec/security.md`. | + +#### `forms/` — JSON-Schema generation for auto-built GUIs + +| Header | Responsibility | +|---|---| +| `forms/forms.hpp` | `schemaJson()`, `allRequiredEngaged()`, `EmptyCapableField` (`morph::forms::`) — JSON Schema per action with derived `required`, `x-decimalPlaces`, `x-order`, `x-options*` | +| `forms/choice.hpp` | `Choice`, `FixedString`, `isChoice` (`morph::forms::`) — a field whose options are the result of executing the named action; surfaces as `x-optionsAction`/`x-optionValue`/`x-optionLabel` in schemas, renders as a combo box | + +#### `util/` — exact values, units, time + +| Header | Responsibility | +|---|---| +| `util/rational.hpp` | `Rational`, `DecimalPlaces`, `RationalError` (`morph::math::`) — exact int64 rational arithmetic with a decimal-precision tag; Glaze wire codec (`{"num","den","dp"}`, canonicalised on read) and `std::formatter` | +| `util/quantity.hpp` | `Quantity`, `UnitMeta`, `UnitTraits` (`morph::units::`) — unit-tagged optional value over `Rational`; units are application enum NTTPs, schemas get `ExtUnits` automatically. See `docs/spec/util/quantity_type.md` for the full design. | +| `util/datetime.hpp` | `DateTime`, `Timestamp` (`morph::time::`) — UTC instant (ms precision) with a strict ISO-8601 wire codec (malformed input is a read *error*) and the optionally-empty field wrapper; schemas carry `"format": "date-time"` | + +### Qt integration headers (`include/morph/qt/`) + +| Header | Responsibility | +|---|---| +| `qt_executor.hpp` | `QtExecutor` — posts callables via `QMetaObject::invokeMethod` | +| `qt_websocket_backend.hpp` | `QtWebSocketBackend` — WebSocket client; implements `IBackend` | +| `qt_websocket_server.hpp` | `QtWebSocketServer` — QObject server; forwards messages to `RemoteServer` | + ## Known limitations ### Validators do not run server-side @@ -623,7 +674,7 @@ Callbacks (`onOffline`, `onOnline`) run directly on the probe thread. A blocking The `cbExec` pointer on `CompletionState` should be set before `setValue` / `setException` is called (or before `attachThen` / `attachOnError` if the state is already ready). If `cbExec` is null when a callback would fire, that callback is silently dropped — no error is raised. This is enforced by the `Completion` constructor, which accepts an `IExecutor*`; the hazard only arises when using `CompletionState` directly (an internal type). -An abandoned **error**, however, is no longer silenced by a null executor: `onErrAttached` (the flag that suppresses the destructor's orphan log) is only set when an executor actually exists to deliver the handler. With a null executor the error handler is never posted, so `onErrAttached` stays `false` and `~CompletionState` still logs the exception through the orphan logger rather than losing it. +An abandoned **error**, however, is not silenced by a null executor: `onErrAttached` (the flag that suppresses the destructor's orphan log) is only set when an executor actually exists to deliver the handler. With a null executor the error handler is never posted, so `onErrAttached` stays `false` and `~CompletionState` still logs the exception through the orphan logger rather than losing it. ### `MainThreadExecutor::runFor` does not drain on timeout diff --git a/docs/planned/backend_changed_dispatch.md b/docs/planned/backend_changed_dispatch.md index df9f238..ef964c0 100644 --- a/docs/planned/backend_changed_dispatch.md +++ b/docs/planned/backend_changed_dispatch.md @@ -1,7 +1,7 @@ # Compile-time `onBackendChanged` dispatch (planned) > **Status: planned — not yet implemented.** This spec extends -> [backend.md](../spec/backend.md) and [bridge.md](../spec/bridge.md). It removes the RTTI +> [backend.md](../spec/core/backend.md) and [bridge.md](../spec/core/bridge.md). It removes the RTTI > `dynamic_cast` sweep in `LocalBackend::notifyBackendChanged` documented as a > limitation in `backend.md`. See [todo.md](../todo.md). @@ -140,14 +140,14 @@ interaction is identical. ## Cross-references -- [backend.md](../spec/backend.md) — `LocalBackend::notifyBackendChanged`, the +- [backend.md](../spec/core/backend.md) — `LocalBackend::notifyBackendChanged`, the `IBackendChangedSink` limitation ("uses RTTI over every model under the registry lock") this removes, and register/deregister where `_changeAware` is maintained. -- [bridge.md](../spec/bridge.md) — `switchBackend` → `notifyBackendChanged`, and the +- [bridge.md](../spec/core/bridge.md) — `switchBackend` → `notifyBackendChanged`, and the posted-to-strand `onBackendChanged` contract this preserves. - [concurrency_and_lifetimes.md](../spec/concurrency_and_lifetimes.md) — the strand-serialised, fires-once, on-the-new-instance guarantees that are unchanged by this refactor. -- [registry.md](../spec/registry.md) — `ModelHolder`, `BackendChangedNotifiable`, the +- [registry.md](../spec/core/registry.md) — `ModelHolder`, `BackendChangedNotifiable`, the `BackendChangedMixin` compile-time detection reused here. diff --git a/docs/planned/connection_scoped_cleanup.md b/docs/planned/connection_scoped_cleanup.md index d659d66..4479f97 100644 --- a/docs/planned/connection_scoped_cleanup.md +++ b/docs/planned/connection_scoped_cleanup.md @@ -2,9 +2,9 @@ > **Status: planned — not yet implemented.** This spec closes the "no > connection-scoped cleanup — orphaned models leak" limitation documented in -> [backend.md](../spec/backend.md): models registered over a connection that +> [backend.md](../spec/core/backend.md): models registered over a connection that > dies stay registered on the server forever. It extends `RemoteServer` and -> `QtWebSocketServer` ([backend.md](../spec/backend.md)) with an **opt-in +> `QtWebSocketServer` ([backend.md](../spec/core/backend.md)) with an **opt-in > connection scope**, and complements > [transport_limits.md](transport_limits.md) and > [instance_authorization.md](instance_authorization.md). See @@ -13,7 +13,7 @@ ## The gap `RemoteServer` is connection-blind by design: `handle()` takes a message and a -reply callback, nothing more ([backend.md](../spec/backend.md), +reply callback, nothing more ([backend.md](../spec/core/backend.md), [transport_limits.md](transport_limits.md)). Models it creates on `register` live in `_models` until an explicit `deregister` arrives. Nothing ever arrives for a connection that dies: @@ -24,7 +24,7 @@ for a connection that dies: stays live until process exit. - **The client cannot reliably clean up either.** `QtWebSocketBackend`'s `deregisterModel` is deliberately fire-and-forget (no nested event loop in a - destructor — [backend.md](../spec/backend.md) design decisions), and a + destructor — [backend.md](../spec/core/backend.md) design decisions), and a crashed or power-cut client never sends anything at all. The header itself warns: "an undelivered or lost `deregister` leaves the model registered on the server indefinitely." @@ -39,7 +39,7 @@ for a connection that dies: The client side already treats model ids as connection-scoped: on disconnect `QtWebSocketBackend` cancels its pending calls, and after a reconnect the `Bridge` re-registers every live handler through the reconnect handler -([backend.md](../spec/backend.md)) — fresh registrations, fresh ids. Old ids +([backend.md](../spec/core/backend.md)) — fresh registrations, fresh ids. Old ids are never reused by a correct client. Only the server side pretends they are still meaningful. @@ -130,7 +130,7 @@ it merely prevents *new* lookups, exactly like an explicit `deregister`. reclaims the connection's instances. - **[instance_authorization.md](instance_authorization.md):** opaque id generation is orthogonal; the recorded owner is erased with the instance. -- **`RemoteServer::LogProvider` logs ([journal.md](../spec/journal.md)):** +- **`RemoteServer::LogProvider` logs ([journal.md](../spec/journal/journal.md)):** erasing the holder releases its reference to the attached `IActionLog`; the log object itself is shared and host-owned, so recorded history survives — consistent with "entries are never removed by the framework". @@ -142,7 +142,7 @@ it merely prevents *new* lookups, exactly like an explicit `deregister`. - **No session resumption.** Model ids stay connection-scoped; a reconnecting client re-registers and gets fresh ids — that is the *existing* contract - (`Bridge` re-registration on reconnect, [backend.md](../spec/backend.md)), + (`Bridge` re-registration on reconnect, [backend.md](../spec/core/backend.md)), which this spec makes the server honour rather than changes. A host that wants ids to survive reconnects must keep using the unscoped path. - **No lease/TTL expiry.** Unscoped registrations (embedded hosts, @@ -177,7 +177,7 @@ it merely prevents *new* lookups, exactly like an explicit `deregister`. ## Cross-references -- [backend.md](../spec/backend.md) — the documented limitation this closes; +- [backend.md](../spec/core/backend.md) — the documented limitation this closes; `RemoteServer` register/deregister/execute paths; the fire-and-forget `deregisterModel` design decision; the reconnect-handler re-registration contract the cleanup relies on. diff --git a/docs/planned/drift_guard.md b/docs/planned/drift_guard.md index 04367ec..54cd97e 100644 --- a/docs/planned/drift_guard.md +++ b/docs/planned/drift_guard.md @@ -13,7 +13,7 @@ code**. [todo.md](../todo.md) enumerates the actual drift that shipped: - the `authenticate` "principal-clearing" behavior a doc described wrongly, - the false "unknown keys ignored" claim that predated the real - `error_on_unknown_keys = false` behavior ([wire.md](../spec/wire.md)), + `error_on_unknown_keys = false` behavior ([wire.md](../spec/core/wire.md)), - a stale `runFor` comment, - a stale `AuthError` cardinality (the enum grew, the doc did not). @@ -44,9 +44,9 @@ The facts, drawn from the audit's failure classes: | Fact class | Examples (verified real symbols) | Source of truth in code | |---|---|---| | **Enum cardinalities** | `AuthError` (`Malformed`/`BadSignature`/`Expired`/`NotYetValid`), `LogLevel` (5: `debug`/`info`/`warn`/`error`/`off`), `ReconnectOutcome` (3), `Metric` (once [observability.md](observability.md) lands) | the `enum class` declaration in the header | -| **Key constants** | `kMaxEnvelopeBytes` (`8 * 1024 * 1024`, [wire.md](../spec/wire.md)), `kMaxDecimalPlaces` ([ARCHITECTURE.md](../ARCHITECTURE.md), `morph::math`), `kClockSkewMs` (60s, [security.md](../spec/security.md)) | the `constexpr` definition | -| **Canonical error strings** | `BackendChangedError` = `"backend changed before completion resolved"`, `BridgeDestroyedError`, `DisconnectedError` ([backend.md](../spec/backend.md)); `err "unauthorized"`, `"model not found"`, `"register requires a typeId"` | the string literal in the throw/reply site | -| **Glaze behavior flags** | `error_on_unknown_keys = false` on `wire::decode` ([wire.md](../spec/wire.md)); duplicate-key last-wins (behavioral, asserted by a pinned test) | the `glz::read<{...}>` options at the call site | +| **Key constants** | `kMaxEnvelopeBytes` (`8 * 1024 * 1024`, [wire.md](../spec/core/wire.md)), `kMaxDecimalPlaces` ([ARCHITECTURE.md](../ARCHITECTURE.md), `morph::math`), `kClockSkewMs` (60s, [security.md](../spec/security.md)) | the `constexpr` definition | +| **Canonical error strings** | `BackendChangedError` = `"backend changed before completion resolved"`, `BridgeDestroyedError`, `DisconnectedError` ([backend.md](../spec/core/backend.md)); `err "unauthorized"`, `"model not found"`, `"register requires a typeId"` | the string literal in the throw/reply site | +| **Glaze behavior flags** | `error_on_unknown_keys = false` on `wire::decode` ([wire.md](../spec/core/wire.md)); duplicate-key last-wins (behavioral, asserted by a pinned test) | the `glz::read<{...}>` options at the call site | The manifest is the one place a human states "the spec claims X"; the code is scanned for the actual value; CI fails if they diverge. When a value legitimately @@ -134,14 +134,14 @@ as the Doxygen `FAIL_ON_WARNINGS` gate `CLAUDE.md` already documents. ## Cross-references -- [wire.md](../spec/wire.md) — `kMaxEnvelopeBytes`, the +- [wire.md](../spec/core/wire.md) — `kMaxEnvelopeBytes`, the `error_on_unknown_keys = false` flag, and the duplicate-key behavior — three of the exact facts the audit found drifted. - [security.md](../spec/security.md) — `AuthError` cardinality and `kClockSkewMs`, audit-class facts this pins. -- [backend.md](../spec/backend.md) — the canonical error-type `what()` strings and +- [backend.md](../spec/core/backend.md) — the canonical error-type `what()` strings and reply strings (`"unauthorized"`, `"model not found"`) pinned as literals. -- [logger.md](../spec/logger.md) — `LogLevel`'s 5-member cardinality. +- [logger.md](../spec/core/logger.md) — `LogLevel`'s 5-member cardinality. - [api_stability.md](api_stability.md) — the complementary compatibility guard; drift-guard checks *doc accuracy*, api-stability checks *API compatibility*. - `CLAUDE.md` — the "specs are authoritative; update the spec on any change" rule diff --git a/docs/planned/durable_offline_queue_impl.md b/docs/planned/durable_offline_queue_impl.md index 894e6fd..c3dc916 100644 --- a/docs/planned/durable_offline_queue_impl.md +++ b/docs/planned/durable_offline_queue_impl.md @@ -5,11 +5,11 @@ > store that implements the `QueueItem::attempts` / `setAttempts` persistence > contract that spec defines (and thereby makes its `SyncWorker`-side > `DeadLetterSink` fire across restarts) — and extends -> [offline.md](../spec/offline.md). See [todo.md](../todo.md). +> [offline.md](../spec/offline/offline.md). See [todo.md](../todo.md). ## The gap -Only `InMemoryOfflineQueue` ships. [offline.md](../spec/offline.md) is blunt about +Only `InMemoryOfflineQueue` ships. [offline.md](../spec/offline/offline.md) is blunt about it: "`InMemoryOfflineQueue` loses everything on exit. A durable/SQL-backed `IOfflineQueue` is the caller's to write (the interface is designed for it, but no implementation is provided here)." @@ -32,7 +32,7 @@ not from an `IOfflineQueue`.) Ship one reference durable `IOfflineQueue` that persists `payload`, `idempotencyKey`, and `attempts` across process restarts, correctly implementing -the [offline.md](../spec/offline.md) queue contract and the +the [offline.md](../spec/offline/offline.md) queue contract and the [durable_queue.md](durable_queue.md) `setAttempts` write-back — so B1's cross-restart dead-lettering actually works without every host re-implementing the store. It is an **optional** component: `InMemoryOfflineQueue` stays the @@ -77,7 +77,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS ix_queue_idem - `id` is `AUTOINCREMENT`, so ids are never reused and a re-opened queue re-presents each row under its **stored** `id` (stable across restarts in this implementation). It remains **queue-local** — the - [offline.md](../spec/offline.md) contract ("Queue-local — not a + [offline.md](../spec/offline/offline.md) contract ("Queue-local — not a cross-subsystem key") allows but never promises stability, so callers must not rely on it. Cross-restart identity is carried by `idempotency_key`, not `id`. - The partial unique index on a non-empty `idempotency_key` gives the insert-time @@ -87,7 +87,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS ix_queue_idem re-enqueue of the same key is a no-op (`INSERT ... ON CONFLICT DO NOTHING`) that returns the existing row's id. This is a deliberate, implementation- specific strengthening: the base `IOfflineQueue` contract only *stores* the - key and never enforces uniqueness ([offline.md](../spec/offline.md)). Empty + key and never enforces uniqueness ([offline.md](../spec/offline/offline.md)). Empty keys (the default) are exempt, so keyless items behave exactly as the in-memory queue does. @@ -115,14 +115,14 @@ protected: - **Crash-safety matches the interface contract.** `drain()` never deletes, so a crash between `drain()` and `markDone()` loses nothing — items reappear on the - next `drain()` ([offline.md](../spec/offline.md)'s "`drain` is non-destructive" + next `drain()` ([offline.md](../spec/offline/offline.md)'s "`drain` is non-destructive" decision). Each write (`enqueue`, `markDone`, `setAttempts`, `setIdempotencyKey`) is a committed transaction; SQLite's WAL provides the durability. - **Thread-safety matches `InMemoryOfflineQueue`.** All operations serialise on an internal mutex around the connection, so it is safe to share between the application's enqueue-on-failure write path and `SyncWorker`'s drain/replay read - path ([offline.md](../spec/offline.md)'s "Ownership: who enqueues"). + path ([offline.md](../spec/offline/offline.md)'s "Ownership: who enqueues"). ### A plain-file variant @@ -136,7 +136,7 @@ over an append-only NDJSON file (mirroring `FileActionLog`'s shape, - `markDone` and `setAttempts` are recorded as tombstone/update lines and compacted on open (last-write-wins per id), so the file is self-healing across restarts and tolerates a torn trailing line the same way `FileActionLog` does - ([journal.md](../spec/journal.md), "Torn-write tolerance"). + ([journal.md](../spec/journal/journal.md), "Torn-write tolerance"). - Same `IOfflineQueue` surface; the choice between SQLite and file is a host decision, not a contract difference. @@ -152,7 +152,7 @@ With this queue installed as the `IOfflineQueue` behind `SyncWorker` 3. `SyncWorker` resumes from the stored count, so the 5-attempt budget is cumulative across restarts and the item genuinely dead-letters (invoking the `DeadLetterSink`) instead of retrying forever — closing the exact failure mode - [offline.md](../spec/offline.md) documents. + [offline.md](../spec/offline/offline.md) documents. ## What this does not do @@ -165,12 +165,12 @@ With this queue installed as the `IOfflineQueue` behind `SyncWorker` `InMemoryOfflineQueue` are untouched and dependency-free. - **No dedup enforcement beyond insert.** The unique index deduplicates a re-enqueue of the same key at write time, but replay-time at-most-once is still - the consumer's job via `idempotencyKey` ([offline.md](../spec/offline.md) — "the + the consumer's job via `idempotencyKey` ([offline.md](../spec/offline/offline.md) — "the queue only *stores* it"). The queue does not become an exactly-once engine. - **Not the transactional outbox.** A model with its own store that needs the log and its state to commit atomically uses [outbox.md](outbox.md); this queue is the *offline write buffer*, a distinct concern. -- **No conflict resolution.** As [offline.md](../spec/offline.md) states, that +- **No conflict resolution.** As [offline.md](../spec/offline/offline.md) states, that lives in the model's `onBackendChanged()`, not the queue. ## Testing (planned) @@ -196,7 +196,7 @@ With this queue installed as the `IOfflineQueue` behind `SyncWorker` `setAttempts` write-back hook this queue concretely implements, and the `SyncWorker`-side `DeadLetterSink` whose cross-restart dead-lettering it makes real. -- [offline.md](../spec/offline.md) — `IOfflineQueue`, `QueueItem`, +- [offline.md](../spec/offline/offline.md) — `IOfflineQueue`, `QueueItem`, `InMemoryOfflineQueue`, `enqueue`/`drain`/`markDone`/`setIdempotencyKey`, the non-destructive-`drain` crash-safety contract, `idempotencyKey` dedup, and the "only an in-memory queue ships" limitation this closes. diff --git a/docs/planned/durable_queue.md b/docs/planned/durable_queue.md index d492976..3ef9198 100644 --- a/docs/planned/durable_queue.md +++ b/docs/planned/durable_queue.md @@ -1,7 +1,7 @@ # Durable offline queue & cross-restart dead-lettering (planned) > **Status: planned — not yet implemented.** This spec extends -> [offline.md](../spec/offline.md). It closes the "retry counter is in-memory and resets +> [offline.md](../spec/offline/offline.md). It closes the "retry counter is in-memory and resets > on restart" and "dead-lettering is log-only" limitations documented there. See > [todo.md](../todo.md). @@ -159,11 +159,11 @@ them. ## Cross-references -- [offline.md](../spec/offline.md) — `IOfflineQueue`, `QueueItem`, `InMemoryOfflineQueue`, +- [offline.md](../spec/offline/offline.md) — `IOfflineQueue`, `QueueItem`, `InMemoryOfflineQueue`, `SyncWorker`, the `setIdempotencyKey` hook this mirrors, and the two Failure modes ("retry counter resets on restart", "dead-lettering is log-only") this closes. -- [journal.md](../spec/journal.md) — the permanent audit trail, distinct from the +- [journal.md](../spec/journal/journal.md) — the permanent audit trail, distinct from the transient queue; `idempotencyKey` is the shared token that lets a replay consumer dedup across both. - [error_handling.md](../spec/error_handling.md) — the swallow-and-treat-as-failure diff --git a/docs/planned/graceful_shutdown.md b/docs/planned/graceful_shutdown.md index ea4525c..3b5f97d 100644 --- a/docs/planned/graceful_shutdown.md +++ b/docs/planned/graceful_shutdown.md @@ -3,9 +3,9 @@ > **Status: planned — not yet implemented.** This spec gives a server an > orderly way to stop: refuse new work, finish what is in flight, tell clients > properly, then tear down. It extends `RemoteServer` and `QtWebSocketServer` -> ([backend.md](../spec/backend.md)), addresses at the server layer the +> ([backend.md](../spec/core/backend.md)), addresses at the server layer the > "no graceful drain / `waitIdle`" limitation -> [executor.md](../spec/executor.md) documents, and integrates with +> [executor.md](../spec/core/executor.md) documents, and integrates with > [observability.md](observability.md)'s readiness signal and > [transport_limits.md](transport_limits.md)'s in-flight accounting. See > [todo.md](../todo.md). @@ -21,7 +21,7 @@ Stopping a morph server today is abrupt at every layer: - **Nothing stops new work first.** There is no way to make `RemoteServer` refuse new `register`/`execute` envelopes while letting started work finish; work keeps arriving until the process is torn down under it. - [executor.md](../spec/executor.md) is explicit that `~ThreadPoolExecutor` + [executor.md](../spec/core/executor.md) is explicit that `~ThreadPoolExecutor` drains already-queued tasks but "tasks posted concurrently with or after destruction may be lost" and there is "no graceful drain / `waitIdle`" — the caller must synchronise teardown externally, and has no primitive to do @@ -70,7 +70,7 @@ void beginShutdown(); ([concurrency_and_lifetimes.md](../spec/concurrency_and_lifetimes.md)) apply unchanged — but now trivially, because every queue is empty. This spec deliberately adds **no** `waitIdle` to `IExecutor` - ([executor.md](../spec/executor.md)'s limitation stands for raw executor + ([executor.md](../spec/core/executor.md)'s limitation stands for raw executor users): the drain condition morph can define precisely — "every accepted execute has replied" — lives at the server layer, where the work is counted. @@ -104,10 +104,10 @@ The existing abrupt `close()` remains unchanged for tests and emergencies; - In-flight calls complete normally during the drain window. - New calls fail fast with `"server shutting down"` — for hosts using the offline stack, an ordinary failure the queue retries after the restart - ([offline.md](../spec/offline.md), [durable_queue.md](durable_queue.md)). + ([offline.md](../spec/offline/offline.md), [durable_queue.md](durable_queue.md)). - The socket then closes with `going away` rather than dying. The Qt client backend's auto-reconnect behaves per its existing config - ([backend.md](../spec/backend.md)) and finds the restarted server; + ([backend.md](../spec/core/backend.md)) and finds the restarted server; differentiated client handling of the close reason is a possible later refinement, not part of this spec. @@ -124,7 +124,7 @@ The existing abrupt `close()` remains unchanged for tests and emergencies; connection draining at the LB, DNS, or mesh layer is the deployment's job. - **Not crash safety.** A power cut still interrupts mid-flight work — that is what the durability track ([outbox.md](outbox.md), - [journal.md](../spec/journal.md)) exists for. This spec makes *intentional* + [journal.md](../spec/journal/journal.md)) exists for. This spec makes *intentional* stops clean, nothing more. - **Does not change local mode.** `LocalBackend` lives and dies with the application; there is no server to drain. @@ -147,9 +147,9 @@ The existing abrupt `close()` remains unchanged for tests and emergencies; ## Cross-references -- [backend.md](../spec/backend.md) — `RemoteServer` dispatch and reply paths; +- [backend.md](../spec/core/backend.md) — `RemoteServer` dispatch and reply paths; `QtWebSocketServer::close()`, today's abrupt stop. -- [executor.md](../spec/executor.md) — the drain-on-destruction semantics and +- [executor.md](../spec/core/executor.md) — the drain-on-destruction semantics and the "no graceful drain / `waitIdle`" limitation this addresses one layer up. - [concurrency_and_lifetimes.md](../spec/concurrency_and_lifetimes.md) — the diff --git a/docs/planned/gui_collections_views.md b/docs/planned/gui_collections_views.md index e249db3..5674873 100644 --- a/docs/planned/gui_collections_views.md +++ b/docs/planned/gui_collections_views.md @@ -2,26 +2,26 @@ > **Status: planned — not yet implemented.** This spec introduces the first > **view-schema** document — a descriptor *above* the per-action form schema of -> [forms.md](../spec/forms.md) that composes a *set* of related actions (a query, +> [forms.md](../spec/forms/forms.md) that composes a *set* of related actions (a query, > an edit, a delete) into a list/table or master-detail **screen**. It sits in > Tier 2 of [gui_overview.md](gui_overview.md) and builds on -> [choice.md](../spec/choice.md) (a query action serving rows) and -> [bridge.md](../spec/bridge.md) (per-action dispatch). It extends nothing in the +> [choice.md](../spec/forms/choice.md) (a query action serving rows) and +> [bridge.md](../spec/core/bridge.md) (per-action dispatch). It extends nothing in the > action schema — it is a new, additive top-level document. See > [todo.md](../todo.md). ## The gap The current surface is **one action → one flat form** -([forms.md](../spec/forms.md)). A renderer builds a form from `schemaJson()` +([forms.md](../spec/forms/forms.md)). A renderer builds a form from `schemaJson()` and fires that one action. There is no concept of a *collection*: nothing says "run query action `ListSamples`, show its rows as a table, let a row open the edit form for action `EditSample`, and let a button run `DeleteSample`." Every real CRUD screen — a list you can drill into and edit — must be hand-wired today, even though all three actions are already registered -([registry.md](../spec/registry.md)) and each already generates its own form. +([registry.md](../spec/core/registry.md)) and each already generates its own form. -`Choice` ([choice.md](../spec/choice.md)) already proves the smaller version of +`Choice` ([choice.md](../spec/forms/choice.md)) already proves the smaller version of this: a query action, executed with an empty body, serves rows the renderer maps to `{value, label}`. A view generalises that one pattern — *a query action serves rows* — from a combo box to a full table with per-row actions. @@ -80,7 +80,7 @@ parallel to the action schema's `x-*`): |---|---|---|---| | `v-kind` | top-level | string | `"collection"` (list/table) or `"master-detail"` (list + inline/side editor). The one required discriminator. | | `v-title` | top-level | string | Human title for the screen. Optional; defaults to `v-query`. | -| `v-query` | top-level | string | Type-id of the registered **query action** whose result rows populate the list. Executed with an empty body — the same contract `Choice`'s options action obeys ([choice.md](../spec/choice.md)). | +| `v-query` | top-level | string | Type-id of the registered **query action** whose result rows populate the list. Executed with an empty body — the same contract `Choice`'s options action obeys ([choice.md](../spec/forms/choice.md)). | | `v-rowKey` | top-level | string | Wire field name that uniquely identifies a row (default `"id"`). Used to correlate a row with the edit/delete it opens. | | `v-columns` | top-level | array | Ordered column descriptors. **Optional** — omitted means "derive every column from the row shape" (below). | | ↳ `field` | column | string | Wire field name in the query result row. | @@ -110,14 +110,14 @@ the same way the form does. `v-columns` is the **declare-to-override** escape hatch: supply it to reorder, relabel, hide, or subset the derived columns. This mirrors `optionalFields` in -[forms.md](../spec/forms.md) — inference is the default, a small typed +[forms.md](../spec/forms/forms.md) — inference is the default, a small typed declaration overrides it. ### How a view is declared in C++ (proposed) A view is a compile-time descriptor over already-registered action types, so its references are checked the way `Choice`'s are (i.e. names are opaque NTTPs, -validated at runtime — see [choice.md](../spec/choice.md)'s author obligations): +validated at runtime — see [choice.md](../spec/forms/choice.md)'s author obligations): ```cpp // namespace morph::views — NEW. @@ -135,7 +135,7 @@ BRIDGE_REGISTER_VIEW(SamplesView, "SamplesView"); // NEW macro, parallels ``` The **NEW** `BRIDGE_REGISTER_VIEW` macro (parallel to `BRIDGE_REGISTER_ACTION`, -[registry.md](../spec/registry.md)) specialises a `ViewTraits` with a +[registry.md](../spec/core/registry.md)) specialises a `ViewTraits` with a `typeId()` and the `viewSchemaJson()` body, and registers the view id so a controller can enumerate views the way `Main.qml` enumerates schemas today. No new dispatch path is introduced — the view descriptor is metadata only. @@ -146,12 +146,12 @@ A collection screen performs three ordinary dispatches, each already fully specified: 1. **Populate** — execute `v-query` with an empty body via the normal execute - path ([bridge.md](../spec/bridge.md)); read rows from the result exactly as + path ([bridge.md](../spec/core/bridge.md)); read rows from the result exactly as `Choice` does. The query action is a pure read, so it declares - `Loggable::No` ([registry.md](../spec/registry.md)) — a table refresh is not + `Loggable::No` ([registry.md](../spec/core/registry.md)) — a table refresh is not an audit event. 2. **Edit** — activating a row builds the `v-rowAction` action's form - ([forms.md](../spec/forms.md)) with fields prefilled from `bind`, then fires + ([forms.md](../spec/forms/forms.md)) with fields prefilled from `bind`, then fires it through the same `execute` / reactive `set<>` path as any form. 3. **Delete** — a `scope: "row"` action fires its bound action (e.g. `DeleteSample{ id }`), guarded by `confirm`. @@ -177,8 +177,8 @@ not mandate the split. action-forms. Multi-screen navigation, menus, and cross-screen flow live in [gui_workflows_navigation.md](gui_workflows_navigation.md), not here. - **No new dispatch path or wire change.** Populate/edit/delete are ordinary - action executes over the existing path ([bridge.md](../spec/bridge.md), - [backend.md](../spec/backend.md)); the view document is metadata a renderer + action executes over the existing path ([bridge.md](../spec/core/bridge.md), + [backend.md](../spec/core/backend.md)); the view document is metadata a renderer consumes, never a payload. - **No server-side "query language."** `v-query` names a registered action that returns whatever rows it returns. There is no filtering/sorting/paging protocol @@ -189,10 +189,10 @@ not mandate the split. scope (it would need a subscription channel morph does not have). - **No nested/joined views.** A view references flat action row shapes, matching the flat-actions-only scope of schema generation - ([forms.md](../spec/forms.md)). A row that is itself a collection is not a + ([forms.md](../spec/forms/forms.md)). A row that is itself a collection is not a documented path. - **Membership/staleness not enforced.** Exactly as with `Choice` - ([choice.md](../spec/choice.md)), a row key bound into an edit/delete may be + ([choice.md](../spec/forms/choice.md)), a row key bound into an edit/delete may be stale by the time the action fires; the handler must re-check, since the wire carries only the bound value. @@ -220,15 +220,15 @@ not mandate the split. - [gui_overview.md](gui_overview.md) — Tier 2; the "view/app schema layer above the action schema" this document opens, and the infer-by-default/declare-to- override principle the column derivation follows. -- [forms.md](../spec/forms.md) — the per-action schema each screen composes; the +- [forms.md](../spec/forms/forms.md) — the per-action schema each screen composes; the `x-order` / `ExtUnits` / `x-decimalPlaces` reused for derived columns and the flat-actions-only scope inherited here. -- [choice.md](../spec/choice.md) — the "a query action serves rows" pattern +- [choice.md](../spec/forms/choice.md) — the "a query action serves rows" pattern generalised from a combo box to a table, including the empty-body query contract and the stale-value caveat. -- [bridge.md](../spec/bridge.md) — the execute / reactive `set<>` path the +- [bridge.md](../spec/core/bridge.md) — the execute / reactive `set<>` path the populate/edit/delete dispatches use unchanged. -- [registry.md](../spec/registry.md) — `ActionTraits::typeId()` (the ids the view +- [registry.md](../spec/core/registry.md) — `ActionTraits::typeId()` (the ids the view references), `Loggable::No` for the query action, and the `BRIDGE_REGISTER_ACTION` pattern the proposed `BRIDGE_REGISTER_VIEW` mirrors. - [gui_workflows_navigation.md](gui_workflows_navigation.md) — the next tier up: diff --git a/docs/planned/gui_computed_fields.md b/docs/planned/gui_computed_fields.md index e554430..80c9591 100644 --- a/docs/planned/gui_computed_fields.md +++ b/docs/planned/gui_computed_fields.md @@ -2,8 +2,8 @@ > **Status: planned — not yet implemented.** This spec is part of the GUI > enhancement program ([gui_overview.md](gui_overview.md), Tier 1). It extends the -> `x-*` vocabulary of [forms.md](../spec/forms.md) and builds on the reactive -> `subscribe`/`set<>` draft path of [bridge.md](../spec/bridge.md). It describes +> `x-*` vocabulary of [forms.md](../spec/forms/forms.md) and builds on the reactive +> `subscribe`/`set<>` draft path of [bridge.md](../spec/core/bridge.md). It describes > the intended behavior; the code does not implement it yet. See > [todo.md](../todo.md). @@ -25,7 +25,7 @@ total must either: Neither is right. The derivation is a first-class property of the action, the renderer should show it **live but locked**, and the server must recompute it **authoritatively** and never trust the client's copy. The reactive engine -already recomputes on every `set<>` ([bridge.md](../spec/bridge.md)); what is +already recomputes on every `set<>` ([bridge.md](../spec/core/bridge.md)); what is missing is a way to *declare* the derivation so the schema exposes it and both ends compute the same value. @@ -41,7 +41,7 @@ sibling fields. From that single declaration: arrived on the wire — the computed field is never trusted from the client. Where the computation is numeric it reuses the exact `Rational`/`Quantity` -arithmetic ([rational.md](../spec/rational.md), [quantity_type.md](../spec/quantity_type.md)) +arithmetic ([rational.md](../spec/util/rational.md), [quantity_type.md](../spec/util/quantity_type.md)) already in the library, so the client's displayed value and the server's stored value are identical to the last digit. @@ -51,7 +51,7 @@ value are identical to the last digit. An action opts in with a `static constexpr` map from a computed member to a pure function of the action, declared next to `optionalFields` -([forms.md](../spec/forms.md)): +([forms.md](../spec/forms/forms.md)): ```cpp struct LineItem { @@ -73,11 +73,11 @@ struct LineItem { - **`computed(dst, {inputs...}, fn)`** (NEW) binds a destination member, its input members, and a **pure** derivation `fn(const A&) -> ValueOfDst`. Members are pointer-to-member NTTPs recovered via `MemberPointerTraits` - ([bridge.md](../spec/bridge.md)), so a renamed field is a compile error and the + ([bridge.md](../spec/core/bridge.md)), so a renamed field is a compile error and the input list is type-checked. - **`computeList(...)`** composes the entries into a `ComputeList<...>` value the framework detects with a `HasComputedFields` concept (NEW), mirroring - `HasOptionalFields` ([forms.md](../spec/forms.md)). + `HasOptionalFields` ([forms.md](../spec/forms/forms.md)). - **`recomputeAll(action)`** (NEW) is the single evaluator: it applies every entry's `fn` and writes the result into the destination member in place. This is the exact same function the client and the server call — no second copy of the @@ -93,9 +93,9 @@ server-only computation — is **not** a computed field and stays in the model's When the inputs and destination are `Quantity`, `fn` is written in ordinary `Quantity` arithmetic — `s.qty * s.price` uses `Quantity::operator*`, whose result -unit is deduced from the operands ([quantity_type.md](../spec/quantity_type.md)) -and whose value is an exact `math::Rational` ([rational.md](../spec/rational.md)). -Before evaluation, `reconcileDeclaredPrecision` ([forms.md](../spec/forms.md)) +unit is deduced from the operands ([quantity_type.md](../spec/util/quantity_type.md)) +and whose value is an exact `math::Rational` ([rational.md](../spec/util/rational.md)). +Before evaluation, `reconcileDeclaredPrecision` ([forms.md](../spec/forms/forms.md)) has already normalised the inputs to their declared precision, and the result is retagged to the destination field's declared precision afterward. So the client's displayed total and the server's stored total are computed from identical inputs @@ -105,11 +105,11 @@ unit-conversion path already guarantees. An empty (unengaged) input propagates: if any declared input has `hasValue() == false`, the destination is left **unengaged** rather than computed from a missing operand, consistent with how `allRequiredEngaged` treats -empty-capable fields ([forms.md](../spec/forms.md)). +empty-capable fields ([forms.md](../spec/forms/forms.md)). ### Schema emission — `x-computed` and `x-readonly` -`mergeSchemaExtras` ([forms.md](../spec/forms.md)) gains a step that walks +`mergeSchemaExtras` ([forms.md](../spec/forms/forms.md)) gains a step that walks `A::computedFields` and patches each destination **property node** (sibling of its `$ref`, exactly like `x-order`) with `x-computed` and `x-readonly`. The property still appears in the schema so the renderer can display it; the annotations tell @@ -131,7 +131,7 @@ engages it, or leaves it empty when an input is empty.) Input names in pointer-to-member the same way `x-order` is derived, so a renderer that wants to recompute optimistically knows which sibling changes should trigger a redisplay. -New keys this spec adds to the [forms.md](../spec/forms.md) renderer-contract +New keys this spec adds to the [forms.md](../spec/forms/forms.md) renderer-contract table (all additive, all optional): | Key | Where | JSON type | Meaning / renderer obligation | @@ -148,10 +148,10 @@ server-returned value. Either way the *authority* is the server's recomputation. ### Client-side: live recompute on the reactive path -On the `set<>` reactive draft path ([bridge.md](../spec/bridge.md)), each accepted +On the `set<>` reactive draft path ([bridge.md](../spec/core/bridge.md)), each accepted field update runs `recomputeAll` on the draft snapshot **before** the readiness check and fire. Because `set<>` already re-fires on every ready patch with -coalescing ([bridge.md](../spec/bridge.md)), the computed field is refreshed live +coalescing ([bridge.md](../spec/core/bridge.md)), the computed field is refreshed live as the user edits its inputs, with no extra machinery — the engine "already recomputes on `set<>`," and this step is what makes the derived value part of that recomputation. A computed member is never a `set<>` target itself (it has no @@ -163,7 +163,7 @@ by the next `recomputeAll`. The crux: the server **never trusts** a client-sent computed value. On the dispatcher path, immediately after `fromJson` and `reconcileDeclaredPrecision` and before the validator/`Model::execute` ([validation.md](validation.md), -[bridge.md](../spec/bridge.md)), the runner calls `recomputeAll(action)`, +[bridge.md](../spec/core/bridge.md)), the runner calls `recomputeAll(action)`, **overwriting** every computed member from its declared inputs. Whatever the wire carried in `total` is discarded and replaced by `qty * price` computed from the (reconciled) inputs the server received. Consequences: @@ -175,7 +175,7 @@ carried in `total` is discarded and replaced by `qty * price` computed from the computed field evaluates on the server's own number, not the client's. - It is a **no-op** for actions with no `computedFields` — zero behaviour change, backward compatible — mirroring how `reconcileDeclaredPrecision` no-ops for - actions with no `Quantity` members ([forms.md](../spec/forms.md)). + actions with no `Quantity` members ([forms.md](../spec/forms/forms.md)). Because the same `recomputeAll` runs on both ends over inputs normalised the same way, the field the client shows and the field the server stores agree by @@ -185,7 +185,7 @@ of the single rule declaration in [gui_cross_field_rules.md](gui_cross_field_rul ## Additivity and renderer fallback `x-computed` and `x-readonly` are additive, optional `x-*` keys, consistent with -the unversioned-schema stance of [forms.md](../spec/forms.md) and +the unversioned-schema stance of [forms.md](../spec/forms/forms.md) and [gui_overview.md](gui_overview.md). An action that declares no `computedFields` emits neither key and behaves exactly as today. A renderer that ignores them still produces a usable form — it just renders the computed field as an ordinary @@ -199,7 +199,7 @@ value is the true derivation) never depends on the client honouring `x-readonly` action's own fields only. A value that needs model state, a database lookup, or the current time is computed in the model's `execute`, not declared here. - **Not nested / cross-action derivation.** Like all of - [forms.md](../spec/forms.md), inputs and destinations are an action's own flat, + [forms.md](../spec/forms/forms.md), inputs and destinations are an action's own flat, top-level members; a computed field cannot draw from a sibling action or a sub-member of a nested aggregate. - **Not a formula language on the wire.** `x-computed` names inputs, not an @@ -211,7 +211,7 @@ value is the true derivation) never depends on the client honouring `x-readonly` [gui_cross_field_rules.md](gui_cross_field_rules.md), evaluated after `recomputeAll`. - **No localisation.** `x-computed`/`x-readonly` carry structure, not display - text, for the same reason the schema is un-localised ([forms.md](../spec/forms.md)); + text, for the same reason the schema is un-localised ([forms.md](../spec/forms/forms.md)); a computed field's caption is translated like any other field's, via [gui_i18n.md](gui_i18n.md)'s catalog. @@ -224,7 +224,7 @@ value is the true derivation) never depends on the client honouring `x-readonly` leaves the destination unengaged; the result is retagged to the destination's declared precision. - Client reactive path: `set<&qty>`/`set<&price>` refresh `total` live via the - existing coalescing fire ([bridge.md](../spec/bridge.md)). + existing coalescing fire ([bridge.md](../spec/core/bridge.md)). - **Server distrust:** an envelope carrying a tampered `total` is overwritten by `recomputeAll` in the dispatcher runner before `Model::execute` on `SimulatedRemoteBackend`, the Qt WebSocket transport, and `LocalBackend`; the @@ -240,19 +240,19 @@ value is the true derivation) never depends on the client honouring `x-readonly` - [gui_overview.md](gui_overview.md) — the umbrella program; this is its Tier-1 computed-fields feature. -- [bridge.md](../spec/bridge.md) — the reactive `subscribe`/`set<>`/`tryFireImpl` +- [bridge.md](../spec/core/bridge.md) — the reactive `subscribe`/`set<>`/`tryFireImpl` draft path (with coalescing) the live client recompute rides on, and `MemberPointerTraits` used to name the destination and inputs. -- [forms.md](../spec/forms.md) — `mergeSchemaExtras`, the property-node `x-*` +- [forms.md](../spec/forms/forms.md) — `mergeSchemaExtras`, the property-node `x-*` placement, `required` derivation (computed fields are excluded), `optionalFields`, and `reconcileDeclaredPrecision` (the normalisation both recomputes build on); the renderer-contract table this extends. - [validation.md](validation.md) — the dispatcher runner where the authoritative server-side `recomputeAll` slots in, alongside precision reconciliation and the `ready()` check. -- [quantity_type.md](../spec/quantity_type.md) — `Quantity` arithmetic +- [quantity_type.md](../spec/util/quantity_type.md) — `Quantity` arithmetic (`operator*`/`operator/`, result-unit deduction) numeric derivations use. -- [rational.md](../spec/rational.md) — the exact `Rational` payload that makes the +- [rational.md](../spec/util/rational.md) — the exact `Rational` payload that makes the client-displayed and server-stored values bit-identical. - [gui_cross_field_rules.md](gui_cross_field_rules.md) — rules that may reference a computed field, evaluated on the server's authoritative value. diff --git a/docs/planned/gui_cross_field_rules.md b/docs/planned/gui_cross_field_rules.md index 14bc03b..331917e 100644 --- a/docs/planned/gui_cross_field_rules.md +++ b/docs/planned/gui_cross_field_rules.md @@ -3,7 +3,7 @@ > **Status: planned — not yet implemented.** This spec is part of the GUI > enhancement program ([gui_overview.md](gui_overview.md), Tier 1) and depends on > the planned server-side validator in [validation.md](validation.md). It extends -> the `x-*` vocabulary and readiness model of [forms.md](../spec/forms.md) with a +> the `x-*` vocabulary and readiness model of [forms.md](../spec/forms/forms.md) with a > **closed, typed rule vocabulary** that one declaration drives onto the schema, > the client submit gate, *and* the server check with no drift. The same > vocabulary carries the **presentation rules** (`visibleWhen` / `readonlyWhen`) @@ -14,7 +14,7 @@ ## The gap Today an action's readiness is a single flat predicate: `allRequiredEngaged()` -([forms.md](../spec/forms.md)) checks that every required empty-capable field is +([forms.md](../spec/forms/forms.md)) checks that every required empty-capable field is engaged, and that is the whole of the framework's declarative validation. Any condition that spans **two or more fields** — "end date must be after start date", "supply either an email or a phone but not both", "discount is required @@ -57,7 +57,7 @@ as today; this feature does not try to replace it. ### Single source of truth: one declaration → three consumers An action opts in by declaring `static constexpr` rules, next to the existing -`optionalFields` opt-out ([forms.md](../spec/forms.md)): +`optionalFields` opt-out ([forms.md](../spec/forms/forms.md)): ```cpp // NEW — proposed vocabulary in namespace morph::forms::rules @@ -87,13 +87,13 @@ struct BookRoom { - **`formRules`** is the declaration the whole feature keys on: a `static constexpr` value of a `RuleList<...>` type (NEW). The engine detects it with a `HasFormRules` concept (NEW), mirroring how `HasOptionalFields` - detects `optionalFields` ([forms.md](../spec/forms.md)). + detects `optionalFields` ([forms.md](../spec/forms/forms.md)). - **`allRulesSatisfied(action)`** (NEW) is the shared evaluator — the single function that walks `A::formRules` and returns `true` only when every rule holds. `allRequiredEngaged` stays exactly as it is; the author `&&`s the two (or the framework does, when a rule list is present — see below). - Field references are **pointer-to-member NTTPs**, recovered via the existing - `MemberPointerTraits` ([bridge.md](../spec/bridge.md)), so a rule names real, + `MemberPointerTraits` ([bridge.md](../spec/core/bridge.md)), so a rule names real, type-checked members — a renamed or deleted field is a compile error, unlike the opaque string names a `Choice` carries. @@ -132,8 +132,8 @@ not fire spuriously while the form is still being filled. ### Numeric comparisons reuse exact `Rational` arithmetic When both operands are `Quantity` (or otherwise numeric), the comparison is -performed on the exact `math::Rational` payload ([rational.md](../spec/rational.md)), -after `reconcileDeclaredPrecision` ([forms.md](../spec/forms.md)) has normalised +performed on the exact `math::Rational` payload ([rational.md](../spec/util/rational.md)), +after `reconcileDeclaredPrecision` ([forms.md](../spec/forms/forms.md)) has normalised each operand to its declared precision — never on a lossy `double`. `Quantity` already exposes `operator*` for its `Rational` payload and comparison over `Rational` is exact, so `greater`/`less` are exact and give the identical result @@ -142,7 +142,7 @@ on client and server. A literal in `equals`/a comparison is likewise carried as ### The `x-rules` schema emission -`mergeSchemaExtras` ([forms.md](../spec/forms.md)) gains a step that walks +`mergeSchemaExtras` ([forms.md](../spec/forms/forms.md)) gains a step that walks `A::formRules` and emits a **top-level** `x-rules` array on the schema object, alongside the existing `required` array. Each element is a self-describing JSON object a renderer (or the server) can evaluate without any C++ type information: @@ -162,7 +162,7 @@ Field names in `x-rules` are the **wire (JSON) field names** the members serialise as — resolved from the pointer-to-member the same way `x-order` is derived — so they line up with the property keys a renderer already indexes. -New keys this spec adds to the [forms.md](../spec/forms.md) renderer-contract +New keys this spec adds to the [forms.md](../spec/forms/forms.md) renderer-contract table (all additive, all optional): | Key | Where | JSON type | Meaning / renderer obligation | @@ -171,7 +171,7 @@ table (all additive, all optional): | ↳ `kind` | rule / condition object | string | One of the closed vocabulary ids in the table above (or a condition id: `engaged`, `notEngaged`, `equals`). An unrecognised `kind` must be treated as "cannot evaluate" — the renderer leaves the gate to the server rather than passing the rule (fail-closed). | | ↳ `fields` | rule / condition object | array of strings | Wire field names the rule ranges over, in declaration order (operand order is significant for `greater`/`less`). | | ↳ `when` | `requiredWhen` / `visibleWhen` / `readonlyWhen` object | rule/condition object | The nested condition the rule keys on: for `requiredWhen`, when the listed `fields` become required; for the presentation kinds, when they are shown / made read-only. Present only on these condition-bearing kinds. | -| ↳ `value` | `equals` condition object | scalar / `{num,den}` | The literal an `equals` condition compares against; a numeric literal is the exact `Rational` `{num, den}` (see [rational.md](../spec/rational.md)), never a `double`. | +| ↳ `value` | `equals` condition object | scalar / `{num,den}` | The literal an `equals` condition compares against; a numeric literal is the exact `Rational` `{num, den}` (see [rational.md](../spec/util/rational.md)), never a `double`. | ### Server-side: the same list, evaluated in the dispatcher @@ -179,7 +179,7 @@ This is the crux of no-drift. [validation.md](validation.md) injects `ActionValidator::ready(action)` into the dispatcher runner after `fromJson` and precision reconciliation. Because the author's `validate()` body calls `allRulesSatisfied(*this)`, and `ActionValidator::ready` auto-detects -`validate()` via the `HasValidate` concept ([registry.md](../spec/registry.md)), +`validate()` via the `HasValidate` concept ([registry.md](../spec/core/registry.md)), **the server evaluates the exact same rule list the client did** — the same typed nodes over the same reconciled values — with zero extra server code. A hand-built envelope that violates a rule is rejected with the `ValidationError` that @@ -193,7 +193,7 @@ server. ### Client-side: live gate on the reactive path -On the `set<>` reactive draft path ([bridge.md](../spec/bridge.md)), the +On the `set<>` reactive draft path ([bridge.md](../spec/core/bridge.md)), the `ActionValidator::ready(snapshot)` check that already gates each fire now transitively runs `allRulesSatisfied`, so the action does not fire until every cross-field rule holds — the live gate and the submit gate become the same @@ -238,7 +238,7 @@ implies the other. Every key here is an additive, optional `x-*` (or the always-safe extension of the existing `required` array), consistent with the unversioned-schema stance of -[forms.md](../spec/forms.md) and [gui_overview.md](gui_overview.md). An action +[forms.md](../spec/forms/forms.md) and [gui_overview.md](gui_overview.md). An action that declares no `formRules` emits no `x-rules` and behaves exactly as today. A renderer that does not understand `x-rules` still produces a usable form: it honours the per-field `required` array and lets the **server** reject any @@ -255,7 +255,7 @@ older renderer treats an unknown `kind` as fail-closed (defers to the server). `validate()`/`execute` and is **not** reflected into `x-rules` — the same division [validation.md](validation.md) draws between field-level readiness and model invariants. -- **Not nested-action rules.** Like all of [forms.md](../spec/forms.md), rules +- **Not nested-action rules.** Like all of [forms.md](../spec/forms/forms.md), rules range only over an action's own **flat, top-level** members. Sub-members of a nested aggregate are not addressable. - **Not authorization.** Rules answer "is this action internally consistent?", not @@ -263,13 +263,13 @@ older renderer treats an unknown `kind` as fail-closed (defers to the server). ([security.md](../spec/security.md)), exactly as [validation.md](validation.md) states. - **Not option-membership validation.** Whether a `Choice` value is a *current* - option is still unchecked at both ends ([choice.md](../spec/choice.md)); a rule + option is still unchecked at both ends ([choice.md](../spec/forms/choice.md)); a rule can require a `Choice` be engaged, not that its value is a live option. - **No localisation of rule messages.** `x-rules` carries structure, not human-readable text; a renderer builds its violation messages itself and localises them through [gui_i18n.md](gui_i18n.md)'s catalog (`.rule.` keys), for the same reason the schema is - un-localised ([forms.md](../spec/forms.md)). + un-localised ([forms.md](../spec/forms/forms.md)). ## Testing (planned) @@ -308,16 +308,16 @@ older renderer treats an unknown `kind` as fail-closed (defers to the server). - [validation.md](validation.md) — the server-side validator this shares its single declaration with; `ValidationError`, the dispatcher injection point, and the precision-reconciliation-before-validate order this rule engine relies on. -- [forms.md](../spec/forms.md) — the `required` array, `allRequiredEngaged`, +- [forms.md](../spec/forms/forms.md) — the `required` array, `allRequiredEngaged`, `mergeSchemaExtras`, `optionalFields`, and the renderer-contract table this `x-rules` block extends; `reconcileDeclaredPrecision`. -- [registry.md](../spec/registry.md) — `ActionValidator::ready`, the +- [registry.md](../spec/core/registry.md) — `ActionValidator::ready`, the `HasValidate` concept that picks up the author's `validate()`, and `BRIDGE_REGISTER_VALIDATOR` as the alternative plug-in point. -- [bridge.md](../spec/bridge.md) — the reactive `set<>`/`tryFireImpl` gate the +- [bridge.md](../spec/core/bridge.md) — the reactive `set<>`/`tryFireImpl` gate the live rule check runs on, and `MemberPointerTraits` used to name rule fields. -- [rational.md](../spec/rational.md) — the exact `Rational` arithmetic numeric +- [rational.md](../spec/util/rational.md) — the exact `Rational` arithmetic numeric comparisons and literals use, so client and server compare identical values. -- [choice.md](../spec/choice.md) — `Choice` engagement, which a rule may reference +- [choice.md](../spec/forms/choice.md) — `Choice` engagement, which a rule may reference but whose option-membership remains unchecked. - [todo.md](../todo.md) — execution order within the GUI program. diff --git a/docs/planned/gui_dependent_choices.md b/docs/planned/gui_dependent_choices.md index 20e0e6f..d0c5d2a 100644 --- a/docs/planned/gui_dependent_choices.md +++ b/docs/planned/gui_dependent_choices.md @@ -2,14 +2,14 @@ > **Status: planned — not yet implemented.** This spec is part of the GUI > enhancement program ([gui_overview.md](gui_overview.md), Tier 1). It extends the -> `Choice` type of [choice.md](../spec/choice.md) and the `x-option*` schema -> vocabulary of [forms.md](../spec/forms.md). It describes the intended behavior; +> `Choice` type of [choice.md](../spec/forms/choice.md) and the `x-option*` schema +> vocabulary of [forms.md](../spec/forms/forms.md). It describes the intended behavior; > the code does not implement it yet. See [todo.md](../todo.md). ## The gap A `Choice` field today sources its options by executing a named action **with an -empty body** ([choice.md](../spec/choice.md)): the renderer calls +empty body** ([choice.md](../spec/forms/choice.md)): the renderer calls `optionsAction()` with no arguments, reads the result rows, and offers them. That makes every `Choice` independent — its option set is a constant of the form, fetched once. @@ -38,19 +38,19 @@ values as parameters**, and emit that dependency in the schema The dependency is declared once in the `Choice` type; the schema carries it to the renderer additively, and everything about how options are *fetched* (a registered action executed over the same wire) is unchanged from -[choice.md](../spec/choice.md). +[choice.md](../spec/forms/choice.md). ## Design ### Extending `Choice` with a parent dependency -`Choice`'s options-metadata NTTPs ([choice.md](../spec/choice.md)) gain an +`Choice`'s options-metadata NTTPs ([choice.md](../spec/forms/choice.md)) gain an optional **dependency list**: the wire field names of the sibling fields whose values parameterise the options action. Because a `Choice` cannot name its siblings by pointer-to-member (it does not know the enclosing action type), the dependency is expressed as `FixedString` field names — the same NTTP vehicle that already carries `OptionsAction`/`ValueField`/`LabelField` -([choice.md](../spec/choice.md)): +([choice.md](../spec/forms/choice.md)): ```cpp // NEW — proposed extension in namespace morph::forms (choice.hpp) @@ -82,7 +82,7 @@ nothing in the schema or the fetch path changes. Adding the variadic tail requires the two companion specialisations that pattern- match the `Choice` parameter list to be generalised in lockstep: `IsChoice` -([choice.md](../spec/choice.md)) and the `glz::meta>` serialisation +([choice.md](../spec/forms/choice.md)) and the `glz::meta>` serialisation specialisation (`choice.hpp`) are both written today against the exact four-parameter `Choice`, so each must gain the same `FixedString... DependsOn` pack to keep matching. The change is @@ -92,7 +92,7 @@ today. The `DependsOn` names are **wire (JSON) field names of sibling fields in the same action** — the same class of unchecked string the existing -`ValueField`/`LabelField` already are ([choice.md](../spec/choice.md) "Author's +`ValueField`/`LabelField` already are ([choice.md](../spec/forms/choice.md) "Author's obligations"): a typo or a renamed sibling compiles cleanly and fails only at runtime when the options action receives an unexpected body. This is an inherent consequence of `Choice` not knowing its enclosing type; it is called out as an @@ -110,7 +110,7 @@ field names and whose values are the **current values** of those sibling fields: ``` The options action is therefore an **ordinary registered action whose body is the -parent selection** — no new dispatch mechanism, exactly as [choice.md](../spec/choice.md) +parent selection** — no new dispatch mechanism, exactly as [choice.md](../spec/forms/choice.md) reuses the action wire for the empty-body case. It returns the same `{valueField, labelField, …}` rows as before, now filtered to the parent. @@ -120,15 +120,15 @@ reuses the action wire for the empty-body case. It returns the same [gui_computed_fields.md](gui_computed_fields.md).) - When a parent value **changes**, the renderer re-fetches with the new body and **clears** any existing child selection that is not present in the new result — - closing (client-side) the staleness that [choice.md](../spec/choice.md) Failure + closing (client-side) the staleness that [choice.md](../spec/forms/choice.md) Failure modes describe for the independent case. Membership is still not *enforced* server-side (see Non-goals). ### Schema emission — `x-optionsDependsOn` -`mergeSchemaExtras` ([forms.md](../spec/forms.md)) already reads a `Choice` +`mergeSchemaExtras` ([forms.md](../spec/forms/forms.md)) already reads a `Choice` property's compile-time metadata to emit `x-optionsAction` / `x-optionValue` / -`x-optionLabel` on the property node ([choice.md](../spec/choice.md)). It gains +`x-optionLabel` on the property node ([choice.md](../spec/forms/choice.md)). It gains one more read: when `optionsDependsOn()` is non-empty it emits `x-optionsDependsOn` on the same property node (sibling of the `$ref`): @@ -143,12 +143,12 @@ one more read: when `optionsDependsOn()` is non-empty it emits } ``` -New key this spec adds to the [forms.md](../spec/forms.md) renderer-contract +New key this spec adds to the [forms.md](../spec/forms/forms.md) renderer-contract table (additive, optional): | Key | Where | JSON type | Meaning / renderer obligation | |---|---|---|---| -| `x-optionsDependsOn` | property node (sibling of `$ref`) | array of strings | Wire field names of sibling fields whose current values parameterise this field's options action. The renderer sends `{name: value, …}` as the options-action request body (instead of an empty body), and **re-fetches** the options — clearing a now-invalid selection — whenever any listed field changes. **Omitted entirely** when the `Choice` declares no dependency (then the options action is called with an empty body, exactly as [choice.md](../spec/choice.md) specifies today). | +| `x-optionsDependsOn` | property node (sibling of `$ref`) | array of strings | Wire field names of sibling fields whose current values parameterise this field's options action. The renderer sends `{name: value, …}` as the options-action request body (instead of an empty body), and **re-fetches** the options — clearing a now-invalid selection — whenever any listed field changes. **Omitted entirely** when the `Choice` declares no dependency (then the options action is called with an empty body, exactly as [choice.md](../spec/forms/choice.md) specifies today). | The dependency names are the parent fields' **wire names**, matching the property keys a renderer already indexes (the same convention as `x-computed.inputs` in @@ -165,26 +165,26 @@ field, and `x-optionsDependsOn: ["country"]` on the parent tells the renderer to fill exactly that field from the sibling. The two must agree — the `DependsOn` names must match the options action's input field names — which is the natural extension of the existing obligation that `ValueField`/`LabelField` match the -options action's *result* field names ([choice.md](../spec/choice.md)). +options action's *result* field names ([choice.md](../spec/forms/choice.md)). ## Additivity and renderer fallback `x-optionsDependsOn` is an additive, optional `x-*` key, consistent with the -unversioned-schema stance of [forms.md](../spec/forms.md) and +unversioned-schema stance of [forms.md](../spec/forms/forms.md) and [gui_overview.md](gui_overview.md), and the `Choice` change is a defaulted variadic tail that leaves every existing `Choice<...>` type and its emitted schema byte-for-byte unchanged. A renderer that does not understand `x-optionsDependsOn` degrades predictably: it falls back to the base `Choice` contract and calls the options action with an **empty body** -([choice.md](../spec/choice.md)). The child list is then unfiltered (or empty, if +([choice.md](../spec/forms/choice.md)). The child list is then unfiltered (or empty, if the options action requires the parent field) — a reduced affordance, not a broken form — exactly the "ignore an `x-*` key and lose only that affordance" -guarantee [forms.md](../spec/forms.md) makes. +guarantee [forms.md](../spec/forms/forms.md) makes. ## Non-goals - **No server-side option-membership enforcement.** As in - [choice.md](../spec/choice.md), the wire value is a bare nullable `T` and the + [choice.md](../spec/forms/choice.md), the wire value is a bare nullable `T` and the server does not check that a submitted child value belongs to the parent's current list. `x-optionsDependsOn` improves the *client* experience (correct fetch, stale-selection clearing); a model that must reject an inconsistent @@ -197,7 +197,7 @@ guarantee [forms.md](../spec/forms.md) makes. dependency, not the fetch strategy. - **Parents are flat siblings only.** A `DependsOn` name must resolve to a top-level sibling field of the same action, consistent with the flat-actions - scope of [forms.md](../spec/forms.md); it cannot reach into a nested aggregate + scope of [forms.md](../spec/forms/forms.md); it cannot reach into a nested aggregate or another action. - **The dependency is one-directional.** `city` depends on `country`; selecting a city does not filter countries. Multi-way constraint solving between picklists @@ -209,7 +209,7 @@ guarantee [forms.md](../spec/forms.md) makes. ## Author's obligations -A dependent `Choice` adds to the three obligations [choice.md](../spec/choice.md) +A dependent `Choice` adds to the three obligations [choice.md](../spec/forms/choice.md) already lists: - **Each `DependsOn` name must be a real sibling field's wire name** in the @@ -246,11 +246,11 @@ already lists: - [gui_overview.md](gui_overview.md) — the umbrella program; this is its Tier-1 dependent-choices feature, the generalisation of the `Choice` pattern the overview names. -- [choice.md](../spec/choice.md) — the base `Choice`/`FixedString` design this +- [choice.md](../spec/forms/choice.md) — the base `Choice`/`FixedString` design this extends: the empty-body options fetch, `optionsAction()`/`valueField()`/ `labelField()`, the `x-option*` emission, the unchecked-string author obligations, and the staleness Failure mode this narrows client-side. -- [forms.md](../spec/forms.md) — `mergeSchemaExtras` and the property-node `x-*` +- [forms.md](../spec/forms/forms.md) — `mergeSchemaExtras` and the property-node `x-*` placement where `x-optionsDependsOn` is emitted; the renderer-contract table this key joins; the flat-actions scope. - [gui_cross_field_rules.md](gui_cross_field_rules.md) — a rule may require a @@ -258,6 +258,6 @@ already lists: closed rule vocabulary does not cover. - [gui_computed_fields.md](gui_computed_fields.md) — shares the "sibling field names as wire strings" and empty-input-propagation conventions. -- [bridge.md](../spec/bridge.md) — the action-execute path the renderer uses to +- [bridge.md](../spec/core/bridge.md) — the action-execute path the renderer uses to fetch options is the same one every other action uses. - [todo.md](../todo.md) — execution order within the GUI program. diff --git a/docs/planned/gui_field_metadata.md b/docs/planned/gui_field_metadata.md index 0ff1bbc..61b6b51 100644 --- a/docs/planned/gui_field_metadata.md +++ b/docs/planned/gui_field_metadata.md @@ -2,7 +2,7 @@ > **Status: planned — not yet implemented.** This spec extends the GUI program > umbrella ([gui_overview.md](gui_overview.md)) and the schema-generation spine -> ([forms.md](../spec/forms.md)). It is a Tier-1 richer-forms feature: purely +> ([forms.md](../spec/forms/forms.md)). It is a Tier-1 richer-forms feature: purely > additive presentation metadata on a single action's flat form. See > [todo.md](../todo.md). @@ -12,7 +12,7 @@ Today `morph::forms::schemaJson()` emits enough for a renderer to *place* and *type* a field — `x-order` for layout order, `x-decimalPlaces` / `x-unitAlternatives` for `Quantity`, `x-optionsAction` / `x-optionValue` / `x-optionLabel` for `Choice`, plus glaze's `type`/bounds/`format` and any `description` from a `glz::json_schema` -specialisation ([forms.md](../spec/forms.md), "Renderer contract"). What it does +specialisation ([forms.md](../spec/forms/forms.md), "Renderer contract"). What it does **not** provide, short of hand-authoring a `glz::json_schema` block, is per-field *presentation* metadata: @@ -29,7 +29,7 @@ per-field *presentation* metadata: The forms layer already establishes the pattern for the fix: metadata that is a compile-time property of the action belongs *in the type or a `static constexpr` declaration*, surfaced through the schema as `x-*` keys — exactly how `Choice` -carries its options source ([choice.md](../spec/choice.md)) and how +carries its options source ([choice.md](../spec/forms/choice.md)) and how `optionalFields` (verified in `forms.hpp`) opts a field out of `required`. ## Goal @@ -111,7 +111,7 @@ inferred title. `mergeSchemaExtras` (verified in `forms.hpp`) gains a pass that, for each reflected member (via `forEachNamedMember`, verified), looks up any matching `FieldMeta` and patches the property node — the same property node that already -carries `x-order` and the `Choice`/`Quantity` keys ([forms.md](../spec/forms.md), +carries `x-order` and the `Choice`/`Quantity` keys ([forms.md](../spec/forms/forms.md), "Where the keys physically land"). Label maps onto the standard JSON-Schema `title`; help maps onto standard `description` (so a renderer that already reads glaze's `description` needs no change); the rest are `x-*` extensions: @@ -125,7 +125,7 @@ glaze's `description` needs no change); the rest are `x-*` extensions: | `x-hidden` | property node (sibling of `$ref`) | boolean | `true` when the field should not be shown at all. Emitted only when `true`. The renderer omits the control but the field remains part of the action payload (submitted at its default/current value). | All five keys are **additive and non-breaking**: they extend the -[forms.md](../spec/forms.md) contract table without renaming or retyping any +[forms.md](../spec/forms/forms.md) contract table without renaming or retyping any existing key, exactly as [gui_overview.md](gui_overview.md)'s versioning stance requires. A renderer that ignores them **falls back to today's behaviour** — it shows the raw wire key as the caption, no helper/placeholder text, and every @@ -154,10 +154,10 @@ This is illustrative of *one* renderer; the contract stays renderer-agnostic. - **`x-hidden` / `x-readonly` are not security controls.** Both keys are presentation only — the field still travels in the payload and a hand-built wire envelope can set it freely. Enforcement stays server-side - ([validation.md](validation.md), [forms.md](../spec/forms.md)'s trust + ([validation.md](validation.md), [forms.md](../spec/forms/forms.md)'s trust boundary). A truly secret field must not be a member of the action at all. - **No i18n.** Labels and help are baked into the one cached schema per type - ([forms.md](../spec/forms.md), "no localisation"). Translated captions are + ([forms.md](../spec/forms/forms.md), "no localisation"). Translated captions are [gui_i18n.md](gui_i18n.md): a renderer-side catalog over stable message keys (`FieldMeta` gains an optional `i18nKey` override there), never a per-locale schema. The `label`/`help`/`placeholder` declared here are that mechanism's @@ -198,11 +198,11 @@ This is illustrative of *one* renderer; the contract stays renderer-agnostic. - [gui_overview.md](gui_overview.md) — the infer-by-default / declare-to-override principle and the additive-`x-*` versioning stance this feature obeys. -- [forms.md](../spec/forms.md) — `schemaJson()`, `mergeSchemaExtras`, +- [forms.md](../spec/forms/forms.md) — `schemaJson()`, `mergeSchemaExtras`, `forEachNamedMember`, the `optionalFields` convention this mirrors, the property-node vs `$def` placement rule, and the renderer-contract table these keys extend. -- [choice.md](../spec/choice.md) — the pattern of carrying field metadata in the +- [choice.md](../spec/forms/choice.md) — the pattern of carrying field metadata in the type / declaration and surfacing it through property-level `x-*` keys. - [gui_layout_grouping.md](gui_layout_grouping.md) — visual structure (sections, tabs, spans) that composes with these per-field labels. diff --git a/docs/planned/gui_i18n.md b/docs/planned/gui_i18n.md index 7e00a0d..f666e9d 100644 --- a/docs/planned/gui_i18n.md +++ b/docs/planned/gui_i18n.md @@ -3,7 +3,7 @@ > **Status: planned — not yet implemented.** This spec is part of the GUI > enhancement program ([gui_overview.md](gui_overview.md), Tier 1, > cross-cutting). It supplies the "different mechanism entirely" that -> [forms.md](../spec/forms.md) defers when it rules out localised schemas, and +> [forms.md](../spec/forms/forms.md) defers when it rules out localised schemas, and > that [gui_field_metadata.md](gui_field_metadata.md), > [gui_computed_fields.md](gui_computed_fields.md), and > [gui_cross_field_rules.md](gui_cross_field_rules.md) all point at from their @@ -18,7 +18,7 @@ The GUI program is about to mint user-visible text with no translation story: - **The schema cannot carry per-locale text.** Each type's schema is memoised once, process-wide (`schemaJson()`'s function-local static), which "precludes localised / i18n schemas … a translated form would need a - different mechanism entirely" ([forms.md](../spec/forms.md), "One cached + different mechanism entirely" ([forms.md](../spec/forms/forms.md), "One cached schema per type — no localisation"). That design is deliberate and stays. - **The program's display text is multiplying anyway.** Tier 1 emits `title` / `description` / `x-placeholder` per field @@ -32,10 +32,10 @@ The GUI program is about to mint user-visible text with no translation story: is baked or renderer-invented, and none is translatable. - **The locale hook exists but nothing consumes it.** `session::Context::locale` is a BCP-47 tag on every call - ([session.md](../spec/session.md)) — plumbed, documented, unused. + ([session.md](../spec/session/session.md)) — plumbed, documented, unused. - **Locale formatting is unspecified.** Decimal comma vs point for `Rational` entry, local-time display of a strictly-UTC `Timestamp` - ([datetime.md](../spec/datetime.md) excludes time-zone conversion) — today + ([datetime.md](../spec/util/datetime.md) excludes time-zone conversion) — today each renderer improvises with no contract to conform to. Deferring this past Tier 1 gets expensive: `fieldMetadata` declarations and @@ -124,16 +124,16 @@ Display formatting is the renderer's duty; the wire stays canonical: - **Numbers.** A locale may render and accept `1.050,25`; the payload is the canonical exact `{num, den, dp}` regardless - ([rational.md](../spec/rational.md)). The renderer converts at the control + ([rational.md](../spec/util/rational.md)). The renderer converts at the control edge; the exact digit routines are locale-free. - **Timestamps.** The wire value is strict UTC ISO-8601 - ([datetime.md](../spec/datetime.md)); displaying and editing in the user's + ([datetime.md](../spec/util/datetime.md)); displaying and editing in the user's zone/format is the control's duty, and a locale-formatted entry must round-trip to the identical canonical wire value. - **Choice option labels are data, not chrome.** Option rows come from - executing the options action ([choice.md](../spec/choice.md)); the catalog + executing the options action ([choice.md](../spec/forms/choice.md)); the catalog never sees them. A model that wants localised rows reads - `session::current()->locale` server-side ([session.md](../spec/session.md)) + `session::current()->locale` server-side ([session.md](../spec/session/session.md)) — the one place server-side locale participates. ### 4. Conformance fixtures ([gui_renderer_toolkit.md](gui_renderer_toolkit.md)) @@ -148,7 +148,7 @@ non-UTC display zone round-trips unchanged. - **No per-locale schema variants.** `schemaJson()` keeps its one cached, un-localised schema; no locale parameter is added anywhere in - [forms.md](../spec/forms.md)'s surface. + [forms.md](../spec/forms/forms.md)'s surface. - **No translation storage format.** Qt `.ts`/`.qm`, gettext, JSON, a database — the provider signature is the whole contract. - **No server-side message localisation.** Canonical error strings are @@ -179,7 +179,7 @@ non-UTC display zone round-trips unchanged. ## Cross-references -- [forms.md](../spec/forms.md) — the cached, un-localised schema this spec +- [forms.md](../spec/forms/forms.md) — the cached, un-localised schema this spec works with rather than against; the deferred "different mechanism" this is. - [gui_field_metadata.md](gui_field_metadata.md) / [gui_layout_grouping.md](gui_layout_grouping.md) / @@ -190,11 +190,11 @@ non-UTC display zone round-trips unchanged. renderer turns into localised violation messages. - [gui_renderer_toolkit.md](gui_renderer_toolkit.md) — where the `TranslationProvider` seam lives and the conformance fixtures run. -- [session.md](../spec/session.md) — `Context::locale`, the server-side hook +- [session.md](../spec/session/session.md) — `Context::locale`, the server-side hook for data (not chrome) localisation. -- [datetime.md](../spec/datetime.md) / [rational.md](../spec/rational.md) — +- [datetime.md](../spec/util/datetime.md) / [rational.md](../spec/util/rational.md) — the canonical wire forms display formatting must round-trip to. -- [choice.md](../spec/choice.md) — option labels as data, out of catalog +- [choice.md](../spec/forms/choice.md) — option labels as data, out of catalog scope. - [drift_guard.md](drift_guard.md) — why canonical server strings stay untranslated. diff --git a/docs/planned/gui_layout_grouping.md b/docs/planned/gui_layout_grouping.md index b09c04d..961640d 100644 --- a/docs/planned/gui_layout_grouping.md +++ b/docs/planned/gui_layout_grouping.md @@ -2,7 +2,7 @@ > **Status: planned — not yet implemented.** This spec extends the GUI program > umbrella ([gui_overview.md](gui_overview.md)) and the schema-generation spine -> ([forms.md](../spec/forms.md)). It is a Tier-1 richer-forms feature: visual +> ([forms.md](../spec/forms/forms.md)). It is a Tier-1 richer-forms feature: visual > structure layered over the existing flat field list, additive and opt-in. See > [todo.md](../todo.md). @@ -10,7 +10,7 @@ `morph::forms::schemaJson()` emits a **flat** form. The only layout signal is `x-order` — the member's 0-based declaration index — which a renderer uses to lay -fields out top-to-bottom in declaration order ([forms.md](../spec/forms.md), +fields out top-to-bottom in declaration order ([forms.md](../spec/forms/forms.md), "Renderer contract"). There is no way to express structure *over* that flat list: - **No sections / fieldsets.** Related fields (all the address fields, all the @@ -118,7 +118,7 @@ container kind without reconstructing them from per-field tags: | `x-colspan` | property node (sibling of `$ref`) | positive integer | Number of grid columns the field should span, from `FieldSpan::colspan`. Emitted only when > 1. A renderer laying fields in a grid widens the control; a single-column renderer ignores it (field still shows full width). | All four keys are **additive and non-breaking**: they extend the -[forms.md](../spec/forms.md) contract table without touching any existing key, +[forms.md](../spec/forms/forms.md) contract table without touching any existing key, per [gui_overview.md](gui_overview.md)'s versioning stance. A renderer that ignores them **falls back to exactly today's flat form** — it drops `x-layout`, `x-group`, `x-section`, and `x-colspan` and lays every field out top-to-bottom by @@ -143,7 +143,7 @@ Illustrative of *one* renderer; the contract stays renderer-agnostic. ## Non-goals - **No nested / recursive groups.** Groups are a single flat level over the flat - field list — no group-within-group. This matches [forms.md](../spec/forms.md)'s + field list — no group-within-group. This matches [forms.md](../spec/forms/forms.md)'s "flat actions only" scope; deep hierarchy would need nested action types the form generator does not descend into. - **No responsive breakpoints.** `x-colspan` is a fixed column count, not a @@ -181,7 +181,7 @@ Illustrative of *one* renderer; the contract stays renderer-agnostic. - [gui_overview.md](gui_overview.md) — infer-by-default / declare-to-override and the additive-`x-*` versioning stance this feature obeys. -- [forms.md](../spec/forms.md) — `schemaJson()`, `mergeSchemaExtras`, +- [forms.md](../spec/forms/forms.md) — `schemaJson()`, `mergeSchemaExtras`, `forEachNamedMember`, `x-order` (the intra-group ordering authority), the property-node placement rule, and the renderer-contract table these keys extend. - [gui_field_metadata.md](gui_field_metadata.md) — per-field labels/help that diff --git a/docs/planned/gui_overview.md b/docs/planned/gui_overview.md index bf3df2c..cb330e5 100644 --- a/docs/planned/gui_overview.md +++ b/docs/planned/gui_overview.md @@ -3,7 +3,7 @@ > **Status: planned — not yet implemented.** This is the umbrella spec for the > GUI-generation enhancement program. It frames the goal, the guiding principle, > and how the individual GUI specs (`gui_*.md`) layer on top of the existing -> schema-driven forms surface ([forms.md](../spec/forms.md), [choice.md](../spec/choice.md)). +> schema-driven forms surface ([forms.md](../spec/forms/forms.md), [choice.md](../spec/forms/choice.md)). > The individual features each have their own spec; this file is the map. See > [todo.md](../todo.md). @@ -17,7 +17,7 @@ without forking the renderer. Today the spine already exists: an action struct is turned into a JSON Schema by `morph::forms::schemaJson()`, enriched with `x-*` extension keys, and a -renderer builds a form from it ([forms.md](../spec/forms.md)). The current surface +renderer builds a form from it ([forms.md](../spec/forms/forms.md)). The current surface is **one action → one flat form**. This program extends that spine along two tiers plus an ecosystem layer. @@ -36,7 +36,7 @@ with "flexible": registration macro on the action. Never mandatory; absence falls back to a sensible convention. 3. **Escape hatch always available.** The schema is a documented, stable - contract ([forms.md](../spec/forms.md) "Renderer contract"). Anything the + contract ([forms.md](../spec/forms/forms.md) "Renderer contract"). Anything the generated GUI cannot express, an app builds by consuming the schema directly or overriding one field's widget (see [gui_renderer_toolkit.md](gui_renderer_toolkit.md)). @@ -85,7 +85,7 @@ from Tier-1 action-forms. ## Versioning stance (unchanged, deliberately) -Per [forms.md](../spec/forms.md), the emitted schema is **unversioned**, and the +Per [forms.md](../spec/forms/forms.md), the emitted schema is **unversioned**, and the program keeps it that way: **every new key each `gui_*.md` spec introduces is additive and optional — an `x-*` extension, a standard JSON-Schema annotation (`title`, `description`), or a new top-level view-schema document — and an older @@ -110,26 +110,26 @@ contract. ## How the specs relate to existing work -- **Builds directly on:** [forms.md](../spec/forms.md) (schema generation, the - `x-*` vocabulary, `allRequiredEngaged`), [choice.md](../spec/choice.md) - (`Choice`/`FixedString`), [quantity_type.md](../spec/quantity_type.md), - [datetime.md](../spec/datetime.md). +- **Builds directly on:** [forms.md](../spec/forms/forms.md) (schema generation, the + `x-*` vocabulary, `allRequiredEngaged`), [choice.md](../spec/forms/choice.md) + (`Choice`/`FixedString`), [quantity_type.md](../spec/util/quantity_type.md), + [datetime.md](../spec/util/datetime.md). - **Cross-field rules tie into** [validation.md](validation.md): one rule declaration should drive the schema's `required`/`x-rules`, the client submit gate, *and* the planned server-side validator — no drift between them. - **Reactive forms build on** the `subscribe`/`set<>` draft mechanism in - [bridge.md](../spec/bridge.md); computed fields and wizards extend it. + [bridge.md](../spec/core/bridge.md); computed fields and wizards extend it. - **Nothing here changes the wire or dispatch semantics** — the GUI program is an opt-in layer over registration, schema generation, and the existing dispatch paths, exactly as the forms layer is today. ## Cross-references -- [forms.md](../spec/forms.md) — the schema-generation spine this program extends; +- [forms.md](../spec/forms/forms.md) — the schema-generation spine this program extends; the normative `x-*` key vocabulary and renderer contract. -- [choice.md](../spec/choice.md) — `Choice`/`FixedString`, the pattern the +- [choice.md](../spec/forms/choice.md) — `Choice`/`FixedString`, the pattern the dependent-choices and view specs generalise. -- [bridge.md](../spec/bridge.md) — the reactive `subscribe`/`set<>` draft path the +- [bridge.md](../spec/core/bridge.md) — the reactive `subscribe`/`set<>` draft path the computed-field and wizard specs build on. - [validation.md](validation.md) — the planned server-side validation the cross-field rules must share a declaration with. diff --git a/docs/planned/gui_renderer_toolkit.md b/docs/planned/gui_renderer_toolkit.md index 5c1319e..c34130e 100644 --- a/docs/planned/gui_renderer_toolkit.md +++ b/docs/planned/gui_renderer_toolkit.md @@ -4,10 +4,10 @@ > the GUI program ([gui_overview.md](gui_overview.md)). It turns the reference > renderer from an *example* into a *shipped, reusable* component, adds a > **conformance test kit** that formalises the "normative" renderer contract of -> [forms.md](../spec/forms.md), and defines a **theming / component-override** +> [forms.md](../spec/forms/forms.md), and defines a **theming / component-override** > registry so an app swaps one field's control without forking the renderer. It > consumes — and does not change — the `x-*` action schema -> ([forms.md](../spec/forms.md)) and the `v-*` / `w-*` / `app-*` view schemas +> ([forms.md](../spec/forms/forms.md)) and the `v-*` / `w-*` / `app-*` view schemas > ([gui_collections_views.md](gui_collections_views.md), > [gui_workflows_navigation.md](gui_workflows_navigation.md)). See > [todo.md](../todo.md). @@ -15,7 +15,7 @@ ## The gap The only renderer is `examples/forms/gui_qml` — a ~425-line `DynamicForm.qml` -plus `Main.qml` ([forms.md](../spec/forms.md)). It is an **example**, not a +plus `Main.qml` ([forms.md](../spec/forms/forms.md)). It is an **example**, not a component: an app that wants schema-driven forms copies and forks it. Three things are missing: @@ -24,7 +24,7 @@ things are missing: the options fetch) lives in an example directory, so every consumer re-implements or vendors it. It is a reference implementation trapped as demo code. -- **No conformance kit.** [forms.md](../spec/forms.md) calls its `x-*` vocabulary +- **No conformance kit.** [forms.md](../spec/forms/forms.md) calls its `x-*` vocabulary **normative** ("the normative list of every key a renderer must understand"), but nothing lets a *new* renderer (web, ImGui) prove it honors that contract. Correctness of a renderer is currently "read the prose and hope." @@ -108,7 +108,7 @@ must produce, proposed as a `morph::conformance` description the harness drives: - Fields render in `x-order`, not JSON key order. - Submission is blocked until every `required` field is engaged, and enabled once - they are (`allRequiredEngaged` parity, [forms.md](../spec/forms.md)). + they are (`allRequiredEngaged` parity, [forms.md](../spec/forms/forms.md)). - A `Quantity` payload is emitted as `{num,den,dp}` **exactly** (the corpus pins expected `num`/`den` for a given typed decimal), and a unit switch recomputes the entry exactly with no float drift. @@ -203,7 +203,7 @@ schema directly. itself ships and maintains the QML reference, not a web or ImGui renderer. - **No server involvement.** The renderer, kit, and slot registry are client-side. Nothing here touches dispatch, the wire, or `RemoteServer` - ([backend.md](../spec/backend.md)); the server still only sees action payloads. + ([backend.md](../spec/core/backend.md)); the server still only sees action payloads. - **The kit does not test look.** It asserts *contract behavior* (order, gating, exact payloads, options fetch), never pixels or styling — those are renderer-specific and out of scope. @@ -234,11 +234,11 @@ schema directly. - [gui_overview.md](gui_overview.md) — the ecosystem layer this document occupies; the "renderer-agnostic contract, QML reference" and "escape-hatch/override" principles the three deliverables realise. -- [forms.md](../spec/forms.md) — the **normative** `x-*` vocabulary and renderer +- [forms.md](../spec/forms/forms.md) — the **normative** `x-*` vocabulary and renderer contract (the `$ref` dual-read, `x-order`, `ExtUnits`, `x-decimalPlaces`, `x-unitAlternatives`, `x-optionsAction`) the reference renderer consumes and the conformance kit formalises into executable assertions. -- [choice.md](../spec/choice.md) — the options-fetch / `optionRows` behavior the +- [choice.md](../spec/forms/choice.md) — the options-fetch / `optionRows` behavior the renderer implements and the kit asserts. - [gui_i18n.md](gui_i18n.md) — the `TranslationProvider` seam the shipped renderer hosts, and the locale fixtures the kit runs (including under the @@ -247,7 +247,7 @@ schema directly. [gui_workflows_navigation.md](gui_workflows_navigation.md) — the `v-*` / `w-*` / `app-*` view schemas the shipped renderer also consumes and the kit's collection / wizard / app fixtures exercise. -- [bridge.md](../spec/bridge.md) — the `set<>` / auto-fire path a slot override +- [bridge.md](../spec/core/bridge.md) — the `set<>` / auto-fire path a slot override participates in, unchanged. -- [backend.md](../spec/backend.md) — unchanged; the renderer, kit, and registry +- [backend.md](../spec/core/backend.md) — unchanged; the renderer, kit, and registry are client-side and never touch dispatch or the wire. diff --git a/docs/planned/gui_widget_hints.md b/docs/planned/gui_widget_hints.md index 52109a0..8605dca 100644 --- a/docs/planned/gui_widget_hints.md +++ b/docs/planned/gui_widget_hints.md @@ -2,8 +2,8 @@ > **Status: planned — not yet implemented.** This spec extends the GUI program > umbrella ([gui_overview.md](gui_overview.md)) and the schema-generation spine -> ([forms.md](../spec/forms.md)), following the type-derived-metadata pattern of -> [choice.md](../spec/choice.md). It is a Tier-1 richer-forms feature: control +> ([forms.md](../spec/forms/forms.md)), following the type-derived-metadata pattern of +> [choice.md](../spec/forms/choice.md). It is a Tier-1 richer-forms feature: control > selection for a single action's fields, additive and opt-in. See > [todo.md](../todo.md). @@ -13,7 +13,7 @@ A renderer today picks a control almost entirely from the JSON-Schema `type` and morph's existing `x-*` keys: a `Choice` (`x-optionsAction`) becomes a combo box, a `Quantity` becomes a numeric input with a unit selector, a `Timestamp` (`"format": "date-time"`) becomes a date-time picker, a `bool` a checkbox, a -`string` a single-line text field ([forms.md](../spec/forms.md), "Renderer +`string` a single-line text field ([forms.md](../spec/forms/forms.md), "Renderer contract"). That covers the typed field palette but leaves real control choices unexpressed: @@ -53,7 +53,7 @@ Unannotated plain types keep their current controls unchanged. In the spirit of `Choice`/`Quantity` — a thin wrapper whose *wire form is its payload* and whose rendering intent lives in the C++ type — the following NEW wrappers are proposed. Each serialises through glaze `meta` as its bare payload -(the `Choice` pattern, [choice.md](../spec/choice.md)) so the wire is unchanged; +(the `Choice` pattern, [choice.md](../spec/forms/choice.md)) so the wire is unchanged; each carries a `noexcept` `hasValue()` where it can be empty so it satisfies `EmptyCapableField` and gates `allRequiredEngaged` (verified in `forms.hpp`) exactly like `Choice`: @@ -82,7 +82,7 @@ struct Ranged { ``` These follow the same three properties `Choice` established -([choice.md](../spec/choice.md)): the control intent is a compile-time property +([choice.md](../spec/forms/choice.md)): the control intent is a compile-time property of the type, the wire carries only the value, and the schema bridges the gap via a property-level `x-*` key. A renderer needs no new wire handling — only to honour the emitted `x-widget`. @@ -118,10 +118,10 @@ range subfields for a `Ranged`: | `x-widget` | property node (sibling of `$ref`) | string | The preferred control id: `"textarea"`, `"slider"`, `"radio"`, `"combo"`, `"password"`, `"checkbox"`, … A `FieldMeta::widget` override wins; else the field type's `widget()`. **Advisory** — a renderer that lacks the named control falls back to the type-default control (text area → text field, slider → numeric input, radio → combo). Omitted when neither a wrapper nor an override supplies one. | | `x-min` | property node (sibling of `$ref`) | number | Slider lower bound, from `Ranged::min()`. Emitted only for a `Ranged` field. Distinct from glaze's schema `minimum` (a *validation* bound); `x-min` is the *control track* start. A renderer without a slider ignores it. | | `x-max` | property node (sibling of `$ref`) | number | Slider upper bound, from `Ranged::max()`. Emitted only for a `Ranged` field. | -| `x-step` | property node (sibling of `$ref`) | number | Slider / numeric increment, from `Ranged::step()`. Emitted only for a `Ranged` field. For a `Quantity` the entry granularity remains `x-decimalPlaces` ([forms.md](../spec/forms.md)); `x-step` is not emitted for `Quantity`. | +| `x-step` | property node (sibling of `$ref`) | number | Slider / numeric increment, from `Ranged::step()`. Emitted only for a `Ranged` field. For a `Quantity` the entry granularity remains `x-decimalPlaces` ([forms.md](../spec/forms/forms.md)); `x-step` is not emitted for `Quantity`. | All keys are **additive and non-breaking**: they extend the -[forms.md](../spec/forms.md) contract table with no change to any existing key, +[forms.md](../spec/forms/forms.md) contract table with no change to any existing key, per [gui_overview.md](gui_overview.md)'s versioning stance. Because `x-widget` is **advisory**, a renderer that ignores it **falls back to exactly today's behaviour** — it selects the control from `type` and the existing keys, so a @@ -168,7 +168,7 @@ Illustrative of *one* renderer; the contract stays renderer-agnostic. - **No option-count-driven auto radio/combo.** Whether a small `Choice` renders as radios is an explicit `x-widget` decision, not inferred from a live option count — the option count is a runtime property of the options action - ([choice.md](../spec/choice.md)), not known at schema-generation time. + ([choice.md](../spec/forms/choice.md)), not known at schema-generation time. ## Testing (planned) @@ -194,11 +194,11 @@ Illustrative of *one* renderer; the contract stays renderer-agnostic. - [gui_overview.md](gui_overview.md) — the derive-from-type-first principle, the wrapper-type family (`Quantity`/`Choice`) this extends, and the additive-`x-*` versioning stance. -- [forms.md](../spec/forms.md) — `schemaJson()`, `mergeSchemaExtras`, +- [forms.md](../spec/forms/forms.md) — `schemaJson()`, `mergeSchemaExtras`, `forEachNamedMember`, `EmptyCapableField` / `required` derivation the new wrappers plug into, the `x-decimalPlaces` entry-granularity precedent, and the renderer-contract table these keys extend. -- [choice.md](../spec/choice.md) — the type-carries-intent / wire-carries-value +- [choice.md](../spec/forms/choice.md) — the type-carries-intent / wire-carries-value pattern (`glz::meta` reflecting the payload) the new wrappers follow. - [gui_field_metadata.md](gui_field_metadata.md) — the `FieldMeta` descriptor that also carries the `x-widget` override, keeping one per-field surface. diff --git a/docs/planned/gui_workflows_navigation.md b/docs/planned/gui_workflows_navigation.md index 4f1f4b2..cc434c1 100644 --- a/docs/planned/gui_workflows_navigation.md +++ b/docs/planned/gui_workflows_navigation.md @@ -2,11 +2,11 @@ > **Status: planned — not yet implemented.** This is the highest-level > view-schema document. It adds two **NEW** top-level descriptors above the -> per-action form schema of [forms.md](../spec/forms.md): a **wizard** that +> per-action form schema of [forms.md](../spec/forms/forms.md): a **wizard** that > sequences several actions into one flow with a draft spanning them, and an > **app-shell / route** descriptor (menu → screens → actions/views) so a renderer > builds navigation. It sits in Tier 2 of [gui_overview.md](gui_overview.md), -> extends the reactive draft of [bridge.md](../spec/bridge.md) to span a +> extends the reactive draft of [bridge.md](../spec/core/bridge.md) to span a > sequence, and composes the screens of > [gui_collections_views.md](gui_collections_views.md). See [todo.md](../todo.md). @@ -14,7 +14,7 @@ Two ceilings remain once single screens exist: -- **No multi-step flow.** [bridge.md](../spec/bridge.md)'s reactive draft +- **No multi-step flow.** [bridge.md](../spec/core/bridge.md)'s reactive draft (`subscribe`/`set<>`/`reset`) spans **one action**: a draft is created lazily on the first `set<>` for its action type and fires when that action is ready. A real onboarding or checkout is several actions in sequence sharing @@ -35,7 +35,7 @@ Two additive, independent descriptors: 1. A **wizard schema** — a `NEW` document naming an ordered list of actions and a **shared draft** that spans them, so a value entered in one step is available to later steps. It extends the per-action draft in - [bridge.md](../spec/bridge.md) to a *sequence* draft, reusing the existing + [bridge.md](../spec/core/bridge.md) to a *sequence* draft, reusing the existing `set<>` / validator machinery per step. 2. An **app-shell / route schema** — a `NEW` lightweight top-level document (menu → screens → actions/views) a renderer turns into navigation, so the same @@ -71,7 +71,7 @@ schemas, referencing action type-ids only. Its **NEW** key vocabulary (`w-*`): | Key | Where | JSON type | Meaning / renderer obligation | |---|---|---|---| | `w-title` | top-level | string | Human title for the whole flow. | -| `w-steps` | top-level | array | Ordered steps. Each step is one registered action rendered as its ordinary [forms.md](../spec/forms.md) form. | +| `w-steps` | top-level | array | Ordered steps. Each step is one registered action rendered as its ordinary [forms.md](../spec/forms/forms.md) form. | | ↳ `action` | step | string | Type-id of the step's action. | | ↳ `title` | step | string | Step label (breadcrumb / header). | | ↳ `prefill` | step | object | Maps this action's field → a `"."` path into an earlier step's *result or submitted draft*. This is how the shared draft threads a value forward. | @@ -79,7 +79,7 @@ schemas, referencing action type-ids only. Its **NEW** key vocabulary (`w-*`): All `w-*` keys are **NEW proposed** keys in a **NEW proposed** document; none are verified against existing code. -**The shared draft — extending [bridge.md](../spec/bridge.md).** Today each +**The shared draft — extending [bridge.md](../spec/core/bridge.md).** Today each action type owns one `SubscriberEntry` draft keyed by `ActionTraits::typeId()`, created on the first `set<>` and persisting across fires. A wizard groups several such drafts into one **flow draft** that outlives @@ -104,7 +104,7 @@ public: - **A step is an ordinary action fire.** `advance()` fires the current step through the same `execute` / reactive path as a standalone form - ([bridge.md](../spec/bridge.md)); `ActionValidator::ready` gates it + ([bridge.md](../spec/core/bridge.md)); `ActionValidator::ready` gates it exactly as it gates a lone form. The wizard adds *sequencing*, not a new execution mode. - **`prefill` threads state forward.** On entering a step, the renderer resolves @@ -114,7 +114,7 @@ public: [gui_collections_views.md](gui_collections_views.md), but the source is an earlier step rather than a table row. - **Drafts persist across steps and `back()`.** A per-step draft persists exactly - as [bridge.md](../spec/bridge.md) already guarantees (a draft "persists across + as [bridge.md](../spec/core/bridge.md) already guarantees (a draft "persists across fires ... destroyed only when the handler is destroyed or `reset()`"). The flow session simply keeps every step's draft alive for the flow's lifetime, so navigating `back()` and forward preserves entered values without re-fetching. @@ -151,7 +151,7 @@ navigation, replacing the "enumerate every schema onto one scroll" of | `app-title` | top-level | string | Application title (window/header). | | `app-menu` | top-level | array | Ordered navigation entries `{ label, screen }`. A renderer builds a menu/sidebar/tabs from these; the target `screen` keys into `app-screens`. | | `app-screens` | top-level | object | Map of screen-id → screen descriptor. | -| ↳ `kind` | screen | string | `"form"` (one action, [forms.md](../spec/forms.md)), `"view"` (a collection/master-detail, [gui_collections_views.md](gui_collections_views.md)), or `"wizard"` (a flow, above). | +| ↳ `kind` | screen | string | `"form"` (one action, [forms.md](../spec/forms/forms.md)), `"view"` (a collection/master-detail, [gui_collections_views.md](gui_collections_views.md)), or `"wizard"` (a flow, above). | | ↳ `ref` | screen | string | The type-id of the referenced action / view / wizard. The renderer fetches that thing's own schema and renders it into the routed area. | A screen is therefore **just a reference** to a Tier-1 form, a @@ -161,7 +161,7 @@ An `app-*`-ignorant renderer falls back to today's behavior: it can still load each referenced action schema directly and render a form. The shell is declared in C++ as a descriptor over registered ids and registered with a **NEW** `BRIDGE_REGISTER_APP` macro (parallel to `BRIDGE_REGISTER_ACTION`, -[registry.md](../spec/registry.md)); like the view registration it is metadata +[registry.md](../spec/core/registry.md)); like the view registration it is metadata only — no dispatch path is added. ### One consistent layering @@ -190,7 +190,7 @@ below, so every layer is independently ignorable — the additive-key contract o it is not a distributed transaction. All-or-nothing is a model-layer concern ([outbox.md](outbox.md)), not a wizard-schema one. - **No new dispatch path or wire change.** Each step and each screen fires - ordinary actions over the existing path ([bridge.md](../spec/bridge.md)); the + ordinary actions over the existing path ([bridge.md](../spec/core/bridge.md)); the wizard and app documents are metadata a renderer consumes. - **No server-driven navigation.** The menu/screens are a static declared descriptor, not a server-pushed, permission-filtered navigation tree. Hiding a @@ -202,14 +202,14 @@ below, so every layer is independently ignorable — the additive-key contract o prefilled shared draft is the committed scope. - **Draft is client-side only.** The flow draft lives in the handler's `SubscriberState`, exactly as the per-action draft does - ([bridge.md](../spec/bridge.md)); it is not persisted or resumable across a + ([bridge.md](../spec/core/bridge.md)); it is not persisted or resumable across a process restart. ## Testing (planned) - `wizardSchemaJson()` emits `w-title` and ordered `w-steps` with per-step `action`, `title`, and `prefill`; each referenced action still emits - its ordinary [forms.md](../spec/forms.md) schema unchanged. + its ordinary [forms.md](../spec/forms/forms.md) schema unchanged. - A `FlowSession` fires step 1, captures its result, and `advance()`s to step 2 with the `prefill` paths issued as `set<>` on step 2's draft; `back()` returns to step 1 with its entered values intact (draft persistence). @@ -222,22 +222,22 @@ below, so every layer is independently ignorable — the additive-key contract o plain form (additive-key fallback / regression guard). - Backend switch mid-flow: an in-flight step fire surfaces `BackendChangedError` on the step's `errSink` while the flow draft survives, matching - [bridge.md](../spec/bridge.md)'s draft-survives-switch guarantee, so the step + [bridge.md](../spec/core/bridge.md)'s draft-survives-switch guarantee, so the step re-fires cleanly against the new backend. ## Cross-references - [gui_overview.md](gui_overview.md) — Tier 2; the highest layer of the view/app schema stack and the additive-key contract each `*-` vocabulary obeys. -- [forms.md](../spec/forms.md) — the per-action form each wizard step and each +- [forms.md](../spec/forms/forms.md) — the per-action form each wizard step and each `kind: "form"` screen renders. - [gui_collections_views.md](gui_collections_views.md) — the `kind: "view"` screens the shell routes to; the `bind` prefill mechanic wizards reuse as `prefill`. -- [bridge.md](../spec/bridge.md) — the per-action reactive draft +- [bridge.md](../spec/core/bridge.md) — the per-action reactive draft (`subscribe`/`set<>`/`reset`, draft persistence across fires and backend switches) the wizard's shared flow draft extends to span a sequence. -- [registry.md](../spec/registry.md) — `ActionTraits::typeId()` (the ids the +- [registry.md](../spec/core/registry.md) — `ActionTraits::typeId()` (the ids the wizard/app reference) and the `BRIDGE_REGISTER_ACTION` pattern the proposed `BRIDGE_REGISTER_APP` mirrors. - [outbox.md](outbox.md) — where cross-action atomicity belongs (the wizard diff --git a/docs/planned/instance_authorization.md b/docs/planned/instance_authorization.md index c6ea60f..c3c8f03 100644 --- a/docs/planned/instance_authorization.md +++ b/docs/planned/instance_authorization.md @@ -169,12 +169,12 @@ rather than a literal — which is the only contract the ids ever guaranteed. - [security.md](../spec/security.md) — the trust model this extends; the shipped `authorize`/`authenticate`/`authorizeInstance` seam and the explicit "register is unauthorized / ids are guessable" limitations this spec closes. -- [backend.md](../spec/backend.md) — `RemoteServer` register/execute/deregister handling, +- [backend.md](../spec/core/backend.md) — `RemoteServer` register/execute/deregister handling, `_nextId`, the `LogProvider`/owner recording, and the `make_shared` lifetime rule the new hooks slot into. -- [session.md](../spec/session.md) — `IAuthorizer`, `Context`, `authenticate`; the new +- [session.md](../spec/session/session.md) — `IAuthorizer`, `Context`, `authenticate`; the new `authorizeRegister` is declared alongside them. -- [wire.md](../spec/wire.md) — the `register` envelope (`typeId`, `contextKey`) +- [wire.md](../spec/core/wire.md) — the `register` envelope (`typeId`, `contextKey`) and the envelope-level `session` the gate reads (wire.md documents `session` for `execute`; the register path already reads it for owner recording — see [security.md](../spec/security.md)); ids stay `uint64_t` so the envelope is diff --git a/docs/planned/journal_evolution.md b/docs/planned/journal_evolution.md index e2b90ca..759bf48 100644 --- a/docs/planned/journal_evolution.md +++ b/docs/planned/journal_evolution.md @@ -4,20 +4,20 @@ > counterpart of [protocol_versioning.md](protocol_versioning.md): that spec > governs peers exchanging envelopes *now*; this one governs a process reading > NDJSON journal lines written months of releases ago. It extends -> [journal.md](../spec/journal.md) (`LogEntry`, `FileActionLog`, `replay()`) +> [journal.md](../spec/journal/journal.md) (`LogEntry`, `FileActionLog`, `replay()`) > and closes two of its documented limitations — no format version on > persisted lines, unbounded file growth. See [todo.md](../todo.md). ## The gap - **No version stamp.** A `FileActionLog` line is a bare `toJson(LogEntry)` - ([journal.md](../spec/journal.md)); nothing records which line format (or + ([journal.md](../spec/journal/journal.md)); nothing records which line format (or which era of action struct) wrote it. A reader confronted with an incompatible file cannot even *detect* it, let alone say what wrote it. - **The reader is strict where the wire is lenient.** `journal::fromJson` decodes with glaze's default options — unknown keys are an **error** — unlike `wire::decode`, which explicitly reads with - `error_on_unknown_keys = false` ([wire.md](../spec/wire.md)). Today that + `error_on_unknown_keys = false` ([wire.md](../spec/core/wire.md)). Today that means the journal format cannot gain *any* new key without every older reader throwing `SerializationError` on the whole file: adding the version stamp itself would be a flag-day. Reader leniency has to land first. @@ -41,7 +41,7 @@ `journal::fromJson` switches to the same explicit stance as the wire: `glz::opts{.error_on_unknown_keys = false}`, for the same reason — a newer writer may add a key an older reader does not know. The duplicate-key -caveat transfers verbatim ([wire.md](../spec/wire.md): last-wins, not a +caveat transfers verbatim ([wire.md](../spec/core/wire.md): last-wins, not a security boundary). This must ship at least one release **before** any new key is written, so downgraded or side-by-side readers never hit the flag-day; the sequencing is the point of doing it now, ahead of need. @@ -67,7 +67,7 @@ struct LogEntry { morph") — fail loud rather than guess at a shape this build has never seen. The existing positional torn-line rule is unchanged: a failing **last** line is still skipped with a warning whatever the failure's cause, an - interior failure still throws ([journal.md](../spec/journal.md)). + interior failure still throws ([journal.md](../spec/journal/journal.md)). - `kLogFormatVersion` bumps only on a *breaking* change to the line format; additive keys (tolerated by step 0) do not bump it — mirroring `kProtocolVersion`'s discipline @@ -109,7 +109,7 @@ void rotate(const std::filesystem::path& sealedPath); - Full-history reads and replay compose segments **oldest → newest, then the active file** — documented recipe, no new API. File order is already the cross-restart ordering authority (`seq` stays process-local, unchanged — - [journal.md](../spec/journal.md)). + [journal.md](../spec/journal/journal.md)). - Crash safety: the rename is a single atomic filesystem operation; a crash around it leaves either the old active file or the sealed file plus a recreated-empty active — never a split line, so the torn-line rule keeps @@ -140,7 +140,7 @@ their stores should adopt this contract's terms when they land. - **No compaction of sealed history.** `checkpoint()` is the reducer, applied *before* entries become durable; once written, the audit trail is immutable ("entries are never removed by the framework", - [journal.md](../spec/journal.md)). + [journal.md](../spec/journal/journal.md)). - **No multi-process appenders.** The existing single-process constraint on `FileActionLog` stands; rotation does not change it. - **No encryption or signing of segments.** At-rest protection of archived @@ -163,10 +163,10 @@ their stores should adopt this contract's terms when they land. ## Cross-references -- [journal.md](../spec/journal.md) — `LogEntry`, `toJson`/`fromJson`, +- [journal.md](../spec/journal/journal.md) — `LogEntry`, `toJson`/`fromJson`, `FileActionLog` (NDJSON, fsync, torn-line rule, process-local `seq`), `replay()`, and the two limitations this closes. -- [wire.md](../spec/wire.md) — the `error_on_unknown_keys = false` precedent +- [wire.md](../spec/core/wire.md) — the `error_on_unknown_keys = false` precedent and duplicate-key caveat the journal reader adopts. - [protocol_versioning.md](protocol_versioning.md) — the additive-only policy and deprecation window this extends to retention scope; the diff --git a/docs/planned/non_qt_transport.md b/docs/planned/non_qt_transport.md index 462af1c..e22c32b 100644 --- a/docs/planned/non_qt_transport.md +++ b/docs/planned/non_qt_transport.md @@ -1,18 +1,18 @@ # A non-Qt / transport-agnostic reference transport (planned) > **Status: planned — not yet implemented.** This spec extends -> [backend.md](../spec/backend.md) (the `IBackend` contract and `RemoteServer` -> handling) and [wire.md](../spec/wire.md) (the `Envelope`). It provides a +> [backend.md](../spec/core/backend.md) (the `IBackend` contract and `RemoteServer` +> handling) and [wire.md](../spec/core/wire.md) (the `Envelope`). It provides a > reference transport that does not depend on Qt, so a server or client can run > without a Qt event loop. See [todo.md](../todo.md). ## The gap -The only network transport is Qt. [backend.md](../spec/backend.md)'s Limitations +The only network transport is Qt. [backend.md](../spec/core/backend.md)'s Limitations name it: "`QtWebSocketBackend` must live on the Qt event loop thread; there is no way to drive it from a plain worker thread ... WebSocket transport is single-threaded and Qt-bound." `RemoteServer` itself is transport-agnostic (it -"receives JSON envelopes ... from any transport," [backend.md](../spec/backend.md)), +"receives JSON envelopes ... from any transport," [backend.md](../spec/core/backend.md)), but the only concrete client `IBackend` and the only concrete server front are `QtWebSocketBackend` / `QtWebSocketServer`. @@ -35,7 +35,7 @@ for Qt apps. ### The contract it must satisfy (all EXISTING) The reference client backend implements every method of -`detail::IBackend` ([backend.md](../spec/backend.md)), matching the semantics the +`detail::IBackend` ([backend.md](../spec/core/backend.md)), matching the semantics the existing transports already establish: | Method | Reference behavior (mirrors `QtWebSocketBackend`) | @@ -49,9 +49,9 @@ existing transports already establish: | `setReconnectHandler(handler)` | Stores it; invoked on each *subsequent* connect so `Bridge` re-registers live bindings. | It reuses the existing typed error hierarchy — `DisconnectedError`, -`BackendChangedError`, `BridgeDestroyedError` ([backend.md](../spec/backend.md)) — +`BackendChangedError`, `BridgeDestroyedError` ([backend.md](../spec/core/backend.md)) — unchanged, and the existing `callId` multiplexing so concurrent in-flight -executes work (`RemoteServer` echoes the request `callId`, [wire.md](../spec/wire.md)). +executes work (`RemoteServer` echoes the request `callId`, [wire.md](../spec/core/wire.md)). ### The threading model is the key difference @@ -76,14 +76,14 @@ public: frames incoming messages and routes them by `callId` to the pending map (guarded by a mutex, since replies arrive on the I/O thread and `execute`/`cancelPending` can be called from `Bridge` on another thread — the same locking `QtWebSocketBackend` - applies to `_pending`, [backend.md](../spec/backend.md)). + applies to `_pending`, [backend.md](../spec/core/backend.md)). - **Synchronous control via condition variable.** `registerModel` sends and waits on a `std::condition_variable` for the correlated reply (or a disconnect wakeup), replacing the nested `QEventLoop`. The disconnect path wakes a parked waiter so a register whose reply never arrives fails with `"disconnected"` rather than hanging — the same hardening `sendSync` has today. - **Callbacks still marshal via `cbExec`.** Completion `.then`/`.onError` deliver - on the `IExecutor*` passed to `execute` ([completion.md](../spec/completion.md)), + on the `IExecutor*` passed to `execute` ([completion.md](../spec/core/completion.md)), so a non-Qt host uses a `ThreadPoolExecutor`/`MainThreadExecutor` ([ARCHITECTURE.md](../ARCHITECTURE.md)) as `cbExec` instead of `QtExecutor`. @@ -107,13 +107,13 @@ public: - For each accepted connection it reads framed text messages and calls `RemoteServer::handle(msg, reply)` (async, posts to the server pool) — identical - to how `QtWebSocketServer::onTextMessage` forwards ([backend.md](../spec/backend.md)). + to how `QtWebSocketServer::onTextMessage` forwards ([backend.md](../spec/core/backend.md)). - The `reply` callback writes back to the originating socket; if the socket is gone before the reply is ready, the reply is dropped (mirroring the Qt front's weak-`QPointer` behavior). - It honours the same lifetime rule: it holds `RemoteServer& _server` by reference, so the server's owning `shared_ptr` must outlive it - ([backend.md](../spec/backend.md)'s Lifetime & ownership). + ([backend.md](../spec/core/backend.md)'s Lifetime & ownership). - TLS and peer verification are the transport's responsibility, per [security.md](../spec/security.md); the reference front documents the same verify-the-peer default as [tls_peer_verification.md](tls_peer_verification.md) @@ -165,13 +165,13 @@ decision recorded at build time. ## Cross-references -- [backend.md](../spec/backend.md) — the full `IBackend` contract +- [backend.md](../spec/core/backend.md) — the full `IBackend` contract (`registerModel`/`registerModelWithContext`/`deregisterModel`/`execute`/ `notifyBackendChanged`/`cancelPending`/`setReconnectHandler`) this implements, the `RemoteServer::handle`/`handleInline` forwarding it drives, the typed error hierarchy it reuses, and the `QtWebSocketBackend`/`QtWebSocketServer` behavior it mirrors without Qt. -- [wire.md](../spec/wire.md) — the `Envelope`, `kind` discriminator, and `callId` +- [wire.md](../spec/core/wire.md) — the `Envelope`, `kind` discriminator, and `callId` correlation both transports share, and `kMaxEnvelopeBytes`. - [security.md](../spec/security.md) — transport confidentiality/peer authentication is the transport's job; the reference front must supply TLS. @@ -179,5 +179,5 @@ decision recorded at build time. default the non-Qt front documents rather than an insecure example. - [transport_limits.md](transport_limits.md) — the connection-level limits any transport (Qt or not) should carry; the reference front's `Config` mirrors them. -- [completion.md](../spec/completion.md) — `Completion`/`cbExec` callback delivery +- [completion.md](../spec/core/completion.md) — `Completion`/`cbExec` callback delivery a non-Qt host wires to a `ThreadPoolExecutor` instead of `QtExecutor`. diff --git a/docs/planned/observability.md b/docs/planned/observability.md index 5437f61..6442a63 100644 --- a/docs/planned/observability.md +++ b/docs/planned/observability.md @@ -2,12 +2,12 @@ > **Status: planned — not yet implemented.** This spec introduces an injectable > observability seam modelled on the replaceable-sink pattern in -> [logger.md](../spec/logger.md), and adds a health/readiness signal to -> [backend.md](../spec/backend.md)'s `RemoteServer`. See [todo.md](../todo.md). +> [logger.md](../spec/core/logger.md), and adds a health/readiness signal to +> [backend.md](../spec/core/backend.md)'s `RemoteServer`. See [todo.md](../todo.md). ## The gap -Logging is a replaceable sink ([logger.md](../spec/logger.md)): a global +Logging is a replaceable sink ([logger.md](../spec/core/logger.md)): a global `std::function` swapped once at startup via `setLogger`, with a lock-free level check on the hot path. That is the only observability seam. There is: @@ -20,9 +20,9 @@ observability seam. There is: ([transport_limits.md](transport_limits.md)) is meant to bound are not even observable. - **No tracing hooks.** A single `execute` crosses the calling thread, the model - strand, and the callback executor ([backend.md](../spec/backend.md)'s "Thread + strand, and the callback executor ([backend.md](../spec/core/backend.md)'s "Thread context"), but there is no span/correlation seam to follow one call across those - threads. `Context::requestId` exists on the wire ([wire.md](../spec/wire.md)) + threads. `Context::requestId` exists on the wire ([wire.md](../spec/core/wire.md)) but nothing consumes it for tracing. - **No health/readiness signal.** `RemoteServer` has no "am I up and able to serve?" callback or endpoint, so a load balancer or orchestrator has nothing to @@ -30,7 +30,7 @@ observability seam. There is: `morph` deliberately keeps the logger minimal — its sink signature is only `(LogLevel, std::string_view)`, with "no source location, no category, no -timestamp, no thread id" ([logger.md](../spec/logger.md)'s Limitations). Metrics +timestamp, no thread id" ([logger.md](../spec/core/logger.md)'s Limitations). Metrics and traces cannot be smuggled through that signature; they need their own seam, built the same injectable way. @@ -85,7 +85,7 @@ void setMetricSink(MetricSink); [[nodiscard]] bool metricsEnabled() noexcept; ``` -- **Hot-path discipline** matches [logger.md](../spec/logger.md): the framework +- **Hot-path discipline** matches [logger.md](../spec/core/logger.md): the framework checks `metricsEnabled()` (a relaxed atomic load, no mutex) before constructing any `MetricEvent`, so a suppressed metric pays only the atomic load — the same contract that lets a filtered `logDebug` skip `std::format`. @@ -95,8 +95,8 @@ void setMetricSink(MetricSink); ### 2. A trace seam (NEW) -Tracing reuses the existing `Context::requestId` ([wire.md](../spec/wire.md), -[session.md](../spec/session.md)) as the correlation id and adds span +Tracing reuses the existing `Context::requestId` ([wire.md](../spec/core/wire.md), +[session.md](../spec/session/session.md)) as the correlation id and adds span begin/end hooks the framework calls around a dispatch: ```cpp @@ -119,7 +119,7 @@ void setTraceSink(TraceSink); // default: none (no-op), thread-safe swap - The framework calls `beginSpan`/`endSpan` around the `ActionDispatcher::dispatch` call inside `RemoteServer::dispatchExecute` and around `localOp` in - `LocalBackend::execute` ([backend.md](../spec/backend.md)) — the same two call + `LocalBackend::execute` ([backend.md](../spec/core/backend.md)) — the same two call sites the journal records at ([ARCHITECTURE.md](../ARCHITECTURE.md)), so a trace covers exactly one `Model::execute`. - With no trace sink, both hooks are skipped behind the same enabled-check as @@ -128,7 +128,7 @@ void setTraceSink(TraceSink); // default: none (no-op), thread-safe swap ### 3. Health / readiness on `RemoteServer` (NEW) Add a readiness query and an optional state-change callback to `RemoteServer` -([backend.md](../spec/backend.md)): +([backend.md](../spec/core/backend.md)): ```cpp // namespace morph::backend — NEW on RemoteServer. @@ -164,7 +164,7 @@ void setHealthHandler(std::function); - **Not a replacement for the logger.** `morph::log` still carries free-text diagnostics; the observability seam carries *structured numbers and spans* the logger's `(LogLevel, string_view)` signature cannot express - ([logger.md](../spec/logger.md)). They are complementary sinks. + ([logger.md](../spec/core/logger.md)). They are complementary sinks. - **No sampling/aggregation logic in `morph`.** The framework emits raw observations; rate-limiting, histograms, and sampling live in the host's sink. - **No preemption or control.** Metrics/health *observe*; they do not throttle or @@ -189,17 +189,17 @@ void setHealthHandler(std::function); ## Cross-references -- [logger.md](../spec/logger.md) — the replaceable-sink pattern (global +- [logger.md](../spec/core/logger.md) — the replaceable-sink pattern (global `std::function` sink, mutex-guarded swap, lock-free hot-path check) this seam copies, and the sink-signature limitation that makes a separate metrics/trace seam necessary. -- [backend.md](../spec/backend.md) — `RemoteServer`/`LocalBackend` dispatch call +- [backend.md](../spec/core/backend.md) — `RemoteServer`/`LocalBackend` dispatch call sites the metric/trace hooks wrap, the "Thread context" a trace follows, and where `health()`/`setHealthHandler` live. - [transport_limits.md](transport_limits.md) — `LimitPolicy`'s in-flight/live-model counters this seam surfaces (shared state), and the values metrics make observable so limits can be tuned. -- [offline.md](../spec/offline.md) — `SyncWorker`/`ReconnectCoordinator` and the +- [offline.md](../spec/offline/offline.md) — `SyncWorker`/`ReconnectCoordinator` and the `IOfflineQueue` depth the `queueDepth`/`reconnect*` metrics report on. -- [wire.md](../spec/wire.md) / [session.md](../spec/session.md) — +- [wire.md](../spec/core/wire.md) / [session.md](../spec/session/session.md) — `Context::requestId`, reused as the trace correlation id. diff --git a/docs/planned/outbox.md b/docs/planned/outbox.md index eae75c7..4270123 100644 --- a/docs/planned/outbox.md +++ b/docs/planned/outbox.md @@ -1,14 +1,14 @@ # Transactional outbox — journal + store atomicity (planned) > **Status: planned — not yet implemented.** This spec extends -> [journal.md](../spec/journal.md). It closes the "no transactional outbox" limitation: +> [journal.md](../spec/journal/journal.md). It closes the "no transactional outbox" limitation: > the action log and a model's own durable store commit as two independent > writes and can diverge on a crash. See [todo.md](../todo.md). ## The gap `morph::journal` records every loggable action to an `IActionLog` after the -action succeeds (`recordIfAttached`, see [journal.md](../spec/journal.md)). A model that +action succeeds (`recordIfAttached`, see [journal.md](../spec/journal/journal.md)). A model that *also* owns a durable store (a SQL database, a file) therefore performs **two independent writes** per action: @@ -167,15 +167,15 @@ crash-consistent) log. ## Cross-references -- [journal.md](../spec/journal.md) — `LogEntry`, `IActionLog`, `recordIfAttached`, the +- [journal.md](../spec/journal/journal.md) — `LogEntry`, `IActionLog`, `recordIfAttached`, the two recording call sites, `SessionLog`/`replay`, and the "no transactional outbox" limitation this closes. - [durable_queue.md](durable_queue.md) — the `idempotencyKey` dedup contract the outbox relay reuses; both are producers into the same at-most-once mechanism. -- [offline.md](../spec/offline.md) — `QueueItem::idempotencyKey` (the original dedup +- [offline.md](../spec/offline/offline.md) — `QueueItem::idempotencyKey` (the original dedup token) and the injected-side-effect pattern (`ReconnectCoordinator::Deps`, `SyncWorker`) that `OutboxRelay` follows. -- [registry.md](../spec/registry.md) — `ActionDispatcher`'s runner and +- [registry.md](../spec/core/registry.md) — `ActionDispatcher`'s runner and `IModelHolder::attachActionLog`/`recordIfAttached`, where the auto-append suppression opt-out lives. - `examples/bank` — the concrete two-write divergence this pattern fixes. diff --git a/docs/planned/protocol_versioning.md b/docs/planned/protocol_versioning.md index c5d4694..dced977 100644 --- a/docs/planned/protocol_versioning.md +++ b/docs/planned/protocol_versioning.md @@ -1,7 +1,7 @@ # Protocol / action-schema versioning & negotiation (planned) > **Status: planned — not yet implemented.** This spec extends -> [wire.md](../spec/wire.md) (the `Envelope` and its forward-compatible decode) and +> [wire.md](../spec/core/wire.md) (the `Envelope` and its forward-compatible decode) and > [security.md](../spec/security.md). It adds a negotiated protocol version on > connect and a documented action-evolution policy, on top of the passive > forward-compatibility the wire layer already gives. See [todo.md](../todo.md). @@ -13,17 +13,17 @@ The wire has *passive* forward compatibility but no *negotiated* version: - **Unknown keys are ignored.** `wire::decode` reads with `glz::read<{.error_on_unknown_keys = false}>`, so "a newer peer may add a field an older peer does not know (and vice versa) without breaking the parse — this - is the wire's forward-compatibility contract" ([wire.md](../spec/wire.md)). A + is the wire's forward-compatibility contract" ([wire.md](../spec/core/wire.md)). A new `Envelope` field is absorbed silently by an old peer. - **But there is no version field and no handshake.** Neither the `Envelope` - ([wire.md](../spec/wire.md) API reference lists `kind`, `callId`, `typeId`, + ([wire.md](../spec/core/wire.md) API reference lists `kind`, `callId`, `typeId`, `contextKey`, `modelId`, `modelType`, `actionType`, `body`, `message`, `session` — no version) nor the connect flow carries a protocol version. A peer cannot discover *whether* the other side speaks a compatible protocol; it can only find out by a request failing in some field-specific way. - **No action-evolution policy.** An action struct (the JSON inside the opaque `body`) can change shape between a client build and a server build. Because - `body` is re-parsed by the action codec ([wire.md](../spec/wire.md)'s "body + `body` is re-parsed by the action codec ([wire.md](../spec/core/wire.md)'s "body double-parse"), a removed or retyped field is a per-action decode failure with no framework-level story for it. There is no written rule that keeps action evolution additive-only, and no deprecation window. @@ -61,7 +61,7 @@ struct Envelope { - `protocolVersion` defaults to `0`, which decodes for **every existing peer** (an old encoder never sets it; `decode` leaves absent keys at their default per - [wire.md](../spec/wire.md)). `0` means "legacy / unspecified" and is treated + [wire.md](../spec/core/wire.md)). `0` means "legacy / unspecified" and is treated exactly as today's unversioned behavior — full backward compatibility. - Because unknown keys are ignored, a new client sending `protocolVersion` to an old server is harmless (the field is dropped), and an old client omitting it is @@ -83,7 +83,7 @@ Introduce a `"hello"` control `kind` exchanged once per connection, before any - The **client** (`QtWebSocketBackend`, `SimulatedRemoteBackend`) sends `"hello"` with `kProtocolVersion` immediately after connect (over the existing synchronous control path — `handleInline` for the simulated backend, the nested-loop - `sendSync` for Qt, per [backend.md](../spec/backend.md)). + `sendSync` for Qt, per [backend.md](../spec/core/backend.md)). - The **server** (`RemoteServer`) answers with its supported `{min, max}` range. If the client's version is outside the range, it replies `err` and the client refuses to proceed (surfaces a clear "protocol version unsupported" rather than @@ -92,7 +92,7 @@ Introduce a `"hello"` control `kind` exchanged once per connection, before any client) behaves exactly as today — `"hello"` is opt-in and its absence is the `protocolVersion == 0` legacy path. A legacy server that does not understand `"hello"` replies `err "unknown envelope kind: hello"` (the existing - unrecognised-kind path, [backend.md](../spec/backend.md)); the client treats + unrecognised-kind path, [backend.md](../spec/core/backend.md)); the client treats that specific error as "peer is unversioned / legacy" and continues without negotiation. @@ -136,7 +136,7 @@ This policy lives in `wire.md` (rewritten on implementation) and is the contract exchange. `forms::schemaJson()` already lets a client discover an action's current shape; this spec does not add a second mechanism. - **Not automatic migration of stored data.** The journal's `LogEntry` payloads - ([journal.md](../spec/journal.md)) are historical facts; replaying an old + ([journal.md](../spec/journal/journal.md)) are historical facts; replaying an old action across a version boundary is the host's concern, bounded by the same additive-only policy. No transform layer ships. - **Not a break in the forward-compat contract.** Unknown keys stay ignored; @@ -161,11 +161,11 @@ This policy lives in `wire.md` (rewritten on implementation) and is the contract ## Cross-references -- [wire.md](../spec/wire.md) — the `Envelope`, the `kind` discriminator, the +- [wire.md](../spec/core/wire.md) — the `Envelope`, the `kind` discriminator, the `error_on_unknown_keys = false` forward-compat contract this builds on, the `body` double-parse that makes action evolution a per-action decode concern, and `kMaxEnvelopeBytes`. -- [backend.md](../spec/backend.md) — `RemoteServer` control-message handling +- [backend.md](../spec/core/backend.md) — `RemoteServer` control-message handling (`handleInline`, the unrecognised-`kind` error path the legacy-detection reuses) and the client control paths (`QtWebSocketBackend` `sendSync`, `SimulatedRemoteBackend`) the `"hello"` exchange rides on. diff --git a/docs/planned/testing_strategy.md b/docs/planned/testing_strategy.md index 97cd8c0..039c7e1 100644 --- a/docs/planned/testing_strategy.md +++ b/docs/planned/testing_strategy.md @@ -5,7 +5,7 @@ > confidence in the §A hardening milestone ([validation.md](validation.md), > [instance_authorization.md](instance_authorization.md), > [transport_limits.md](transport_limits.md)) and exercises the untrusted-input -> boundaries in [wire.md](../spec/wire.md) and [backend.md](../spec/backend.md). +> boundaries in [wire.md](../spec/core/wire.md) and [backend.md](../spec/core/backend.md). > See [todo.md](../todo.md). ## The gap @@ -17,13 +17,13 @@ lists real hardening tests — `test_wire_hardening.cpp`, `test_server_limits.cp single-shot, in-process, deterministic unit test. Missing: - **No fuzzing.** `wire::decode` is the untrusted-input boundary - ([wire.md](../spec/wire.md)) and the action codec re-parses the opaque `body` a + ([wire.md](../spec/core/wire.md)) and the action codec re-parses the opaque `body` a second time, yet nothing throws *randomised, malformed, adversarial* input at either. `test_server_limits.cpp` checks a *fixed* 5000-deep nesting and a lone continuation byte — hand-picked cases, not a coverage-guided search. - **No soak test.** `switchBackend`, reconnect backoff, and - `ReconnectCoordinator`/`SyncWorker` churn ([offline.md](../spec/offline.md), - [backend.md](../spec/backend.md)) are tested for a single transition, not for + `ReconnectCoordinator`/`SyncWorker` churn ([offline.md](../spec/offline/offline.md), + [backend.md](../spec/core/backend.md)) are tested for a single transition, not for thousands of cycles over hours where a slow leak, a missed `cancelPending`, or a strand backlog would surface. - **No load/throughput benchmark.** There is no measurement of dispatch latency @@ -73,9 +73,9 @@ Targets: - **`wire::decode`** — the outer parse. The invariant under fuzzing: every input either decodes to an `Envelope` or throws `std::runtime_error`; it never crashes, hangs, or exhibits UB. The `kMaxEnvelopeBytes` cap - ([wire.md](../spec/wire.md)) bounds the input the harness need supply. + ([wire.md](../spec/core/wire.md)) bounds the input the harness need supply. - **The inner `body` re-parse** — feed a decoded `body` through a representative - action's `ActionTraits::fromJson`, since [wire.md](../spec/wire.md) warns the + action's `ActionTraits::fromJson`, since [wire.md](../spec/core/wire.md) warns the inner parse "needs its own limits" and the outer parse never walks `body`. This is where the double-parse hazard would detonate; the fuzzer proves it detonates *safely* (defined error, no crash). @@ -95,9 +95,9 @@ cycles the backend and connectivity machinery under continuous execute load: `SimulatedRemoteBackend`/`QtWebSocketBackend` while executes are in flight, asserting every in-flight `Completion` resolves (via result or `BackendChangedError`) and none leak — exercising the stage-all-then-commit atomicity and `cancelPending` - ([backend.md](../spec/backend.md), [ARCHITECTURE.md](../ARCHITECTURE.md)). + ([backend.md](../spec/core/backend.md), [ARCHITECTURE.md](../ARCHITECTURE.md)). - Drive `NetworkMonitor` → `ReconnectCoordinator` → `SyncWorker` through thousands - of offline/online flaps ([offline.md](../spec/offline.md)), asserting the + of offline/online flaps ([offline.md](../spec/offline/offline.md)), asserting the offline queue drains, attempt counts behave, and no unbounded growth occurs. - Measured for **resource stability**: RSS, live `ModelId` count, pending-map size, and strand queue depth must be flat across the run (a leak or a stuck strand @@ -125,7 +125,7 @@ Extend the cross-process TLS test ([security.md](../spec/security.md)'s `test_qt_websocket.cpp`) with a *hostile* client process that: sends oversized frames (past `kMaxEnvelopeBytes` and past a tighter transport cap), floods connections/messages (exercising [transport_limits.md](transport_limits.md)), -sends duplicate-key envelopes ([wire.md](../spec/wire.md)'s smuggling caveat), and +sends duplicate-key envelopes ([wire.md](../spec/core/wire.md)'s smuggling caveat), and opens-then-stalls connections. The server must stay up and serving honest clients throughout — the concrete demonstration that the limits and the wire bounds hold against a real adversary, not just a unit assertion. @@ -162,9 +162,9 @@ This spec *is* the testing plan; its own acceptance criteria are: ## Cross-references -- [wire.md](../spec/wire.md) — `decode`, `kMaxEnvelopeBytes`, the `body` +- [wire.md](../spec/core/wire.md) — `decode`, `kMaxEnvelopeBytes`, the `body` double-parse and duplicate-key caveats the fuzzers target. -- [backend.md](../spec/backend.md) — `RemoteServer::handle`/`dispatchExecute`, the +- [backend.md](../spec/core/backend.md) — `RemoteServer::handle`/`dispatchExecute`, the `switchBackend`/`cancelPending` lifecycle the soak test churns, and the transports the load benchmark drives. - [security.md](../spec/security.md) — the existing hardening tests diff --git a/docs/planned/tls_peer_verification.md b/docs/planned/tls_peer_verification.md index a72de77..891f28b 100644 --- a/docs/planned/tls_peer_verification.md +++ b/docs/planned/tls_peer_verification.md @@ -106,7 +106,7 @@ struct QtWebSocketServerConfig { The guard uses only `morph::log` (the existing replaceable sink) and the `SecureMode`/`NonSecureMode` distinction the server already tracks from its `std::optional tls` constructor argument -([backend.md](../spec/backend.md)). It adds no dependency and does not touch +([backend.md](../spec/core/backend.md)). It adds no dependency and does not touch `RemoteServer`. > If this spec lands alongside [transport_limits.md](transport_limits.md)'s @@ -167,13 +167,13 @@ The guard uses only `morph::log` (the existing replaceable sink) and the - [security.md](../spec/security.md) — the trust model, the token-replay risk this closes, the `VerifyNone` limitation, the "Use TLS and verify the peer" hardening-checklist item, and the `tests/certs/` throwaway material. -- [backend.md](../spec/backend.md) — `QtWebSocketBackend`/`QtWebSocketServer`, the +- [backend.md](../spec/core/backend.md) — `QtWebSocketBackend`/`QtWebSocketServer`, the `std::optional tls` constructor arguments, `SecureMode`, and `listen()`/`port()` where the guard lands. - [transport_limits.md](transport_limits.md) — the sibling `QtWebSocketServerConfig` connection-level knobs (`handshakeTimeout`, `maxConnections`) this config merges with; TLS handshake timeout is enforced there. -- [logger.md](../spec/logger.md) — the `LogLevel::error` sink the exposure guard +- [logger.md](../spec/core/logger.md) — the `LogLevel::error` sink the exposure guard logs through. - [non_qt_transport.md](non_qt_transport.md) — a transport that is not Qt must supply its own equivalent TLS + verification story; this spec is Qt-specific. diff --git a/docs/planned/transport_limits.md b/docs/planned/transport_limits.md index 6f14ed3..70d0380 100644 --- a/docs/planned/transport_limits.md +++ b/docs/planned/transport_limits.md @@ -3,14 +3,14 @@ > **Status: planned — not yet implemented.** This spec covers the > denial-of-service bounds `morph` deliberately delegates to the transport today, > and the design for the pieces that should ship with the Qt transport (and any -> future one). It extends [security.md](../spec/security.md) and [wire.md](../spec/wire.md). See +> future one). It extends [security.md](../spec/security.md) and [wire.md](../spec/core/wire.md). See > [todo.md](../todo.md). ## The gap `RemoteServer` is transport-agnostic and sees only decoded envelopes. The one wire-layer bound that exists is `wire::kMaxEnvelopeBytes` (8 MiB per message, -checked before parsing — see [wire.md](../spec/wire.md)). Everything else is explicitly +checked before parsing — see [wire.md](../spec/core/wire.md)). Everything else is explicitly the transport's job and, in the shipped Qt transport, **absent**: - **No per-request timeout.** A slow or stalled dispatch leaves the client's @@ -129,7 +129,7 @@ that actually owns sockets. contract). A model that can run unboundedly long must bound *itself*. - **Not a replacement for an upstream WAF / reverse proxy.** For public exposure, a hardened proxy (rate limiting, connection management, request - canonicalization for the duplicate-key caveat in [wire.md](../spec/wire.md)) is still + canonicalization for the duplicate-key caveat in [wire.md](../spec/core/wire.md)) is still recommended in front of `RemoteServer`; these knobs are defence-in-depth and the baseline for the shipped transport, not a full edge stack. - **No global (cross-connection) rate limit in the transport.** `messagesPerSecond` @@ -157,14 +157,14 @@ that actually owns sockets. - [security.md](../spec/security.md) — the hardening checklist item ("Bound message size and add timeouts in the transport") this implements; the threat model that motivates it. -- [wire.md](../spec/wire.md) — `kMaxEnvelopeBytes`, the one always-on bound, and the +- [wire.md](../spec/core/wire.md) — `kMaxEnvelopeBytes`, the one always-on bound, and the `body` double-parse and duplicate-key caveats a front proxy still handles. -- [backend.md](../spec/backend.md) — `RemoteServer` register/execute paths where the +- [backend.md](../spec/core/backend.md) — `RemoteServer` register/execute paths where the server-side `LimitPolicy` checks live; `QtWebSocketServer` where the connection-level config applies; the `handle()` reply-exactly-once contract the timeout path must preserve. - [instance_authorization.md](instance_authorization.md) — `maxLiveModels` complements `authorizeRegister`: one bounds *how many* instances the server will hold, the other bounds *who* may create them. -- [completion.md](../spec/completion.md) — the idempotent `setValue`/`setException` that +- [completion.md](../spec/core/completion.md) — the idempotent `setValue`/`setException` that makes a late or duplicate resolution harmless on the client side. diff --git a/docs/planned/validation.md b/docs/planned/validation.md index 8316673..d84d1bd 100644 --- a/docs/planned/validation.md +++ b/docs/planned/validation.md @@ -8,7 +8,7 @@ ## The gap -`ActionValidator::ready(action)` (see [registry.md](../spec/registry.md)) is the +`ActionValidator::ready(action)` (see [registry.md](../spec/core/registry.md)) is the framework's readiness/validity predicate. Today it gates only the **client**: - The reactive `set<...>` path (`BridgeHandler::tryFireImpl`, `bridge.md`) checks @@ -143,15 +143,15 @@ validator sees the same value the model will. ## Cross-references -- [registry.md](../spec/registry.md) — `ActionValidator::ready`, the `HasValidate` +- [registry.md](../spec/core/registry.md) — `ActionValidator::ready`, the `HasValidate` concept, `ActionDispatcher::registerAction`'s runner (the injection point), `BRIDGE_REGISTER_VALIDATOR`. -- [bridge.md](../spec/bridge.md) — the client-side gates this makes symmetric: +- [bridge.md](../spec/core/bridge.md) — the client-side gates this makes symmetric: `tryFireImpl` (reactive) and `ActionExecuteRegistry::execute` (request/reply), including the existing precision-reconciliation step. -- [forms.md](../spec/forms.md) — `allRequiredEngaged`, the readiness predicate typically +- [forms.md](../spec/forms/forms.md) — `allRequiredEngaged`, the readiness predicate typically used as an action's `validate()` body; `reconcileDeclaredPrecision`. -- [backend.md](../spec/backend.md) — the dispatch call sites (`RemoteServer` remote, +- [backend.md](../spec/core/backend.md) — the dispatch call sites (`RemoteServer` remote, `LocalBackend` local) and the strand `catch` that turns the throw into an `err`. - [security.md](../spec/security.md) — why validation is *not* authorization and where the two enforcement seams sit relative to each other. diff --git a/docs/spec/concurrency_and_lifetimes.md b/docs/spec/concurrency_and_lifetimes.md index 92a76fb..876e10e 100644 --- a/docs/spec/concurrency_and_lifetimes.md +++ b/docs/spec/concurrency_and_lifetimes.md @@ -227,7 +227,7 @@ posting to the pool — posting would deadlock if the pool were saturated by the in-flight action. `handleInline` **rejects `execute`**: an `execute` reply is produced asynchronously on the model strand, *after* `handleInline` has returned and destroyed the local reply buffer the deferred callback would write into — a -dangling-write hazard. See [backend.md](backend.md) for the exact wiring. +dangling-write hazard. See [backend.md](core/backend.md) for the exact wiring. ### `cancelPending` — snapshot-then-deliver, weak-ptr tracked @@ -460,21 +460,21 @@ One-liners to remember: ## Cross-references -- [`executor.md`](executor.md) — `IExecutor`, `ThreadPoolExecutor`, +- [`executor.md`](core/executor.md) — `IExecutor`, `ThreadPoolExecutor`, `StrandExecutor`, `ModelId`; the "destroy strand before base pool" rule in detail. -- [`completion.md`](completion.md) — `Completion` / `CompletionState` +- [`completion.md`](core/completion.md) — `Completion` / `CompletionState` internals and orphan-error logging. -- [`bridge.md`](bridge.md) — `Bridge`, `BridgeHandler`, `switchBackend`, +- [`bridge.md`](core/bridge.md) — `Bridge`, `BridgeHandler`, `switchBackend`, `executeVia`, the liveness token. -- [`backend.md`](backend.md) — `LocalBackend`, `RemoteServer`, +- [`backend.md`](core/backend.md) — `LocalBackend`, `RemoteServer`, `SimulatedRemoteBackend`, `cancelPending`, the `make_shared` requirement. -- [`offline.md`](offline.md) — `NetworkMonitor`, `ReconnectCoordinator`, +- [`offline.md`](offline/offline.md) — `NetworkMonitor`, `ReconnectCoordinator`, `SyncWorker` wiring and the reconnect ordering guarantee. -- [`registry.md`](registry.md) — `ActionDispatcher` / `ModelRegistryFactory` and +- [`registry.md`](core/registry.md) — `ActionDispatcher` / `ModelRegistryFactory` and the static-init registration model. -- [`logger.md`](logger.md) — the logging fast path and sink contract. -- [`session.md`](session.md) — `Context`, the thread-local, and `IAuthorizer`. +- [`logger.md`](core/logger.md) — the logging fast path and sink contract. +- [`session.md`](session/session.md) — `Context`, the thread-local, and `IAuthorizer`. - [`security.md`](security.md) — where the authoritative principal comes from and the `RemoteServer` enforcement points. - `error_handling.md` — the framework-wide error-propagation story that the diff --git a/docs/spec/backend.md b/docs/spec/core/backend.md similarity index 100% rename from docs/spec/backend.md rename to docs/spec/core/backend.md diff --git a/docs/spec/bridge.md b/docs/spec/core/bridge.md similarity index 99% rename from docs/spec/bridge.md rename to docs/spec/core/bridge.md index b1f755f..570c87b 100644 --- a/docs/spec/bridge.md +++ b/docs/spec/core/bridge.md @@ -230,7 +230,7 @@ than trusting the raw wire body: (`Quantity::declaredDecimals`), so the stored value's precision matches the schema's advertised `x-decimalPlaces` instead of whatever runtime `dp` the client sent. It is a no-op for actions with no `Quantity` members and for - actions whose type glaze cannot reflect. See [forms.md](forms.md). + actions whose type glaze cannot reflect. See [forms.md](../forms/forms.md). - **Validator enforcement.** `ActionValidator::ready(action)` is checked; if it returns `false` the executor throws `std::invalid_argument` and the completion resolves through `onError` (a proper error reply upstream) — the @@ -520,9 +520,9 @@ make teardown order-independent.) - [`backend.md`](backend.md) — `IBackend`, `LocalBackend`, `SimulatedRemoteBackend`, `registerModelWithContext`, `cancelPending`, `BackendChangedError`/`BridgeDestroyedError`, reconnect handlers. -- [`session.md`](session.md) — `session::Context` attached to every +- [`session.md`](../session/session.md) — `session::Context` attached to every `executeVia` call via the default session. -- [`security.md`](security.md) — how the session principal drives +- [`security.md`](../security.md) — how the session principal drives authorization on the execute path. - [`wire.md`](wire.md) — the `register` envelope carrying `contextKey` and the action call/result serialization used by remote backends. @@ -531,5 +531,5 @@ make teardown order-independent.) - [`registry.md`](registry.md) — `ModelTraits`, `ActionTraits`, `ActionValidator`, `Loggable`, `BRIDGE_REGISTER_ACTION`, and the server-side `ActionDispatcher` counterpart. -- [`concurrency_and_lifetimes.md`](concurrency_and_lifetimes.md) — the broader +- [`concurrency_and_lifetimes.md`](../concurrency_and_lifetimes.md) — the broader mutex-ordering and object-lifetime rules this type participates in. \ No newline at end of file diff --git a/docs/spec/completion.md b/docs/spec/core/completion.md similarity index 99% rename from docs/spec/completion.md rename to docs/spec/core/completion.md index 335575b..a9788ea 100644 --- a/docs/spec/completion.md +++ b/docs/spec/core/completion.md @@ -287,7 +287,7 @@ state; the log is emitted only when the state itself is finally destroyed with a is the executor on which every callback is posted. - [`logger.md`](logger.md) — `morph::log::logError`, the error-handling sink used by orphan detection when an error is abandoned. -- [`error_handling.md`](error_handling.md) — the framework-wide error-propagation +- [`error_handling.md`](../error_handling.md) — the framework-wide error-propagation story; the orphan-logging contract detailed in this file is summarised there alongside the executor and backend error paths. - [`bridge.md`](bridge.md) — `BridgeHandler` produces `Completion` from diff --git a/docs/spec/executor.md b/docs/spec/core/executor.md similarity index 99% rename from docs/spec/executor.md rename to docs/spec/core/executor.md index 39ff2a3..5153d3e 100644 --- a/docs/spec/executor.md +++ b/docs/spec/core/executor.md @@ -399,9 +399,9 @@ These are honest, known gaps — accepted trade-offs, not bugs: results land on the right thread. - `error_handling.md` — the framework-wide error-propagation story that the per-task logging here plugs into (currently also summarised under - *Error propagation* in `../ARCHITECTURE.md`). + *Error propagation* in `../../ARCHITECTURE.md`). - `concurrency_and_lifetimes.md` — the broader threading and teardown-ordering model; the "destroy strand before base pool" rule above is a concrete instance - of it (see also *Thread safety* in `../ARCHITECTURE.md`). + of it (see also *Thread safety* in `../../ARCHITECTURE.md`). - [`bridge.md`](bridge.md) — the bridge wires backends to a GUI executor and a strand-backed dispatcher; it is the primary consumer of these types. diff --git a/docs/spec/logger.md b/docs/spec/core/logger.md similarity index 100% rename from docs/spec/logger.md rename to docs/spec/core/logger.md diff --git a/docs/spec/registry.md b/docs/spec/core/registry.md similarity index 99% rename from docs/spec/registry.md rename to docs/spec/core/registry.md index 96b2b29..4f34921 100644 --- a/docs/spec/registry.md +++ b/docs/spec/core/registry.md @@ -332,7 +332,7 @@ ownerPrincipal)` hook on every `execute` and `deregister`. The hook **defaults to allow**, so this registry's type-keyed behaviour is unchanged unless a deployer installs an authorizer that overrides it. The type registry maps type ids only; instance ownership lives one layer up in `RemoteServer`. See -[session.md](session.md) and [security.md](security.md). +[session.md](../session/session.md) and [security.md](../security.md). ### `ActionExecuteRegistry` @@ -598,14 +598,14 @@ testing obligation, not a compile-time guarantee. for any TU using `BRIDGE_REGISTER_ACTION` (`registerActionExecutorOnce` is only *defined* there) and the parallel executor path this spec's `ActionExecuteRegistry` section summarises. -- **[journal.md](journal.md)** — `IActionLog`, `LogEntry`, `SessionLog`, +- **[journal.md](../journal/journal.md)** — `IActionLog`, `LogEntry`, `SessionLog`, checkpoint coalescing, and `ScopedActionLog`. Explains how the runner's `recordIfAttached` call and `ActionLogPolicy::coalesce` feed the durable log, and provides the scoped-install pattern the registries lack. - **[backend.md](backend.md)** — backends store `IModelHolder`s in a single map and drive `IBackendChangedSink` / `BackendChangedMixin`; the model instances created by `ModelRegistryFactory` land here. -- **[security.md](security.md)** — the `session::current()` principal stamped onto +- **[security.md](../security.md)** — the `session::current()` principal stamped onto every logged entry by `recordIfAttached`, and the trust boundary of the string-keyed remote dispatch surface. - **Error handling** — the `detail::ParseError` / `std::runtime_error` taxonomy diff --git a/docs/spec/wire.md b/docs/spec/core/wire.md similarity index 100% rename from docs/spec/wire.md rename to docs/spec/core/wire.md diff --git a/docs/spec/error_handling.md b/docs/spec/error_handling.md index cce50d7..98a2564 100644 --- a/docs/spec/error_handling.md +++ b/docs/spec/error_handling.md @@ -313,15 +313,15 @@ accordingly. ## Cross-references -- [`completion.md`](completion.md) — `Completion` / `CompletionState`, orphan logging, single-shot callbacks. -- [`executor.md`](executor.md) — `IExecutor`, `ThreadPoolExecutor`, `MainThreadExecutor`, and the strand. -- [`backend.md`](backend.md) — `IBackend`, `LocalBackend`, `cancelPending`, the three backend error types. -- [`wire.md`](wire.md) — the `Envelope` protocol and `encode`/`decode`. -- [`registry.md`](registry.md) — `ActionDispatcher`, `ModelRegistryFactory`, `ParseError`, the codec macros. -- [`journal.md`](journal.md) — `LogEntry`, `SerializationError`, `replay`, `SessionLog`. -- [`rational.md`](rational.md) — `Rational`, `RationalError`, the clamping wire codec. -- [`quantity_type.md`](quantity_type.md) — `Quantity`, empty-propagation arithmetic, the ordering `logic_error`. -- [`datetime.md`](datetime.md) — `DateTime` and its strict, rejecting ISO-8601 codec. -- [`session.md`](session.md) / [`security.md`](security.md) — `Context`, `IAuthorizer`, `AuthError`, `SigningAuthorizer`. -- [`logger.md`](logger.md) — `morph::log`, `setLogger`, log levels. +- [`completion.md`](core/completion.md) — `Completion` / `CompletionState`, orphan logging, single-shot callbacks. +- [`executor.md`](core/executor.md) — `IExecutor`, `ThreadPoolExecutor`, `MainThreadExecutor`, and the strand. +- [`backend.md`](core/backend.md) — `IBackend`, `LocalBackend`, `cancelPending`, the three backend error types. +- [`wire.md`](core/wire.md) — the `Envelope` protocol and `encode`/`decode`. +- [`registry.md`](core/registry.md) — `ActionDispatcher`, `ModelRegistryFactory`, `ParseError`, the codec macros. +- [`journal.md`](journal/journal.md) — `LogEntry`, `SerializationError`, `replay`, `SessionLog`. +- [`rational.md`](util/rational.md) — `Rational`, `RationalError`, the clamping wire codec. +- [`quantity_type.md`](util/quantity_type.md) — `Quantity`, empty-propagation arithmetic, the ordering `logic_error`. +- [`datetime.md`](util/datetime.md) — `DateTime` and its strict, rejecting ISO-8601 codec. +- [`session.md`](session/session.md) / [`security.md`](security.md) — `Context`, `IAuthorizer`, `AuthError`, `SigningAuthorizer`. +- [`logger.md`](core/logger.md) — `morph::log`, `setLogger`, log levels. - [`ARCHITECTURE.md`](../ARCHITECTURE.md#error-propagation) — the "Error propagation" overview this spec expands. diff --git a/docs/spec/choice.md b/docs/spec/forms/choice.md similarity index 80% rename from docs/spec/choice.md rename to docs/spec/forms/choice.md index 6bf8292..db7a9e0 100644 --- a/docs/spec/choice.md +++ b/docs/spec/forms/choice.md @@ -13,8 +13,6 @@ which action to call and which result fields to use without hardcoding anything. ## Contents -- [FixedString — NTTP string](#fixedstring--an-nttp-compile-time-string) -- [FixedString notes](#fixedstring-notes) - [Choice — structure](#choice--structure) - [Empty state](#empty-state) - [Wire and schema](#wire-and-schema) @@ -26,59 +24,7 @@ which action to call and which result fields to use without hardcoding anything. - [Failure modes](#failure-modes) - [Limitations](#limitations) - [Cross-references](#cross-references) - -## FixedString — an NTTP compile-time string - -`morph::forms::FixedString` is a structural type that can appear as a -non-type template parameter. It stores `N` characters (including the terminating -null) in a `std::array` and is constructed `consteval` from a string -literal. - -It is an **alias** for the single shared definition `morph::detail::FixedString` -(`include/morph/detail/fixed_string.hpp`). The units layer's `NamedQuantity` -(see [quantity_type.md](quantity_type.md)) uses the *same* underlying type via -its own `morph::units::detail::FixedString` alias, so there is exactly one -`FixedString` definition in the codebase, not two look-alikes. `morph::forms::` -and `morph::units::detail::` are kept as names for source compatibility and -layer-local readability. - -| Member | Signature | Notes | -|---|---|---| -| `data` | `std::array data{}` | Null-terminated storage. | -| ctor | `consteval FixedString(const char (&literal)[N]) noexcept` | From a string literal of the same length. | -| `view()` | `[[nodiscard]] constexpr std::string_view view() const noexcept` | Excludes the null terminator — length is `N - 1`. | - -The `consteval` constructor guarantees that `FixedString` is only ever -initialised from a compile-time literal, so every template instantiation is -visible to the compiler at the point of use. - -## FixedString notes - -A few subtleties matter because `FixedString` is the vehicle that carries the -options metadata into the type system: - -- **`N` counts the terminating null.** The literal `"id"` is `const char[3]`, - so `FixedString<3>`; `view()` returns `{data.data(), N - 1}` — a two-char - view `"id"` that stops before the null. The null is stored but never part of - the string. -- **An empty literal yields a zero-length name.** `""` is `const char[1]`, so - `FixedString<1>` and `view()` is a length-0 `string_view`. Nothing rejects - this — an empty action name or field name compiles and simply produces an - empty `x-option*` annotation that no client can act on. -- **Type identity depends on structural NTTP equality.** `FixedString` is a - *structural type* (only public, non-mutable data members: the `std::array`), - so it is usable as a non-type template parameter and two `FixedString` - values compare member-wise. Two separate `"id"` literals therefore produce - the *same* `FixedString<3>` value, so `Choice` written - in two translation units is one and the same type. This is what makes - `Choice` type identity stable across the codebase and what lets `glz::meta` - and `isChoice` match on the instantiation. -- **`FixedString` is public only as an NTTP vehicle.** It exists so a string - literal can travel as a template parameter; it is not a general-purpose - string type and is not intended for direct use in application code. The - only supported way to produce one is a string literal in a `Choice` - template-argument position. - + ## Choice — structure ```cpp @@ -293,13 +239,13 @@ and nothing more. That leaves gaps neither the client nor the server closes: `EmptyCapableField` concept plus the not-`std::optional`/not-`optionalFields` rule), and where `mergeSchemaExtras` emits the `x-optionsAction` / `x-optionValue` / `x-optionLabel` property annotations. -- **[quantity_type.md](quantity_type.md)** and **[datetime.md](datetime.md)** — +- **[quantity_type.md](../util/quantity_type.md)** and **[datetime.md](../util/datetime.md)** — `Quantity` and `Timestamp` share the *one kind of empty* pattern: the blank state lives inside the value as `std::optional`, `hasValue()` reports engagement, and a non-optional member is required. `Choice` is the third member of that family. -- **[security.md](security.md)** — the options metadata and any +- **[security.md](../security.md)** — the options metadata and any membership expectation are enforced (if at all) only on the client; the server sees a bare nullable value. This is the client-only-validation trust boundary — never trust a submitted `Choice` value to be a current, valid - option without server-side re-checking. \ No newline at end of file + option without server-side re-checking. diff --git a/docs/spec/forms.md b/docs/spec/forms/forms.md similarity index 96% rename from docs/spec/forms.md rename to docs/spec/forms/forms.md index 9b88a8a..592697c 100644 --- a/docs/spec/forms.md +++ b/docs/spec/forms/forms.md @@ -352,7 +352,7 @@ handler: - **`BridgeHandler::executeJson` → `ActionExecuteRegistry`** (the local / client-side path a schema-driven GUI uses). This path **now enforces** `ActionValidator::ready` after decoding and before invoking the handler - (see [bridge.md](bridge.md)): an action that fails `validate()` is rejected with + (see [bridge.md](../core/bridge.md)): an action that fails `validate()` is rejected with an error, never executed. It also retags `Quantity` fields to their declared precision (below). So on this path the schema's `required` array and the handler agree by construction. @@ -368,7 +368,7 @@ quantities inside the handler**. The `examples/forms` model does exactly this its `execute(RecordMeasurement)` calls `action.validate()` (the same `allRequiredEngaged` predicate) and throws `std::invalid_argument` if it fails, so schema, form, and server agree regardless of which dispatch path was used. See -[security.md](security.md) for the wire dispatcher's validation stance. +[security.md](../security.md) for the wire dispatcher's validation stance. ### Advertised precision is enforced on dispatch @@ -422,11 +422,11 @@ wrong or un-merged schema rather than fail loudly. | Spec | Why | |---|---| -| [choice.md](choice.md) | Full `Choice` / `FixedString` API and design (this spec cross-refs rather than duplicates them). | -| [quantity_type.md](quantity_type.md) | `Quantity`, its unit tags, `UnitTraits::relations`, and `convert` — the source of `x-decimalPlaces`, `x-unitAlternatives`, and `ExtUnits`. | -| [datetime.md](datetime.md) | `DateTime` / `Timestamp`, the ISO-8601 wire format, and the `"format": "date-time"` schema annotation. | -| [rational.md](rational.md) | Exact `Rational` values; the `num`/`den` in each `x-unitAlternatives` entry are a `Rational` numerator/denominator, which is why unit switches recompute exactly. | -| [security.md](security.md) | The dispatcher's trust boundary — why `required` gates only the client and handlers must re-validate. | +| [choice.md](choice.md) | Full `Choice` API and design (this spec cross-refs rather than duplicates it). | +| [quantity_type.md](../util/quantity_type.md) | `Quantity`, its unit tags, `UnitTraits::relations`, and `convert` — the source of `x-decimalPlaces`, `x-unitAlternatives`, and `ExtUnits`. | +| [datetime.md](../util/datetime.md) | `DateTime` / `Timestamp`, the ISO-8601 wire format, and the `"format": "date-time"` schema annotation. | +| [rational.md](../util/rational.md) | Exact `Rational` values; the `num`/`den` in each `x-unitAlternatives` entry are a `Rational` numerator/denominator, which is why unit switches recompute exactly. | +| [security.md](../security.md) | The dispatcher's trust boundary — why `required` gates only the client and handlers must re-validate. | ## Out of scope diff --git a/docs/spec/journal.md b/docs/spec/journal/journal.md similarity index 100% rename from docs/spec/journal.md rename to docs/spec/journal/journal.md diff --git a/docs/spec/offline.md b/docs/spec/offline/offline.md similarity index 99% rename from docs/spec/offline.md rename to docs/spec/offline/offline.md index ae30048..d5720e8 100644 --- a/docs/spec/offline.md +++ b/docs/spec/offline/offline.md @@ -303,7 +303,7 @@ of its own state. Because the drain is posted (asynchronous), it completes some time *after* `switchBackend` returns; a test or host that must observe the drained result waits for it (the conflict-resolution tests poll a model counter) rather than assuming it finished synchronously. See -[bridge.md](bridge.md)'s `switchBackend` for the exact posting mechanism. +[bridge.md](../core/bridge.md)'s `switchBackend` for the exact posting mechanism. The two paths are mutually exclusive per queue — a queue drained inside `onBackendChanged()` and also handed to a `SyncWorker::run()` would be diff --git a/docs/spec/security.md b/docs/spec/security.md index 560fbd1..4197afb 100644 --- a/docs/spec/security.md +++ b/docs/spec/security.md @@ -6,9 +6,9 @@ session layer (`session.hpp` `Context`/`IAuthorizer`, `session_auth.hpp` `RemoteServer` enforcement points in `remote.hpp`). Read this before deploying a `RemoteServer` on anything but a trusted local socket. -Related specs: [session.md](session.md) (the `Context`/`IAuthorizer` types), -[wire.md](wire.md) (the envelope the `session` travels in), -[backend.md](backend.md) (`RemoteServer`/`LocalBackend` dispatch), +Related specs: [session.md](session/session.md) (the `Context`/`IAuthorizer` types), +[wire.md](core/wire.md) (the envelope the `session` travels in), +[backend.md](core/backend.md) (`RemoteServer`/`LocalBackend` dispatch), [error_handling.md](error_handling.md) (how a rejected request surfaces). The shipped `morph::qt` WebSocket transport supplies the TLS layer discussed below. @@ -29,19 +29,19 @@ application. Be explicit about the boundary: defined outcome rather than undefined behaviour. - A wire-layer message-size cap: `wire::decode` rejects any envelope larger than `wire::kMaxEnvelopeBytes` (8 MiB) before parsing, bounding a single message's - allocation and parse cost (see [wire.md](wire.md)). This is a coarse + allocation and parse cost (see [wire.md](core/wire.md)). This is a coarse per-message backstop only — not a rate limit, timeout, or inner-`body` bound. **morph does NOT provide (the application/transport must):** - **Wire-layer transport security.** The `wire` envelope carries no encryption - and no per-request timeout (see [wire.md](wire.md)). `RemoteServer` itself is + and no per-request timeout (see [wire.md](core/wire.md)). `RemoteServer` itself is transport-agnostic and sees only decoded envelopes; confidentiality and timeouts are the transport's job. The shipped Qt transport *does* offer TLS (see below), but a transport that does not is plaintext. Run `RemoteServer` behind a transport that provides TLS. There **is** now one wire-layer bound: `wire::decode` rejects any envelope whose serialized form exceeds `wire::kMaxEnvelopeBytes` (8 MiB) before parsing - (see [wire.md](wire.md) — "Parsing guarantees and hardening"). This caps the + (see [wire.md](core/wire.md) — "Parsing guarantees and hardening"). This caps the peak allocation and parse cost of a single message — including deeply-nested JSON smuggled inside the opaque `body` string, which the double-parse would otherwise only surface on the inner re-parse. It is a coarse per-message @@ -54,7 +54,7 @@ application. Be explicit about the boundary: Without transport-level TLS a stolen token can be replayed. - **Type-level `register` authorization.** The `register` envelope is **not** passed through `authorize`, and model ids are guessable sequential integers - assigned from a single counter (see [backend.md](backend.md), + assigned from a single counter (see [backend.md](core/backend.md), `RemoteServer::_nextId`). Any client that can send envelopes can register models. **Per-instance ownership on `execute`/`deregister` *is* now enforceable** via the optional `IAuthorizer::authorizeInstance` hook (see @@ -380,7 +380,7 @@ responsibility: - **Bound message size and add timeouts in the transport.** The wire layer now caps a single message at `wire::kMaxEnvelopeBytes` (8 MiB) but imposes **no** per-request timeout and **no** per-connection rate or count limit (see - [wire.md](wire.md)); `QtWebSocketServer` adds no cap beyond `QWebSocket`'s + [wire.md](core/wire.md)); `QtWebSocketServer` adds no cap beyond `QWebSocket`'s default. A hostile client can still exhaust memory with many sub-cap messages or leave `Completion`s pending forever, so the transport must add its own size/rate bounds and timeouts. diff --git a/docs/spec/session.md b/docs/spec/session/session.md similarity index 94% rename from docs/spec/session.md rename to docs/spec/session/session.md index fc6b3f2..3fd6d29 100644 --- a/docs/spec/session.md +++ b/docs/spec/session/session.md @@ -7,7 +7,7 @@ caller. The authentication *mechanism* — signed bearer tokens, `SigningAuthorizer`, and the `RemoteServer` enforcement points — lives in -[security.md](security.md), the primary companion to this spec. This file +[security.md](../security.md), the primary companion to this spec. This file documents the `session` **types and their contracts**; it cross-references security.md for the full trust model rather than duplicating it. @@ -42,7 +42,7 @@ verbatim from caller to model. | `metadata` | `std::unordered_map` | Free-form bag for feature flags, A/B buckets, app-specific metadata. | On remote backends the entire `Context` — including `token` — is serialised into -the wire envelope's `session` field (see [wire.md](wire.md)) so `RemoteServer` +the wire envelope's `session` field (see [wire.md](../core/wire.md)) so `RemoteServer` sees the same values the GUI sent. On the local backend it travels in-memory via `ActionCall`. @@ -50,7 +50,7 @@ sees the same values the GUI sent. On the local backend it travels in-memory via server *derives* from that credential. See [the `authenticate` hook](#the-authenticate-hook--the-authoritative-principal) for how the derived principal replaces the client's claim, and -[security.md](security.md) for the token format and login flow. +[security.md](../security.md) for the token format and login flow. ## How a `Context` originates and flows @@ -75,7 +75,7 @@ From the `ActionCall` the session travels backend-specifically: dispatch. The bridge plumbing (`setDefaultSession`/`defaultSession`, the `ActionCall` -stamp) is specified in [bridge.md](bridge.md); this spec covers only the +stamp) is specified in [bridge.md](../core/bridge.md); this spec covers only the `Context` payload and its server-side handling. ## IAuthorizer — gate for action dispatch @@ -127,7 +127,7 @@ calls `authenticate` **after `authorize` has already succeeded**: time-of-check/time-of-use gap (a token can pass `authorize` and then expire before `authenticate`, whose `nullopt` would otherwise let the client's claim survive) and the analogous authorize-only passthrough. See - [security.md](security.md). + [security.md](../security.md). This split keeps the two concerns separate: `authorize` answers "is this call permitted?" and `authenticate` answers "who is actually making it?". An @@ -144,7 +144,7 @@ principal regardless. The full mechanism — token format, verification order, `RemoteServer`'s `dispatchExecute` call site, and the login flow — is documented in -[security.md](security.md). This spec fixes the *contract*: `authenticate` +[security.md](../security.md). This spec fixes the *contract*: `authenticate` is consulted post-`authorize`, its return replaces the principal when present, and its `nullopt` clears the principal. @@ -175,7 +175,7 @@ The **default returns `true`**, so `AllowAllAuthorizer` and a plain `SigningAuthorizer` impose no per-instance restriction — behaviour is unchanged unless an authorizer overrides the hook (typically `ownerPrincipal.empty() || ownerPrincipal == ctx.principal`). The full mechanism, the register-time owner -recording, and the trust model are in [security.md](security.md) +recording, and the trust model are in [security.md](../security.md) ("The per-instance ownership hook"). This spec fixes the *contract*: `authorizeInstance` is consulted per `execute` and per `deregister`, defaults to allow, and receives the instance id plus its recorded owner. @@ -192,7 +192,7 @@ instance when the default is sufficient. `AllowAllAuthorizer` is **fail-open**: it is convenient for local and simulated development and wrong for production. See [Trust boundary](#trust-boundary) and -[security.md](security.md) ("The default is fail-open") for why an exposed +[security.md](../security.md) ("The default is fail-open") for why an exposed server must replace it. ## Trust boundary @@ -210,7 +210,7 @@ input. The rules that govern where trust begins: - **The local backend does not authorize at all.** `authorize` is a **remote-only gate**: `LocalBackend::execute` installs the session context but never consults the authorizer (its sole call site is `RemoteServer`'s - `dispatchExecute`, see [backend.md](backend.md)). Any security-critical check + `dispatchExecute`, see [backend.md](../core/backend.md)). Any security-critical check must therefore be enforced inside the model so it holds in both local and remote modes. - **The `RemoteServer` default authorizer is fail-open.** An unconfigured server @@ -222,8 +222,8 @@ input. The rules that govern where trust begins: defaults to allow) — or, for logic the framework cannot know, in the model. The registry that maps those type ids to runners is described in -[registry.md](registry.md); the enforcement points and the full threat model are -in [security.md](security.md). +[registry.md](../core/registry.md); the enforcement points and the full threat model are +in [security.md](../security.md). ## Thread safety — `current()` and `ScopedContext` @@ -347,18 +347,18 @@ if (const auto* ctx = morph::session::current(); ctx != nullptr) { - **`current()` is dispatch-thread-only.** It returns `nullptr` off the dispatch thread; session data needed across a thread boundary must be captured first. -See [security.md](security.md) for the complete threat model and hardening +See [security.md](../security.md) for the complete threat model and hardening checklist (TLS, message-size bounds, control-message authorization, secret rotation). ## Cross-references -- [security.md](security.md) — **the authentication subsystem**: signed bearer +- [security.md](../security.md) — **the authentication subsystem**: signed bearer tokens, `SigningAuthorizer`, the `RemoteServer` enforcement points, the threat model, and hardening guidance. The primary companion to this spec. -- [wire.md](wire.md) — the envelope the `Context` (including `token`) is +- [wire.md](../core/wire.md) — the envelope the `Context` (including `token`) is serialised into on the remote path. -- [backend.md](backend.md) — `RemoteServer` / `LocalBackend` dispatch, and where +- [backend.md](../core/backend.md) — `RemoteServer` / `LocalBackend` dispatch, and where the authorizer is (and is not) consulted. -- [registry.md](registry.md) — the model/action type registry behind the type +- [registry.md](../core/registry.md) — the model/action type registry behind the type ids `authorize` receives. diff --git a/docs/spec/datetime.md b/docs/spec/util/datetime.md similarity index 100% rename from docs/spec/datetime.md rename to docs/spec/util/datetime.md diff --git a/docs/spec/quantity_type.md b/docs/spec/util/quantity_type.md similarity index 99% rename from docs/spec/quantity_type.md rename to docs/spec/util/quantity_type.md index 40a0352..7fd44bc 100644 --- a/docs/spec/quantity_type.md +++ b/docs/spec/util/quantity_type.md @@ -458,7 +458,7 @@ argument (a structural fixed-capacity buffer built from a string literal), which is what lets the name live in the type. `morph::units::detail::FixedString` is an alias for the single shared `morph::detail::FixedString` (`include/morph/detail/fixed_string.hpp`) — the *same* type the forms layer -exposes as `morph::forms::FixedString` for `Choice` (see [choice.md](choice.md)); +exposes as `morph::forms::FixedString` for `Choice` (see [choice.md](../forms/choice.md)); there is one definition, not two. `NamedQuantity` publicly derives from `Quantity` (declared-precision default) and offers four ways in — a **default constructor** (empty, then named), an **`optional` constructor**, a @@ -990,12 +990,12 @@ the placeholder exists precisely to avoid writing shared work twice. over the canonical `num/den` at the runtime `DecimalPlaces`; it does **not** route through `Rational`'s own `std::formatter` or `toDouble`. This spec does not restate the `Rational` arithmetic rules. -- **[`forms.md`](forms.md)** — how a `Quantity` field reaches a generated form: +- **[`forms.md`](../forms/forms.md)** — how a `Quantity` field reaches a generated form: `ExtUnits` (unit id/display, emitted by `to_json_schema`) plus the schema-merge keys `x-decimalPlaces` (from `declaredDecimals`) and `x-unitAlternatives` (from `unitAlternatives()`), added per member by `morph::forms`' `mergeSchemaExtras`. -- **[`choice.md`](choice.md)** and **[`datetime.md`](datetime.md)** — +- **[`choice.md`](../forms/choice.md)** and **[`datetime.md`](datetime.md)** — one-kind-of-empty siblings. `Choice` and `Timestamp` share `Quantity`'s `std::optional`-backed single empty state and total `==` (empty == empty). **The family is deliberately *not* uniform on ordering — do not assume one diff --git a/docs/spec/rational.md b/docs/spec/util/rational.md similarity index 97% rename from docs/spec/rational.md rename to docs/spec/util/rational.md index 7401a79..0fdf29e 100644 --- a/docs/spec/rational.md +++ b/docs/spec/util/rational.md @@ -343,8 +343,8 @@ expected operator+(Left const&, Right const&) noexcept; | Spec | Relationship | |---|---| | [`quantity_type.md`](quantity_type.md) | `Rational` is the **runtime substrate** for `Quantity`. A `Quantity`'s declared precision and the forms layer's `x-decimalPlaces` schema annotation both resolve, at runtime, to a `Rational`'s `DecimalPlaces` tag — the `dp` value carried on the wire and propagated through arithmetic here is exactly the precision a `Quantity` declares. The overflow envelope and `INT64_MIN` hazards documented above therefore bound `Quantity` too. | -| [`forms.md`](forms.md) | The form generator reads `x-decimalPlaces` (and the `Rational` wire shape `{"num","den","dp"}`) to build precision-aware numeric inputs; a form value is a `Rational` under the hood, so its display uses `toDouble`/formatting and its exact value uses the wire codec. | -| [`security.md`](security.md) | `setWire` performs the untrusted-wire **clamping** (`den == 0 → 1`, out-of-range `dp` → `[1, 18]`, `INT64_MIN` → `-INT64_MAX`). This is the boundary defence that keeps a hostile payload from reaching the UB-prone negation/overflow sites; see the clamping semantics discussion there. | +| [`forms.md`](../forms/forms.md) | The form generator reads `x-decimalPlaces` (and the `Rational` wire shape `{"num","den","dp"}`) to build precision-aware numeric inputs; a form value is a `Rational` under the hood, so its display uses `toDouble`/formatting and its exact value uses the wire codec. | +| [`security.md`](../security.md) | `setWire` performs the untrusted-wire **clamping** (`den == 0 → 1`, out-of-range `dp` → `[1, 18]`, `INT64_MIN` → `-INT64_MAX`). This is the boundary defence that keeps a hostile payload from reaching the UB-prone negation/overflow sites; see the clamping semantics discussion there. | | [`datetime.md`](datetime.md) | Contrast case for wire-decode policy: the `DateTime` codec is **strict** (rejects malformed input) whereas `Rational::setWire` is **lenient/clamping** (silently repairs it). See [Limitations](#limitations) for why the difference matters. | ## Limitations diff --git a/examples/bank/gui/BankClient.cpp b/examples/bank/gui/BankClient.cpp index 3ae2b06..d914eda 100644 --- a/examples/bank/gui/BankClient.cpp +++ b/examples/bank/gui/BankClient.cpp @@ -2,8 +2,8 @@ #include "BankClient.hpp" -#include -#include +#include +#include #include #include diff --git a/examples/bank/gui/BankClient.hpp b/examples/bank/gui/BankClient.hpp index 5e6232d..d625151 100644 --- a/examples/bank/gui/BankClient.hpp +++ b/examples/bank/gui/BankClient.hpp @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#include -#include +#include +#include #include #include diff --git a/examples/bank/gui/controllers/AccountController.hpp b/examples/bank/gui/controllers/AccountController.hpp index 3ebb1b5..d71345b 100644 --- a/examples/bank/gui/controllers/AccountController.hpp +++ b/examples/bank/gui/controllers/AccountController.hpp @@ -7,7 +7,7 @@ #include "BankController.hpp" #ifndef Q_MOC_RUN -#include +#include #include "bank/models/account_model.hpp" #endif diff --git a/examples/bank/gui/controllers/AppController.hpp b/examples/bank/gui/controllers/AppController.hpp index cd9a40b..d7b7dcf 100644 --- a/examples/bank/gui/controllers/AppController.hpp +++ b/examples/bank/gui/controllers/AppController.hpp @@ -8,7 +8,7 @@ // Hidden from moc: moc follows includes and its parser trips on the heavy // morph/Lightweight headers. The compiler still sees them. #ifndef Q_MOC_RUN -#include +#include #include "bank/models/auth_model.hpp" #endif diff --git a/examples/bank/gui/controllers/CardController.hpp b/examples/bank/gui/controllers/CardController.hpp index cfe7620..fe0b34e 100644 --- a/examples/bank/gui/controllers/CardController.hpp +++ b/examples/bank/gui/controllers/CardController.hpp @@ -8,7 +8,7 @@ #include "BankController.hpp" #ifndef Q_MOC_RUN -#include +#include #include "bank/models/account_model.hpp" #include "bank/models/card_model.hpp" diff --git a/examples/bank/gui/controllers/LoanController.hpp b/examples/bank/gui/controllers/LoanController.hpp index 6ab893a..0187860 100644 --- a/examples/bank/gui/controllers/LoanController.hpp +++ b/examples/bank/gui/controllers/LoanController.hpp @@ -8,7 +8,7 @@ #include "BankController.hpp" #ifndef Q_MOC_RUN -#include +#include #include "bank/models/account_model.hpp" #include "bank/models/loan_model.hpp" diff --git a/examples/bank/gui/controllers/PayeeController.hpp b/examples/bank/gui/controllers/PayeeController.hpp index 051919d..d1b49ab 100644 --- a/examples/bank/gui/controllers/PayeeController.hpp +++ b/examples/bank/gui/controllers/PayeeController.hpp @@ -8,7 +8,7 @@ #include "BankController.hpp" #ifndef Q_MOC_RUN -#include +#include #include "bank/models/account_model.hpp" #include "bank/models/payee_model.hpp" diff --git a/examples/bank/gui/controllers/TransactionController.hpp b/examples/bank/gui/controllers/TransactionController.hpp index 156b27a..a4b72f0 100644 --- a/examples/bank/gui/controllers/TransactionController.hpp +++ b/examples/bank/gui/controllers/TransactionController.hpp @@ -8,7 +8,7 @@ #include "BankController.hpp" #ifndef Q_MOC_RUN -#include +#include #include "bank/models/account_model.hpp" #include "bank/models/transaction_model.hpp" diff --git a/examples/bank/gui_wasm/include/bank/models/account_model.hpp b/examples/bank/gui_wasm/include/bank/models/account_model.hpp index 9874753..eb30441 100644 --- a/examples/bank/gui_wasm/include/bank/models/account_model.hpp +++ b/examples/bank/gui_wasm/include/bank/models/account_model.hpp @@ -3,8 +3,8 @@ // WASM shadow of include/bank/models/account_model.hpp (in-memory backend). -#include -#include +#include +#include #include "bank/dto/account_dto.hpp" diff --git a/examples/bank/gui_wasm/include/bank/models/auth_model.hpp b/examples/bank/gui_wasm/include/bank/models/auth_model.hpp index 3f565c8..6b08720 100644 --- a/examples/bank/gui_wasm/include/bank/models/auth_model.hpp +++ b/examples/bank/gui_wasm/include/bank/models/auth_model.hpp @@ -6,8 +6,8 @@ // dependency (no db_model.hpp). Persistence is the in-memory store. This header // is placed first on the WASM include path so it wins over the native one. -#include -#include +#include +#include #include "bank/dto/auth_dto.hpp" #include "bank/dto/common.hpp" diff --git a/examples/bank/gui_wasm/include/bank/models/card_model.hpp b/examples/bank/gui_wasm/include/bank/models/card_model.hpp index 8068921..f39a803 100644 --- a/examples/bank/gui_wasm/include/bank/models/card_model.hpp +++ b/examples/bank/gui_wasm/include/bank/models/card_model.hpp @@ -3,8 +3,8 @@ // WASM shadow of include/bank/models/card_model.hpp (in-memory backend). -#include -#include +#include +#include #include "bank/dto/card_dto.hpp" #include "bank/dto/common.hpp" diff --git a/examples/bank/gui_wasm/include/bank/models/loan_model.hpp b/examples/bank/gui_wasm/include/bank/models/loan_model.hpp index 66c0c82..3a2fe18 100644 --- a/examples/bank/gui_wasm/include/bank/models/loan_model.hpp +++ b/examples/bank/gui_wasm/include/bank/models/loan_model.hpp @@ -3,8 +3,8 @@ // WASM shadow of include/bank/models/loan_model.hpp (in-memory backend). -#include -#include +#include +#include #include "bank/dto/loan_dto.hpp" diff --git a/examples/bank/gui_wasm/include/bank/models/payee_model.hpp b/examples/bank/gui_wasm/include/bank/models/payee_model.hpp index ff690d3..efb2ec2 100644 --- a/examples/bank/gui_wasm/include/bank/models/payee_model.hpp +++ b/examples/bank/gui_wasm/include/bank/models/payee_model.hpp @@ -3,8 +3,8 @@ // WASM shadow of include/bank/models/payee_model.hpp (in-memory backend). -#include -#include +#include +#include #include "bank/dto/common.hpp" #include "bank/dto/payee_dto.hpp" diff --git a/examples/bank/gui_wasm/include/bank/models/payment_model.hpp b/examples/bank/gui_wasm/include/bank/models/payment_model.hpp index 717e823..4e27934 100644 --- a/examples/bank/gui_wasm/include/bank/models/payment_model.hpp +++ b/examples/bank/gui_wasm/include/bank/models/payment_model.hpp @@ -3,8 +3,8 @@ // WASM shadow of include/bank/models/payment_model.hpp (in-memory backend). -#include -#include +#include +#include #include "bank/dto/common.hpp" #include "bank/dto/payment_dto.hpp" diff --git a/examples/bank/gui_wasm/include/bank/models/transaction_model.hpp b/examples/bank/gui_wasm/include/bank/models/transaction_model.hpp index 4f0a9c9..b7127f3 100644 --- a/examples/bank/gui_wasm/include/bank/models/transaction_model.hpp +++ b/examples/bank/gui_wasm/include/bank/models/transaction_model.hpp @@ -3,8 +3,8 @@ // WASM shadow of include/bank/models/transaction_model.hpp (in-memory backend). -#include -#include +#include +#include #include "bank/dto/transaction_dto.hpp" diff --git a/examples/bank/include/bank/app/app.hpp b/examples/bank/include/bank/app/app.hpp index 8590901..ea2eb31 100644 --- a/examples/bank/include/bank/app/app.hpp +++ b/examples/bank/include/bank/app/app.hpp @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#include -#include +#include +#include #include #include diff --git a/examples/bank/include/bank/core/principal.hpp b/examples/bank/include/bank/core/principal.hpp index 2d8a0f3..d071a72 100644 --- a/examples/bank/include/bank/core/principal.hpp +++ b/examples/bank/include/bank/core/principal.hpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#include +#include #include diff --git a/examples/bank/include/bank/models/account_model.hpp b/examples/bank/include/bank/models/account_model.hpp index 57b3a70..881cff4 100644 --- a/examples/bank/include/bank/models/account_model.hpp +++ b/examples/bank/include/bank/models/account_model.hpp @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#include -#include +#include +#include #include "bank/db/db_model.hpp" #include "bank/dto/account_dto.hpp" diff --git a/examples/bank/include/bank/models/auth_model.hpp b/examples/bank/include/bank/models/auth_model.hpp index 43c9571..0147822 100644 --- a/examples/bank/include/bank/models/auth_model.hpp +++ b/examples/bank/include/bank/models/auth_model.hpp @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#include -#include +#include +#include #include "bank/db/db_model.hpp" #include "bank/dto/auth_dto.hpp" diff --git a/examples/bank/include/bank/models/budget_model.hpp b/examples/bank/include/bank/models/budget_model.hpp index f02998c..45c8051 100644 --- a/examples/bank/include/bank/models/budget_model.hpp +++ b/examples/bank/include/bank/models/budget_model.hpp @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#include -#include +#include +#include #include "bank/db/db_model.hpp" #include "bank/dto/budget_dto.hpp" diff --git a/examples/bank/include/bank/models/card_model.hpp b/examples/bank/include/bank/models/card_model.hpp index 3adfa33..f9eee0b 100644 --- a/examples/bank/include/bank/models/card_model.hpp +++ b/examples/bank/include/bank/models/card_model.hpp @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#include -#include +#include +#include #include "bank/db/db_model.hpp" #include "bank/dto/card_dto.hpp" diff --git a/examples/bank/include/bank/models/loan_model.hpp b/examples/bank/include/bank/models/loan_model.hpp index 7dfec33..cd82aad 100644 --- a/examples/bank/include/bank/models/loan_model.hpp +++ b/examples/bank/include/bank/models/loan_model.hpp @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#include -#include +#include +#include #include "bank/db/db_model.hpp" #include "bank/dto/loan_dto.hpp" diff --git a/examples/bank/include/bank/models/notification_model.hpp b/examples/bank/include/bank/models/notification_model.hpp index 6f71f46..57f3211 100644 --- a/examples/bank/include/bank/models/notification_model.hpp +++ b/examples/bank/include/bank/models/notification_model.hpp @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#include -#include +#include +#include #include "bank/db/db_model.hpp" #include "bank/dto/common.hpp" diff --git a/examples/bank/include/bank/models/payee_model.hpp b/examples/bank/include/bank/models/payee_model.hpp index e8feedd..1885a8b 100644 --- a/examples/bank/include/bank/models/payee_model.hpp +++ b/examples/bank/include/bank/models/payee_model.hpp @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#include -#include +#include +#include #include "bank/db/db_model.hpp" #include "bank/dto/common.hpp" diff --git a/examples/bank/include/bank/models/payment_model.hpp b/examples/bank/include/bank/models/payment_model.hpp index 2fd1747..688b0b7 100644 --- a/examples/bank/include/bank/models/payment_model.hpp +++ b/examples/bank/include/bank/models/payment_model.hpp @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#include -#include +#include +#include #include "bank/db/db_model.hpp" #include "bank/dto/common.hpp" diff --git a/examples/bank/include/bank/models/statement_model.hpp b/examples/bank/include/bank/models/statement_model.hpp index 7eaf431..8a48c36 100644 --- a/examples/bank/include/bank/models/statement_model.hpp +++ b/examples/bank/include/bank/models/statement_model.hpp @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#include -#include +#include +#include #include "bank/db/db_model.hpp" #include "bank/dto/statement_dto.hpp" diff --git a/examples/bank/include/bank/models/transaction_model.hpp b/examples/bank/include/bank/models/transaction_model.hpp index f952de6..93ebe24 100644 --- a/examples/bank/include/bank/models/transaction_model.hpp +++ b/examples/bank/include/bank/models/transaction_model.hpp @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once -#include -#include +#include +#include #include "bank/db/db_model.hpp" #include "bank/dto/transaction_dto.hpp" diff --git a/examples/bank/src/app/app.cpp b/examples/bank/src/app/app.cpp index 54a7f92..e784f53 100644 --- a/examples/bank/src/app/app.cpp +++ b/examples/bank/src/app/app.cpp @@ -3,8 +3,8 @@ #include "bank/app/app.hpp" #include -#include -#include +#include +#include #include diff --git a/examples/bank/src/cli/main.cpp b/examples/bank/src/cli/main.cpp index 6c7666b..3ba0026 100644 --- a/examples/bank/src/cli/main.cpp +++ b/examples/bank/src/cli/main.cpp @@ -5,13 +5,13 @@ // to show that the model code and call sites are identical regardless of where // the models actually execute. -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include #include #include diff --git a/examples/bank/src/models/account_model.cpp b/examples/bank/src/models/account_model.cpp index 60be7af..e8252c0 100644 --- a/examples/bank/src/models/account_model.cpp +++ b/examples/bank/src/models/account_model.cpp @@ -3,7 +3,7 @@ #include "bank/models/account_model.hpp" #include -#include +#include #include #include diff --git a/examples/bank/tests/bank_test_support.hpp b/examples/bank/tests/bank_test_support.hpp index 7e5bd69..00a00fc 100644 --- a/examples/bank/tests/bank_test_support.hpp +++ b/examples/bank/tests/bank_test_support.hpp @@ -2,8 +2,8 @@ #pragma once #include -#include -#include +#include +#include #include #include diff --git a/examples/bank/tests/test_account.cpp b/examples/bank/tests/test_account.cpp index 2146cd5..590908d 100644 --- a/examples/bank/tests/test_account.cpp +++ b/examples/bank/tests/test_account.cpp @@ -2,7 +2,7 @@ #include -#include +#include #include #include diff --git a/examples/bank/tests/test_auth.cpp b/examples/bank/tests/test_auth.cpp index 672cba4..bfd960b 100644 --- a/examples/bank/tests/test_auth.cpp +++ b/examples/bank/tests/test_auth.cpp @@ -2,7 +2,7 @@ #include -#include +#include #include #include diff --git a/examples/bank/tests/test_budget.cpp b/examples/bank/tests/test_budget.cpp index a5f9b7f..ca1367c 100644 --- a/examples/bank/tests/test_budget.cpp +++ b/examples/bank/tests/test_budget.cpp @@ -2,7 +2,7 @@ #include -#include +#include #include #include diff --git a/examples/bank/tests/test_card.cpp b/examples/bank/tests/test_card.cpp index bcfefd0..c28a913 100644 --- a/examples/bank/tests/test_card.cpp +++ b/examples/bank/tests/test_card.cpp @@ -2,7 +2,7 @@ #include -#include +#include #include #include diff --git a/examples/bank/tests/test_loan.cpp b/examples/bank/tests/test_loan.cpp index b31e7ef..eaaa73b 100644 --- a/examples/bank/tests/test_loan.cpp +++ b/examples/bank/tests/test_loan.cpp @@ -2,7 +2,7 @@ #include -#include +#include #include #include diff --git a/examples/bank/tests/test_notification.cpp b/examples/bank/tests/test_notification.cpp index 85c9340..f8074b2 100644 --- a/examples/bank/tests/test_notification.cpp +++ b/examples/bank/tests/test_notification.cpp @@ -2,7 +2,7 @@ #include -#include +#include #include #include diff --git a/examples/bank/tests/test_offline.cpp b/examples/bank/tests/test_offline.cpp index 89ee11b..05b5848 100644 --- a/examples/bank/tests/test_offline.cpp +++ b/examples/bank/tests/test_offline.cpp @@ -7,10 +7,10 @@ #include -#include -#include -#include -#include +#include +#include +#include +#include #include #include diff --git a/examples/bank/tests/test_payee.cpp b/examples/bank/tests/test_payee.cpp index 0c9a121..d6de930 100644 --- a/examples/bank/tests/test_payee.cpp +++ b/examples/bank/tests/test_payee.cpp @@ -2,7 +2,7 @@ #include -#include +#include #include #include diff --git a/examples/bank/tests/test_payment.cpp b/examples/bank/tests/test_payment.cpp index e625fd0..f91c5c4 100644 --- a/examples/bank/tests/test_payment.cpp +++ b/examples/bank/tests/test_payment.cpp @@ -2,7 +2,7 @@ #include -#include +#include #include #include diff --git a/examples/bank/tests/test_relations.cpp b/examples/bank/tests/test_relations.cpp index e681acd..eb6c614 100644 --- a/examples/bank/tests/test_relations.cpp +++ b/examples/bank/tests/test_relations.cpp @@ -14,7 +14,7 @@ #include #include -#include +#include #include #include diff --git a/examples/bank/tests/test_remote.cpp b/examples/bank/tests/test_remote.cpp index b80a8b8..d8150c6 100644 --- a/examples/bank/tests/test_remote.cpp +++ b/examples/bank/tests/test_remote.cpp @@ -8,11 +8,11 @@ #include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include #include #include diff --git a/examples/bank/tests/test_statement.cpp b/examples/bank/tests/test_statement.cpp index bc003ac..9235537 100644 --- a/examples/bank/tests/test_statement.cpp +++ b/examples/bank/tests/test_statement.cpp @@ -2,7 +2,7 @@ #include -#include +#include #include #include diff --git a/examples/bank/tests/test_transaction.cpp b/examples/bank/tests/test_transaction.cpp index 40dd58f..2ff0ab7 100644 --- a/examples/bank/tests/test_transaction.cpp +++ b/examples/bank/tests/test_transaction.cpp @@ -2,7 +2,7 @@ #include -#include +#include #include #include diff --git a/examples/forms/gui_qml/FormsController.cpp b/examples/forms/gui_qml/FormsController.cpp index 97a840b..7f0852f 100644 --- a/examples/forms/gui_qml/FormsController.cpp +++ b/examples/forms/gui_qml/FormsController.cpp @@ -7,7 +7,7 @@ #include #include -#include +#include #include "lab_schemas.hpp" diff --git a/examples/forms/gui_qml/FormsController.hpp b/examples/forms/gui_qml/FormsController.hpp index 08cb424..23b1e8e 100644 --- a/examples/forms/gui_qml/FormsController.hpp +++ b/examples/forms/gui_qml/FormsController.hpp @@ -22,8 +22,8 @@ // Q_PROPERTY declarations below; it must not be pointed at morph's // template-heavy headers (bridge.hpp, glaze) or the Qt executor. #ifndef Q_MOC_RUN -#include -#include +#include +#include #include #include "lab_model.hpp" diff --git a/examples/forms/lab_model.hpp b/examples/forms/lab_model.hpp index 8f2f8d2..5a35c36 100644 --- a/examples/forms/lab_model.hpp +++ b/examples/forms/lab_model.hpp @@ -10,11 +10,11 @@ /// - required fields -> member types + `optionalFields` opt-out /// - readiness (validate) -> morph::forms::allRequiredEngaged -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include #include #include diff --git a/examples/forms/lab_units.hpp b/examples/forms/lab_units.hpp index 046c955..60ac6ec 100644 --- a/examples/forms/lab_units.hpp +++ b/examples/forms/lab_units.hpp @@ -11,7 +11,7 @@ /// never `kgm3`). The `id` strings become schema/wire vocabulary: append new /// enumerators, never renumber or rename existing ones. -#include +#include #include #include diff --git a/examples/forms/main.cpp b/examples/forms/main.cpp index 76d2edc..f98acf2 100644 --- a/examples/forms/main.cpp +++ b/examples/forms/main.cpp @@ -14,7 +14,7 @@ /// the REPL — which dispatches it through the same type-erased /// `ActionDispatcher` seam `RemoteServer` uses — and read the model's reply. -#include +#include #include #include diff --git a/include/morph/backend.hpp b/include/morph/core/backend.hpp similarity index 99% rename from include/morph/backend.hpp rename to include/morph/core/backend.hpp index af87db6..ac1ad46 100644 --- a/include/morph/backend.hpp +++ b/include/morph/core/backend.hpp @@ -15,7 +15,7 @@ #include "completion.hpp" #include "model.hpp" #include "registry.hpp" -#include "session.hpp" +#include "../session/session.hpp" #include "strand.hpp" namespace morph::backend { diff --git a/include/morph/bridge.hpp b/include/morph/core/bridge.hpp similarity index 99% rename from include/morph/bridge.hpp rename to include/morph/core/bridge.hpp index 3cf9690..bd15b57 100644 --- a/include/morph/bridge.hpp +++ b/include/morph/core/bridge.hpp @@ -16,9 +16,9 @@ #include "backend.hpp" #include "completion.hpp" -#include "forms.hpp" +#include "../forms/forms.hpp" #include "registry.hpp" -#include "session.hpp" +#include "../session/session.hpp" namespace morph::bridge { diff --git a/include/morph/completion.hpp b/include/morph/core/completion.hpp similarity index 100% rename from include/morph/completion.hpp rename to include/morph/core/completion.hpp diff --git a/include/morph/executor.hpp b/include/morph/core/executor.hpp similarity index 100% rename from include/morph/executor.hpp rename to include/morph/core/executor.hpp diff --git a/include/morph/logger.hpp b/include/morph/core/logger.hpp similarity index 100% rename from include/morph/logger.hpp rename to include/morph/core/logger.hpp diff --git a/include/morph/model.hpp b/include/morph/core/model.hpp similarity index 99% rename from include/morph/model.hpp rename to include/morph/core/model.hpp index 343c8b6..8deb840 100644 --- a/include/morph/model.hpp +++ b/include/morph/core/model.hpp @@ -7,8 +7,8 @@ #include #include -#include "action_log.hpp" -#include "session.hpp" +#include "../journal/action_log.hpp" +#include "../session/session.hpp" #include "strand.hpp" namespace morph::model::detail { diff --git a/include/morph/registry.hpp b/include/morph/core/registry.hpp similarity index 100% rename from include/morph/registry.hpp rename to include/morph/core/registry.hpp diff --git a/include/morph/remote.hpp b/include/morph/core/remote.hpp similarity index 99% rename from include/morph/remote.hpp rename to include/morph/core/remote.hpp index e6c8131..00f26bc 100644 --- a/include/morph/remote.hpp +++ b/include/morph/core/remote.hpp @@ -12,9 +12,9 @@ #include #include -#include "action_log.hpp" +#include "../journal/action_log.hpp" #include "backend.hpp" -#include "session.hpp" +#include "../session/session.hpp" #include "wire.hpp" namespace morph::backend { diff --git a/include/morph/strand.hpp b/include/morph/core/strand.hpp similarity index 100% rename from include/morph/strand.hpp rename to include/morph/core/strand.hpp diff --git a/include/morph/wire.hpp b/include/morph/core/wire.hpp similarity index 99% rename from include/morph/wire.hpp rename to include/morph/core/wire.hpp index 3476ae5..3b26b13 100644 --- a/include/morph/wire.hpp +++ b/include/morph/core/wire.hpp @@ -8,7 +8,7 @@ #include #include -#include "session.hpp" +#include "../session/session.hpp" namespace morph::wire { diff --git a/include/morph/detail/quantity_equation.hpp b/include/morph/detail/quantity_equation.hpp index 42a9995..19927f4 100644 --- a/include/morph/detail/quantity_equation.hpp +++ b/include/morph/detail/quantity_equation.hpp @@ -17,7 +17,7 @@ #include #include -#include "../rational.hpp" +#include "../util/rational.hpp" namespace morph::units::detail { diff --git a/include/morph/choice.hpp b/include/morph/forms/choice.hpp similarity index 98% rename from include/morph/choice.hpp rename to include/morph/forms/choice.hpp index 1a2dec7..24920f6 100644 --- a/include/morph/choice.hpp +++ b/include/morph/forms/choice.hpp @@ -2,7 +2,7 @@ #pragma once -/// @file choice.hpp +/// @file forms/choice.hpp /// @brief Fields whose value is picked from a list served by another action. /// /// A `morph::forms::Choice` member declares, in the type: @@ -41,7 +41,7 @@ #include #include -#include "detail/fixed_string.hpp" +#include "../detail/fixed_string.hpp" namespace morph::forms { diff --git a/include/morph/forms.hpp b/include/morph/forms/forms.hpp similarity index 99% rename from include/morph/forms.hpp rename to include/morph/forms/forms.hpp index 56f7c6b..90ef574 100644 --- a/include/morph/forms.hpp +++ b/include/morph/forms/forms.hpp @@ -2,7 +2,7 @@ #pragma once -/// @file forms.hpp +/// @file forms/forms.hpp /// @brief JSON-Forms-style schema generation for morph actions. /// /// Given an action type `A` (a plain aggregate, as registered with @@ -73,7 +73,7 @@ #include #include "choice.hpp" -#include "quantity.hpp" +#include "../util/quantity.hpp" namespace morph::forms { diff --git a/include/morph/action_log.hpp b/include/morph/journal/action_log.hpp similarity index 100% rename from include/morph/action_log.hpp rename to include/morph/journal/action_log.hpp diff --git a/include/morph/file_action_log.hpp b/include/morph/journal/file_action_log.hpp similarity index 99% rename from include/morph/file_action_log.hpp rename to include/morph/journal/file_action_log.hpp index ef88834..db3dbc1 100644 --- a/include/morph/file_action_log.hpp +++ b/include/morph/journal/file_action_log.hpp @@ -2,7 +2,7 @@ #pragma once #include "action_log.hpp" -#include "logger.hpp" +#include "../core/logger.hpp" #include #include diff --git a/include/morph/journal.hpp b/include/morph/journal/journal.hpp similarity index 99% rename from include/morph/journal.hpp rename to include/morph/journal/journal.hpp index 32d2e51..e553afa 100644 --- a/include/morph/journal.hpp +++ b/include/morph/journal/journal.hpp @@ -11,8 +11,8 @@ #include #include "action_log.hpp" -#include "model.hpp" -#include "registry.hpp" +#include "../core/model.hpp" +#include "../core/registry.hpp" namespace morph::journal { diff --git a/include/morph/network_monitor.hpp b/include/morph/offline/network_monitor.hpp similarity index 100% rename from include/morph/network_monitor.hpp rename to include/morph/offline/network_monitor.hpp diff --git a/include/morph/offline_queue.hpp b/include/morph/offline/offline_queue.hpp similarity index 100% rename from include/morph/offline_queue.hpp rename to include/morph/offline/offline_queue.hpp diff --git a/include/morph/reconnect_coordinator.hpp b/include/morph/offline/reconnect_coordinator.hpp similarity index 99% rename from include/morph/reconnect_coordinator.hpp rename to include/morph/offline/reconnect_coordinator.hpp index 131de15..1ad036f 100644 --- a/include/morph/reconnect_coordinator.hpp +++ b/include/morph/offline/reconnect_coordinator.hpp @@ -7,7 +7,7 @@ #include #include -#include "logger.hpp" +#include "../core/logger.hpp" namespace morph::offline { diff --git a/include/morph/sync_worker.hpp b/include/morph/offline/sync_worker.hpp similarity index 99% rename from include/morph/sync_worker.hpp rename to include/morph/offline/sync_worker.hpp index bec30a7..21cad90 100644 --- a/include/morph/sync_worker.hpp +++ b/include/morph/offline/sync_worker.hpp @@ -8,7 +8,7 @@ #include #include -#include "logger.hpp" +#include "../core/logger.hpp" #include "offline_queue.hpp" namespace morph::offline { diff --git a/include/morph/qt/qt_executor.hpp b/include/morph/qt/qt_executor.hpp index 7d06ccb..2f34d3d 100644 --- a/include/morph/qt/qt_executor.hpp +++ b/include/morph/qt/qt_executor.hpp @@ -3,7 +3,7 @@ #pragma once #include #include -#include +#include #include namespace morph::qt { diff --git a/include/morph/qt/qt_websocket_backend.hpp b/include/morph/qt/qt_websocket_backend.hpp index 2cc10d6..79519dd 100644 --- a/include/morph/qt/qt_websocket_backend.hpp +++ b/include/morph/qt/qt_websocket_backend.hpp @@ -6,8 +6,8 @@ #include #include #include -#include -#include +#include +#include #include #include #include diff --git a/include/morph/qt/qt_websocket_server.hpp b/include/morph/qt/qt_websocket_server.hpp index 398194a..4e16c3d 100644 --- a/include/morph/qt/qt_websocket_server.hpp +++ b/include/morph/qt/qt_websocket_server.hpp @@ -5,7 +5,7 @@ #include #include #include -#include +#include #include #include diff --git a/include/morph/session.hpp b/include/morph/session/session.hpp similarity index 100% rename from include/morph/session.hpp rename to include/morph/session/session.hpp diff --git a/include/morph/session_auth.hpp b/include/morph/session/session_auth.hpp similarity index 99% rename from include/morph/session_auth.hpp rename to include/morph/session/session_auth.hpp index 0ea0796..ab7a3dc 100644 --- a/include/morph/session_auth.hpp +++ b/include/morph/session/session_auth.hpp @@ -15,7 +15,7 @@ #include "session.hpp" -/// @file session_auth.hpp +/// @file session/session_auth.hpp /// @brief Opt-in authenticated sessions: signed bearer tokens and a verifying /// `IAuthorizer` so a `RemoteServer` can trust the caller's identity. /// diff --git a/include/morph/datetime.hpp b/include/morph/util/datetime.hpp similarity index 99% rename from include/morph/datetime.hpp rename to include/morph/util/datetime.hpp index 22d2d00..37679e3 100644 --- a/include/morph/datetime.hpp +++ b/include/morph/util/datetime.hpp @@ -2,7 +2,7 @@ #pragma once -/// @file datetime.hpp +/// @file util/datetime.hpp /// @brief UTC timestamps for morph actions. /// /// Two types, mirroring the `Rational`/`Quantity` split: diff --git a/include/morph/quantity.hpp b/include/morph/util/quantity.hpp similarity index 99% rename from include/morph/quantity.hpp rename to include/morph/util/quantity.hpp index 4d7e131..36a1388 100644 --- a/include/morph/quantity.hpp +++ b/include/morph/util/quantity.hpp @@ -2,7 +2,7 @@ #pragma once -/// @file quantity.hpp +/// @file util/quantity.hpp /// @brief Unit-tagged, optionally-empty exact values with a derivation trail. /// /// `morph::units::Quantity` wraps an *optional* `morph::math::Rational` and @@ -59,7 +59,7 @@ #include #endif -#include "detail/fixed_string.hpp" +#include "../detail/fixed_string.hpp" #include "rational.hpp" namespace morph::units { @@ -981,7 +981,7 @@ struct NamedQuantity : Quantity { } // namespace morph::units #if MORPH_QUANTITY_PROVENANCE -#include "detail/quantity_equation.hpp" +#include "../detail/quantity_equation.hpp" #endif /// @brief Renders value + unit (`5.2kW`, `N/A%`); no `operator<<` is provided. diff --git a/include/morph/rational.hpp b/include/morph/util/rational.hpp similarity index 99% rename from include/morph/rational.hpp rename to include/morph/util/rational.hpp index 0fa3ab3..6340d32 100644 --- a/include/morph/rational.hpp +++ b/include/morph/util/rational.hpp @@ -2,7 +2,7 @@ #pragma once -/// @file rational.hpp +/// @file util/rational.hpp /// @brief Exact rational arithmetic for morph model/action values. /// /// `morph::math::Rational` is a small, value-semantic, trivially-copyable diff --git a/src/main.cpp b/src/main.cpp index 5b5bc1c..0448097 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,9 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 -#include -#include -#include -#include +#include +#include +#include +#include #include #include diff --git a/src/qt/qt_websocket_backend.cpp b/src/qt/qt_websocket_backend.cpp index fb9e138..7d901d8 100644 --- a/src/qt/qt_websocket_backend.cpp +++ b/src/qt/qt_websocket_backend.cpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/tests/qt/qt_test_client_main.cpp b/tests/qt/qt_test_client_main.cpp index aed426a..cbf78a1 100644 --- a/tests/qt/qt_test_client_main.cpp +++ b/tests/qt/qt_test_client_main.cpp @@ -17,8 +17,8 @@ #include #include #include -#include -#include +#include +#include #include #include #include diff --git a/tests/qt/qt_test_models.hpp b/tests/qt/qt_test_models.hpp index dfdbdd9..040f8d0 100644 --- a/tests/qt/qt_test_models.hpp +++ b/tests/qt/qt_test_models.hpp @@ -8,8 +8,8 @@ // (`bridge_model_reg_##M`) which can't accept `::` in the type name. #pragma once -#include -#include +#include +#include #include struct ProcTestEchoAction { diff --git a/tests/qt/qt_test_server_main.cpp b/tests/qt/qt_test_server_main.cpp index 1038ba9..ba5ae92 100644 --- a/tests/qt/qt_test_server_main.cpp +++ b/tests/qt/qt_test_server_main.cpp @@ -13,9 +13,9 @@ #include #include #include -#include +#include #include -#include +#include #include #include #include diff --git a/tests/qt/test_qt_websocket.cpp b/tests/qt/test_qt_websocket.cpp index 379db78..b52cc39 100644 --- a/tests/qt/test_qt_websocket.cpp +++ b/tests/qt/test_qt_websocket.cpp @@ -10,14 +10,14 @@ #include #include #include -#include -#include +#include +#include #include #include #include -#include -#include -#include +#include +#include +#include #include #include #include diff --git a/tests/test_action_log.cpp b/tests/test_action_log.cpp index 49b25ff..660cf4a 100644 --- a/tests/test_action_log.cpp +++ b/tests/test_action_log.cpp @@ -7,15 +7,15 @@ // runner and Bridge::executeVia's localOp), SessionLog checkpoint/undo, and // journal::replay. -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include diff --git a/tests/test_action_log_phase2.cpp b/tests/test_action_log_phase2.cpp index 3c26f09..a18d9e6 100644 --- a/tests/test_action_log_phase2.cpp +++ b/tests/test_action_log_phase2.cpp @@ -5,15 +5,15 @@ // RemoteServer::LogProvider mechanism that closes phase 1's "remote identity" // gap. (Phase 3, a Kafka-shaped sink, was dropped for now.) -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include diff --git a/tests/test_backend_extra.cpp b/tests/test_backend_extra.cpp index 835baf7..72722b8 100644 --- a/tests/test_backend_extra.cpp +++ b/tests/test_backend_extra.cpp @@ -1,9 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 -#include -#include -#include -#include +#include +#include +#include +#include #include #include #include diff --git a/tests/test_bridge_execute_json.cpp b/tests/test_bridge_execute_json.cpp index 7986f16..1172d08 100644 --- a/tests/test_bridge_execute_json.cpp +++ b/tests/test_bridge_execute_json.cpp @@ -1,9 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 -#include -#include -#include -#include +#include +#include +#include +#include #include #include diff --git a/tests/test_bridge_fixes.cpp b/tests/test_bridge_fixes.cpp index 1b20c72..79f71e1 100644 --- a/tests/test_bridge_fixes.cpp +++ b/tests/test_bridge_fixes.cpp @@ -17,13 +17,13 @@ // their declared precision so the stored value matches the schema's // advertised x-decimalPlaces. -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include #include #include diff --git a/tests/test_bridge_lifetime.cpp b/tests/test_bridge_lifetime.cpp index 7b90b66..251a170 100644 --- a/tests/test_bridge_lifetime.cpp +++ b/tests/test_bridge_lifetime.cpp @@ -15,10 +15,10 @@ #include #include #include -#include -#include -#include -#include +#include +#include +#include +#include #include #include #include diff --git a/tests/test_bridge_local.cpp b/tests/test_bridge_local.cpp index f773c2a..a793d7f 100644 --- a/tests/test_bridge_local.cpp +++ b/tests/test_bridge_local.cpp @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 -#include -#include -#include +#include +#include +#include #include #include #include diff --git a/tests/test_bridge_remote.cpp b/tests/test_bridge_remote.cpp index 43f3a2c..8245383 100644 --- a/tests/test_bridge_remote.cpp +++ b/tests/test_bridge_remote.cpp @@ -1,9 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 -#include -#include -#include -#include +#include +#include +#include +#include #include #include #include diff --git a/tests/test_completion.cpp b/tests/test_completion.cpp index e49aaf4..4b00b72 100644 --- a/tests/test_completion.cpp +++ b/tests/test_completion.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -#include +#include #include #include #include diff --git a/tests/test_completion_branches.cpp b/tests/test_completion_branches.cpp index 011f6a9..b61c3f0 100644 --- a/tests/test_completion_branches.cpp +++ b/tests/test_completion_branches.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -#include +#include #include #include #include diff --git a/tests/test_completion_extra.cpp b/tests/test_completion_extra.cpp index e5b1e11..5390727 100644 --- a/tests/test_completion_extra.cpp +++ b/tests/test_completion_extra.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 -#include -#include +#include +#include #include #include #include diff --git a/tests/test_concurrency_invariants.cpp b/tests/test_concurrency_invariants.cpp index fedf5a9..70c8352 100644 --- a/tests/test_concurrency_invariants.cpp +++ b/tests/test_concurrency_invariants.cpp @@ -6,16 +6,16 @@ // callback executor identity, atomic-once delivery, orphan logging, atomic // backend swap under load, monitor reentrancy, and mid-replay cancellation. -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include #include diff --git a/tests/test_conflict_resolution.cpp b/tests/test_conflict_resolution.cpp index 567aa6a..c8ef7de 100644 --- a/tests/test_conflict_resolution.cpp +++ b/tests/test_conflict_resolution.cpp @@ -11,11 +11,11 @@ // The conflict checker and resolver are injected at construction so each // test can control the scenario without subclassing. -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include #include #include #include diff --git a/tests/test_coverage_extra.cpp b/tests/test_coverage_extra.cpp index 5db0680..35cfbe9 100644 --- a/tests/test_coverage_extra.cpp +++ b/tests/test_coverage_extra.cpp @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 -#include -#include -#include +#include +#include +#include #include #include #include diff --git a/tests/test_coverage_gaps.cpp b/tests/test_coverage_gaps.cpp index b570385..35a5c88 100644 --- a/tests/test_coverage_gaps.cpp +++ b/tests/test_coverage_gaps.cpp @@ -4,14 +4,14 @@ // case names the file:line range it is meant to cover so future readers can // understand why an unusual edge case is being exercised here. -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include #include diff --git a/tests/test_coverage_push95.cpp b/tests/test_coverage_push95.cpp index 563a0ab..cb789c7 100644 --- a/tests/test_coverage_push95.cpp +++ b/tests/test_coverage_push95.cpp @@ -4,18 +4,18 @@ // file:line range it exercises so future readers understand why an unusual // edge case lives here. Companion to test_coverage_gaps.cpp / test_coverage_extra.cpp. -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include diff --git a/tests/test_datetime.cpp b/tests/test_datetime.cpp index 23825ca..b608a36 100644 --- a/tests/test_datetime.cpp +++ b/tests/test_datetime.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -#include +#include #include diff --git a/tests/test_dispatch_di.cpp b/tests/test_dispatch_di.cpp index afabde9..d8573aa 100644 --- a/tests/test_dispatch_di.cpp +++ b/tests/test_dispatch_di.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -#include +#include #include #include #include diff --git a/tests/test_example.cpp b/tests/test_example.cpp index 24b4d4e..ee2b1cc 100644 --- a/tests/test_example.cpp +++ b/tests/test_example.cpp @@ -1,9 +1,9 @@ #include #include #include -#include -#include -#include +#include +#include +#include #include #include diff --git a/tests/test_executor.cpp b/tests/test_executor.cpp index 1f3a623..867ec8a 100644 --- a/tests/test_executor.cpp +++ b/tests/test_executor.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -#include +#include #include #include #include diff --git a/tests/test_executor_extra.cpp b/tests/test_executor_extra.cpp index 9b1bcc2..f071318 100644 --- a/tests/test_executor_extra.cpp +++ b/tests/test_executor_extra.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -#include +#include #include #include #include diff --git a/tests/test_handler_binding.cpp b/tests/test_handler_binding.cpp index 5e8ee83..db07f6c 100644 --- a/tests/test_handler_binding.cpp +++ b/tests/test_handler_binding.cpp @@ -1,9 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 -#include -#include -#include -#include +#include +#include +#include +#include #include #include #include diff --git a/tests/test_issues.cpp b/tests/test_issues.cpp index 710d4b1..2f382d6 100644 --- a/tests/test_issues.cpp +++ b/tests/test_issues.cpp @@ -3,18 +3,18 @@ // Tests covering issues found during codebase analysis. // Each section corresponds to a numbered issue in the analysis document. -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include #include diff --git a/tests/test_logger.cpp b/tests/test_logger.cpp index 166d359..ef27437 100644 --- a/tests/test_logger.cpp +++ b/tests/test_logger.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -#include +#include #include #include #include diff --git a/tests/test_model.cpp b/tests/test_model.cpp index 511bf36..865d0ee 100644 --- a/tests/test_model.cpp +++ b/tests/test_model.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -#include +#include #include #include diff --git a/tests/test_network_monitor.cpp b/tests/test_network_monitor.cpp index b8dc87f..14925af 100644 --- a/tests/test_network_monitor.cpp +++ b/tests/test_network_monitor.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -#include +#include #include #include #include diff --git a/tests/test_offline_integration.cpp b/tests/test_offline_integration.cpp index e2f3d76..5559623 100644 --- a/tests/test_offline_integration.cpp +++ b/tests/test_offline_integration.cpp @@ -14,13 +14,13 @@ // 6. All queue items were replayed and removed. // 7. Execute on the handler still works, confirming the new backend is live. -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include #include #include #include diff --git a/tests/test_offline_queue.cpp b/tests/test_offline_queue.cpp index 73ecfd6..4d5f82c 100644 --- a/tests/test_offline_queue.cpp +++ b/tests/test_offline_queue.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -#include +#include #include #include #include diff --git a/tests/test_policy_hardening.cpp b/tests/test_policy_hardening.cpp index 41a42df..cc7b6c4 100644 --- a/tests/test_policy_hardening.cpp +++ b/tests/test_policy_hardening.cpp @@ -9,17 +9,17 @@ // 6. UnitRelation positivity guard (compile-time; witnessed here at runtime). // 7. Log-injection sanitisation in the default sink. -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include diff --git a/tests/test_quantity.cpp b/tests/test_quantity.cpp index 22e2d64..0152672 100644 --- a/tests/test_quantity.cpp +++ b/tests/test_quantity.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -#include +#include #include diff --git a/tests/test_quantity_forms.cpp b/tests/test_quantity_forms.cpp index ff94102..dce2fb4 100644 --- a/tests/test_quantity_forms.cpp +++ b/tests/test_quantity_forms.cpp @@ -1,12 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include #include diff --git a/tests/test_rational.cpp b/tests/test_rational.cpp index 4d2c0b2..f2f5777 100644 --- a/tests/test_rational.cpp +++ b/tests/test_rational.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -#include +#include #include #include diff --git a/tests/test_reconnect_coordinator.cpp b/tests/test_reconnect_coordinator.cpp index 0c0a25f..850e407 100644 --- a/tests/test_reconnect_coordinator.cpp +++ b/tests/test_reconnect_coordinator.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 -#include -#include +#include +#include #include #include #include diff --git a/tests/test_registry_extra.cpp b/tests/test_registry_extra.cpp index 6bd600a..bf24b87 100644 --- a/tests/test_registry_extra.cpp +++ b/tests/test_registry_extra.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -#include +#include #include #include #include diff --git a/tests/test_remote_extra.cpp b/tests/test_remote_extra.cpp index 83dfdad..6dab4b4 100644 --- a/tests/test_remote_extra.cpp +++ b/tests/test_remote_extra.cpp @@ -1,12 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include #include #include #include diff --git a/tests/test_security_fixes.cpp b/tests/test_security_fixes.cpp index 474cc23..9059629 100644 --- a/tests/test_security_fixes.cpp +++ b/tests/test_security_fixes.cpp @@ -7,12 +7,12 @@ // unauthenticated identity is never presented to the model. // (FIX 2 / FIX 4 live in test_bridge_lifetime.cpp; FIX 3 is Qt-only.) -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include #include #include diff --git a/tests/test_server_limits.cpp b/tests/test_server_limits.cpp index fd82e9e..c66af71 100644 --- a/tests/test_server_limits.cpp +++ b/tests/test_server_limits.cpp @@ -5,12 +5,12 @@ // captures throughput numbers for the local round-trip via Catch2's benchmark // support (see catch2/benchmark). -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include #include #include #include diff --git a/tests/test_session_auth.cpp b/tests/test_session_auth.cpp index 8585f23..5625d68 100644 --- a/tests/test_session_auth.cpp +++ b/tests/test_session_auth.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -#include +#include #include diff --git a/tests/test_strand.cpp b/tests/test_strand.cpp index 29bee38..c17dd1a 100644 --- a/tests/test_strand.cpp +++ b/tests/test_strand.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -#include +#include #include #include #include diff --git a/tests/test_strand_extra.cpp b/tests/test_strand_extra.cpp index c3ac44d..02416f9 100644 --- a/tests/test_strand_extra.cpp +++ b/tests/test_strand_extra.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #include -#include +#include #include #include #include diff --git a/tests/test_strand_race.cpp b/tests/test_strand_race.cpp index 5899128..650f9c7 100644 --- a/tests/test_strand_race.cpp +++ b/tests/test_strand_race.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 -#include -#include +#include +#include #include #include diff --git a/tests/test_subscription.cpp b/tests/test_subscription.cpp index 083d2d9..577738a 100644 --- a/tests/test_subscription.cpp +++ b/tests/test_subscription.cpp @@ -1,10 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include #include #include #include diff --git a/tests/test_support.hpp b/tests/test_support.hpp index f27c538..425c55f 100644 --- a/tests/test_support.hpp +++ b/tests/test_support.hpp @@ -8,8 +8,8 @@ // keeps the test code consistent and lets the production API not have to expose // test-only utilities. -#include -#include +#include +#include #include #include diff --git a/tests/test_switch_backend.cpp b/tests/test_switch_backend.cpp index a4a7094..5e67665 100644 --- a/tests/test_switch_backend.cpp +++ b/tests/test_switch_backend.cpp @@ -3,11 +3,11 @@ #include #include #include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include #include #include "test_support.hpp" @@ -232,7 +232,7 @@ TEST_CASE( // ── Remote backend no-op ────────────────────────────────────────────────────── -#include +#include TEST_CASE("morph::backend::SimulatedRemoteBackend::notifyBackendChanged is a documented no-op", "[remote][notify]") { morph::exec::ThreadPoolExecutor pool{2}; diff --git a/tests/test_sync_worker.cpp b/tests/test_sync_worker.cpp index eaec9eb..e7d7275 100644 --- a/tests/test_sync_worker.cpp +++ b/tests/test_sync_worker.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 -#include -#include +#include +#include #include #include #include diff --git a/tests/test_wire_hardening.cpp b/tests/test_wire_hardening.cpp index f6d3cc1..968c4bc 100644 --- a/tests/test_wire_hardening.cpp +++ b/tests/test_wire_hardening.cpp @@ -11,7 +11,7 @@ // keys (last-wins); there is no option to enable rejection. These tests pin // the actual, documented behavior so the spec claim stays honest. -#include +#include #include #include