ADR 0037 — Command forms (command.form)
- Status: accepted
- Date: 2026-10-04
- Slice: P37 (builder-roadmap: DSL-first, codegen-first, UI-first)
Context
Every command gets a command page in the Leptos web lens
(/commands/<cmd-kebab>), but the page was read-only: a table of field
names + Rust types and an "invoke" card naming the endpoint. To run a
command a user had to leave the web UI (admin surface, curl, CLI) — the
web's only command inputs were the P36 page-action dialogs (bound to a
specific projection) and the workflow canvas's raw-JSON textarea.
Meanwhile the shared cmd-form dispatch handler (the static
dispatch.js) already knew how to POST any form.cmd-form to its
data-mount — it just treated every control value as a string, so
typed inputs (ints, bools) could not round-trip.
The gap, DSL-first: a command should be able to declare its form (title, submit label, per-field hints) and the web lens should render a working, typed form for every command.
Decision
1. command.form (DSL + plan)
A command may declare
command:
name: PlaceOrder
fields: [ … ]
form:
title: "New order" # default: the command's `about`, else its name
submit: "Place order" # default: "submit <Command>"
fields:
- field: customer # a command input name (fail-closed)
label: Customer # default: the field name
widget: text # text | number | checkbox | textarea | select | json
required: true # default: the field's requiredness
default: "" # initial value (string literal)
placeholder: who?
The plan resolves a CmdFormPlan for every command (declared or
not):
- Unlisted inputs are appended in declaration order with a
type-derived widget: JSON/struct →
json, enum →select(variants resolved from the aggregate's in-scope enums: its own + the schema enums), bool →checkbox, int/float →number, else →text. - Declared hints refine the auto row; an omitted
widgetderives from the type the same way. - Fail-closed at plan time: a
fieldthat is not a command input, a duplicatedfield, an unknownwidget,selecton a non-enum,numberon a non-numeric,checkboxon a non-bool.
AggPlan.enums now carries the enums in scope for the aggregate's
field types (its own plus the schema enums), so the P36 dialog
resolution and the new form resolution agree on what an enum is.
2. The command page is a form (web codegen)
CommandPage_<Cmd> replaces the read-only fields table with a
form.cmd-form card: one <div> per form field — a label (required
fields carry *) and the control for the widget (<input type=text| number>, <input type=checkbox>, <select> with an empty "—" option
- enum variants (the default variant
selected),<textarea rows=3>(font-mono+{}placeholder forjson)) — then the submit button (formsubmitlabel) and adispatch-statusline the handler writes to. The "invoke" card (endpoint + CLI hint) stays below. Non-command pages (workflow CLI commands) keep the fields table.
3. Typed dispatch (the shared dispatch.js)
The global cmd-form handler now encodes by control type: checkbox →
bool, type=number → JSON number, textarea → JSON-parsed (falling back
to the raw string), else → string. This also upgrades the P36 dialogs'
SSR-sibling admin forms and any authored cmd-form.
4. P36 dialogs: the unified widget vocabulary
ui.actions[].dialog[].widget now accepts the same vocabulary
(text|number|checkbox|textarea|select|json); an omitted widget
derives from the field type (previously it had to be spelled out, and
an empty value was an error). select renders an enum <select>
(variants from the plan; string encoding on dispatch).
5. The web binary honors the app's CLI seam
The generated web main now reads serve --bind <addr> (the subcommand
is ignored), then MOAIC_BIND_ADDR (ADR 0025), then the leptos dev
site addr. The e2e harness spawns binaries with exactly that interface,
so the web SSR surface is testable by the same app_e2e blocks as the
app binary (the full-stack e2e gains a full_stack_web block asserting
the rendered form's markup).
Consequences
- Every command is runnable from the web with typed, pre-fillable controls — no JSON authoring for the common case; JSON fields keep a JSON textarea.
- Form semantics are display-side:
requiredmarks the label, the server-side truth stays the guard +command.validate(P36) + deserialization; the form never weakens a check. - The command page SSR HTML is e2e-assertable (GET → 200 + markup), which becomes the pattern for the upcoming web slices (P38–P40).
AggPlan.enumswidening changes the SSR admin form's selects: enum fields typed by a schema enum now render<select>s there too (previously text) — intended parity.
Verification
- Plan tests: type-derived widgets (text/number/checkbox/select+ variants/json), declared hints (title/submit/label/placeholder/ required/default), unlisted append, and all five fail-closed errors.
- Render tests: the command page's typed controls (incl.
selecteddefault variant, json textarea, required asterisk, dispatch-status), thedispatch.jsnumber encoding, and the dialogselect+ derived widgets. - Full-stack e2e: a new
full_stack_webblock boots the web binary via the harness (serve --bind) and asserts the SSR form markup (declared title/submit, field controls, auto-derived json widget, declared default prefill). - Conformance re-pinned: command-page forms in all examples' web
goldens,
dispatch.js, the webmainbind seam, and the full-stack app surface for the newPlaceOrder.urgentbool input (proto/agg/model/mcp/openapi/site).