Mosaic one model, many lenses

ADR 0046 — Use-case-first pages + rendered action dialogs

  • Status: accepted
  • Date: 2026-10-05
  • Slice: P46 (builder-roadmap: UI-first — the app reads as a set of use cases)

Context

P45 demoted the CQRS model pages to a collapsed "Developer" console, so the shell no longer starts from CQRS. But the list pages themselves still read as CQRS: every projection's list page rendered a card titled rows with the description "live read model — replayed from the event log". And the "trigger / create / update" surface (the per-page actions introduced in P36) was never actually rendered in any committed app — no golden example carried a table-view action (the flagship full-stack actions were on a kanban projection, which does not render action buttons). The action/dialog codegen was therefore never compiled, and a set of latent bugs shipped.

Goal: make a generated app read as a set of use cases — pages that list and present things and let the caller create / update / trigger them — framed in business terms, with the CQRS vocabulary confined to the developer console.

Decision

1. The list page is a use-case page, not a "read model" (compiler)

The list-page card no longer injects CQRS language:

  • The CardTitle is the projection's ui.page business label (falling back to the projection name) — e.g. Documents, Tickets — threaded from the page hint into rows_card / rows_view_card.
  • The CardDescription is the projection's about (a business sentence the author writes), shown only when present. The hardcoded "live read model — replayed from the event log" is gone. ProjPlan now carries about for this.

So the page title + description are the author's business framing; the compiler adds none of its own.

2. Neutral command-form submit (compiler)

The default command-form submit label was submit <Command> (the raw command name in a button). It is now a neutral Submit. (The command page title already defaulted to the command's about — business — so the create/update form reads as an action, not a CQRS command.)

3. The reference app is a set of named use-case pages (mosaic-pages)

The pages app (eugeis/mosaic-pages, a docs portal) now declares its two projections as use-case pages:

  • DocList → Documents (ui.page label, business about) with the create/update surface: a toolbar New document action (a dialog over Doc.Publish: id / app / title / body) and a row Retire action (two-step confirm over Doc.Retire).
  • LogList → Activity (the tenant's audit trail, read-only).

These are the app's front pages (above the Developer console, per P45).

4. The rendered action / dialog codegen (compiler — bug fixes)

Because no golden exercised a rendered table-view action, the action dialog codegen was broken in four independent ways. All are fixed and now pinned in the full-stack golden (a new Open ticket toolbar action on the split Tickets page):

  • The dialog cancel button emitted unquoted text (>cancel</button> with a stray "), which broke the view! token stream and left the component with an unclosed delimiter. It is now >"cancel"</button>.
  • The dialog component was named snake_case (doc_list_0_dlg); a lowercase tag in view! resolves as an HTML element. It is now PascalCase (DocList0Dlg), definition and reference in lockstep.
  • The dialog field signals used create_signal("") → &str, which does not satisfy bind:value's IntoSplitSignal. They are now create_rw_signal(String) / create_rw_signal(bool) (read-write String/bool signals).
  • The dialog's row prop is a plain value; it is now named row (a bare _row is not getter-wrapped by #[component]) and passed as a value (the dialog is created only while its Show is open, so it is current then). A let _ = &row; suppresses the unused warning for toolbar dialogs.
  • The per-action execute block is now a ;-terminated statement (not a bare tail block), and required-field guards are ;-terminated — view!'s syn parser rejects a bare block / bare if when another statement follows it in an on:click closure.

Consequences

  • Generated apps read as use cases: business-titled pages with a business description and an in-context create/update surface; CQRS confined to the Developer console.
  • The full-stack golden now pins a rendered table-view action, so a web-rendering regression in the action/dialog codegen fails conformance (and the example web is compiled in CI / local verification).
  • The mosaic-pages reference app ships named use-case pages (Documents, Activity) with a working publish/retire surface.
  • ProjPlan.about is new public surface (plan); the list-page card title now derives from the ui.page label, so a projection with a page hint shows that label on its list page (not just in the nav).