You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up from #793/#828 (the generic GenericModelBridgeCore work): a QML
client today has zero way to learn what models or actions the backend
exposes without being compiled against them. The user's framing: they want
to add a model server-side and have "all introduced models... automatically
served and exposed and understood by the client app" — a workspace the
client discovers, not a fixed set it was built knowing about.
Goal
The concrete target this is aiming at: one generic client binary usable
against any of the seven ladder rungs (pastebin, polls, bookmarks, kanban,
ledger, lims, crm) without being rebuilt per rung — point it at a different
server and it discovers and renders whatever that server exposes. This is
explicitly the stated end goal, not just "nicer discovery."
That target has two distinct gaps, addressed in the two sections below:
Dispatch coupling — a client can't talk to a model it wasn't compiled
against at all, even a schema-driven one (original scope of this issue).
What exists today, and what's missing — verified directly against the headers
Schema discovery exists, but only per-already-known-model, and only over
Remote.ActionDispatcher::schemasJson(modelId)
(include/morph/core/registry.hpp:880) returns the full {actionType: schema} document for one model — but it takes modelId as a
parameter the caller must already have. There is nothing upstream of it that
answers "what models exist" — confirmed by grepping the whole framework for
any listing/enumeration surface:
Dispatch itself requires a compile-time Model type — schema discovery
alone would not be enough. Every handler-shaped public method on morph::bridge::Bridge is a member template on Model
(registerHandler<Model>, attachHandler<Model>, attachHandlerAsync<Model>, assignHandlerPrimary<Model>, listInstancesOf<Model>, the BRIDGE_REGISTER_ACTION-registered <Model, Action> pair) — confirmed by
listing every public method signature in the class body
(include/morph/core/bridge.hpp, class Bridge { ... }, ~230 lines
surveyed). There is no raw, string-only "dispatch this actionId against this
modelId" entry point on Bridge itself. The actual dispatch path, ActionExecuteRegistry::execute<Sharing>(modelId, actionId, handler, bodyJson)
(bridge.hpp:97-111), takes a type-erased void* handler, but that pointer
must have been produced by constructing a concrete BridgeHandler<Model, Sharing> for that exact Model — its own doc comment
says so explicitly ("Type-erased BridgeHandler<Model, Sharing>* matching modelId"). So even a client that fetched a model's full schema at runtime
could not actually invoke an action on it unless the client binary was
already compiled with a BridgeHandler<ThatModel> — the schema tells it what to send, not how to send it.
Why this is the real blocker, not a UI problem
The rendering half of this is already solved generically: DynamicForm.qml
is one framework-owned component that draws any schema it's handed, with no
per-model QML anywhere in the tree (established while investigating #793).
The gap is entirely at the dispatch layer described above — a model
introduced on the server today requires a client rebuild (a new BridgeHandler<NewModel> instantiation, wired into a new or existing FormsControllerCore<NewModel>/GenericModelBridgeCore<NewModel>) no matter
how good schema discovery gets.
Filling the bespoke-UI gap: server-delivered QML modules
Raised alongside the Goal above: even if dispatch coupling (gap 1) is fully
solved, the client's UI ceiling is still "whatever the generic renderer can
draw." One candidate to close gap 2 as well: have the server ship the bespoke QML for a rung's non-generic parts (e.g. kanban's board view), and
have the client load it at runtime — Qt supports instantiating QML from a
string or a URL at runtime (QQmlComponent/Loader.source can point at
network content), so the mechanism has real prior art in Qt itself, even
though nothing in morph today does this — this is a new capability, not
an extension of an existing seam like schemasJson is for gap 1.
This is a materially bigger change than gap 1, with its own open questions,
not yet designed:
Trust boundary, categorically different from JSON discovery. A schema
document is inert data the client's own fixed renderer interprets; a
server-delivered QML file is executable code (QML plus embedded
JavaScript) the client would run. docs/spec/forms/forms.md's trust-model
language ("the server never trusts the client") is about the opposite
direction — nothing in the codebase today establishes what a client should
trust from a server. This needs its own explicit design, not an
extension of the forms trust model.
Client/server version compatibility. Server-authored QML would need to
target whatever QML/Qt runtime version the connecting client actually has;
a mismatch is a runtime failure, not a compile-time one.
Caching/offline story. Every rung's offline queue and sync worker
(morph::offline) assume the client's own logic is always available
locally; runtime-fetched UI code raises the question of what happens
offline before it's ever been fetched once.
Review/provenance. Unlike a schema (validated structurally against ActionTraits<A>), there's no proposed mechanism for a client to validate
that server-delivered QML is the version/author it expects.
Not scoped further here — flagging it as the natural second half of this
issue's Goal, to be designed separately (likely its own issue) once gap 1
has a direction, since the trust-boundary question above may turn out to be
the harder problem of the two.
Verification status
Directly verified (not inferred): the absence of any model-enumeration
API (grep above, zero matches across include/morph/), and that every Bridge handler-registration method is a Model template (read the full class Bridge body in include/morph/core/bridge.hpp).
Not verified / open questions this issue does not answer:
Whether a type-erased dispatch path is feasible at all without breaking ActionExecuteRegistry's Sharing-keyed lookup (bridge.hpp:65-79) — no
design attempted.
Whether ActionDispatcher::schemasJson(modelId)'s Remote-only restriction
is a deliberate trust-boundary decision or simply unimplemented for Local
mode — not checked against docs/spec/core/bridge.md's "Design decisions"
table.
Whether a "list registered models" endpoint belongs on ActionDispatcher
(server-side directory, mirroring schemasJson) or needs a new registry
entirely — no design attempted.
What a client does with a newly discovered model at runtime without any
compiled type for it — this likely requires the client to operate purely
on JSON (schema-in, JSON-body-out) for undiscovered models, which is a
different, weaker contract than the typed BridgeHandler<Model> path
gives today, and needs its own design discussion about what capabilities
(validation, offline queueing, typed results) a "fully dynamic" model
loses relative to a compiled one.
Whether server-delivered QML (above) is even the right answer to gap 2, or
whether a richer, still-declarative view/layout vocabulary (extending morph::views/docs/spec/forms/views.md rather than shipping executable
code) could cover kanban/bookmarks-shaped bespoke UI without opening an
execution trust boundary at all — not evaluated against each other.
What would change the verdict
Close as not worth building if a design pass concludes the typed BridgeHandler<Model> requirement is load-bearing enough (e.g. for the
offline queue, replay ledger, or shared-instance directory) that a truly
type-erased dispatch path would have to reimplement or bypass those
subsystems — at which point "the client rebuilds when a model is added" may
be the right tradeoff, and the issue becomes "make that rebuild as small as GenericModelBridgeCore's macro-free composition already makes it" rather
than "eliminate it."
Re-scope to "schema discovery only" (dropping the client's ability to talk
to genuinely new models without a rebuild) if a full type-erased dispatch
design turns out to conflict with the trust-boundary or sharing-policy
invariants above.
Split the "server-delivered QML" idea into its own issue once gap 1 has a
direction, rather than let it block on or get bundled with the dispatch-layer
work above — they are separable designs with different risk profiles.
Follow-up from #793/#828 (the generic
GenericModelBridgeCorework): a QMLclient today has zero way to learn what models or actions the backend
exposes without being compiled against them. The user's framing: they want
to add a model server-side and have "all introduced models... automatically
served and exposed and understood by the client app" — a workspace the
client discovers, not a fixed set it was built knowing about.
Goal
The concrete target this is aiming at: one generic client binary usable
against any of the seven ladder rungs (pastebin, polls, bookmarks, kanban,
ledger, lims, crm) without being rebuilt per rung — point it at a different
server and it discovers and renders whatever that server exposes. This is
explicitly the stated end goal, not just "nicer discovery."
That target has two distinct gaps, addressed in the two sections below:
against at all, even a schema-driven one (original scope of this issue).
ever be as capable as its generic renderer (
DynamicForm.qmlplus ageneric list/query view). Anything a rung does that isn't plain
schema-driven CRUD — kanban's drag-drop board, bookmarks' multi-model
dispatch (qt/forms: BookmarkFormsController hand-rolls multi-model action routing that no core template supports #827), any rung's custom offline-queue/sync-status display — has
no generic equivalent today, by design (examples: where the 42k lines of application code go, and the four building blocks that would remove most of it #793 measured that this curation
code doesn't generalize). Section "Filling the bespoke-UI gap" below is a
candidate answer to this second gap, added after this issue was filed.
What exists today, and what's missing — verified directly against the headers
Schema discovery exists, but only per-already-known-model, and only over
Remote.
ActionDispatcher::schemasJson(modelId)(
include/morph/core/registry.hpp:880) returns the full{actionType: schema}document for one model — but it takesmodelIdas aparameter the caller must already have. There is nothing upstream of it that
answers "what models exist" — confirmed by grepping the whole framework for
any listing/enumeration surface:
Dispatch itself requires a compile-time
Modeltype — schema discoveryalone would not be enough. Every handler-shaped public method on
morph::bridge::Bridgeis a member template onModel(
registerHandler<Model>,attachHandler<Model>,attachHandlerAsync<Model>,assignHandlerPrimary<Model>,listInstancesOf<Model>, theBRIDGE_REGISTER_ACTION-registered<Model, Action>pair) — confirmed bylisting every public method signature in the class body
(
include/morph/core/bridge.hpp,class Bridge { ... }, ~230 linessurveyed). There is no raw, string-only "dispatch this actionId against this
modelId" entry point on
Bridgeitself. The actual dispatch path,ActionExecuteRegistry::execute<Sharing>(modelId, actionId, handler, bodyJson)(
bridge.hpp:97-111), takes a type-erasedvoid* handler, but that pointermust have been produced by constructing a concrete
BridgeHandler<Model, Sharing>for that exactModel— its own doc commentsays so explicitly ("Type-erased
BridgeHandler<Model, Sharing>*matchingmodelId"). So even a client that fetched a model's full schema at runtimecould not actually invoke an action on it unless the client binary was
already compiled with a
BridgeHandler<ThatModel>— the schema tells itwhat to send, not how to send it.
Why this is the real blocker, not a UI problem
The rendering half of this is already solved generically:
DynamicForm.qmlis one framework-owned component that draws any schema it's handed, with no
per-model QML anywhere in the tree (established while investigating #793).
The gap is entirely at the dispatch layer described above — a model
introduced on the server today requires a client rebuild (a new
BridgeHandler<NewModel>instantiation, wired into a new or existingFormsControllerCore<NewModel>/GenericModelBridgeCore<NewModel>) no matterhow good schema discovery gets.
Filling the bespoke-UI gap: server-delivered QML modules
Raised alongside the Goal above: even if dispatch coupling (gap 1) is fully
solved, the client's UI ceiling is still "whatever the generic renderer can
draw." One candidate to close gap 2 as well: have the server ship the
bespoke QML for a rung's non-generic parts (e.g. kanban's board view), and
have the client load it at runtime — Qt supports instantiating QML from a
string or a URL at runtime (
QQmlComponent/Loader.sourcecan point atnetwork content), so the mechanism has real prior art in Qt itself, even
though nothing in morph today does this — this is a new capability, not
an extension of an existing seam like
schemasJsonis for gap 1.This is a materially bigger change than gap 1, with its own open questions,
not yet designed:
document is inert data the client's own fixed renderer interprets; a
server-delivered QML file is executable code (QML plus embedded
JavaScript) the client would run.
docs/spec/forms/forms.md's trust-modellanguage ("the server never trusts the client") is about the opposite
direction — nothing in the codebase today establishes what a client should
trust from a server. This needs its own explicit design, not an
extension of the forms trust model.
target whatever QML/Qt runtime version the connecting client actually has;
a mismatch is a runtime failure, not a compile-time one.
(
morph::offline) assume the client's own logic is always availablelocally; runtime-fetched UI code raises the question of what happens
offline before it's ever been fetched once.
ActionTraits<A>), there's no proposed mechanism for a client to validatethat server-delivered QML is the version/author it expects.
Not scoped further here — flagging it as the natural second half of this
issue's Goal, to be designed separately (likely its own issue) once gap 1
has a direction, since the trust-boundary question above may turn out to be
the harder problem of the two.
Verification status
Directly verified (not inferred): the absence of any model-enumeration
API (grep above, zero matches across
include/morph/), and that everyBridgehandler-registration method is aModeltemplate (read the fullclass Bridgebody ininclude/morph/core/bridge.hpp).Not verified / open questions this issue does not answer:
ActionExecuteRegistry'sSharing-keyed lookup (bridge.hpp:65-79) — nodesign attempted.
ActionDispatcher::schemasJson(modelId)'s Remote-only restrictionis a deliberate trust-boundary decision or simply unimplemented for Local
mode — not checked against
docs/spec/core/bridge.md's "Design decisions"table.
ActionDispatcher(server-side directory, mirroring
schemasJson) or needs a new registryentirely — no design attempted.
compiled type for it — this likely requires the client to operate purely
on JSON (schema-in, JSON-body-out) for undiscovered models, which is a
different, weaker contract than the typed
BridgeHandler<Model>pathgives today, and needs its own design discussion about what capabilities
(validation, offline queueing, typed results) a "fully dynamic" model
loses relative to a compiled one.
whether a richer, still-declarative view/layout vocabulary (extending
morph::views/docs/spec/forms/views.mdrather than shipping executablecode) could cover kanban/bookmarks-shaped bespoke UI without opening an
execution trust boundary at all — not evaluated against each other.
What would change the verdict
Close as not worth building if a design pass concludes the typed
BridgeHandler<Model>requirement is load-bearing enough (e.g. for theoffline queue, replay ledger, or shared-instance directory) that a truly
type-erased dispatch path would have to reimplement or bypass those
subsystems — at which point "the client rebuilds when a model is added" may
be the right tradeoff, and the issue becomes "make that rebuild as small as
GenericModelBridgeCore's macro-free composition already makes it" ratherthan "eliminate it."
Re-scope to "schema discovery only" (dropping the client's ability to talk
to genuinely new models without a rebuild) if a full type-erased dispatch
design turns out to conflict with the trust-boundary or sharing-policy
invariants above.
Split the "server-delivered QML" idea into its own issue once gap 1 has a
direction, rather than let it block on or get bundled with the dispatch-layer
work above — they are separable designs with different risk profiles.