CX · Reference guideProjected surfaces — the ux capability
PlaygroundDownloadsAboutv0.17.0

Projected surfaces — the ux capability

You define commands and queries; the platform projects the interface. The ux capability is CX's third projection — the same [?def]s that serve the wire and the agent-tools face also produce forms, tables, and live regions, derived at render time from the definitions themselves. There is no UI manifest to drift, the same way there is no tool manifest to drift: when a command gains a parameter, its form gains a field.

One projection, many faces

Three entry points cover the surface. [$ux:form] projects a command's parameter schema into a complete form; [$ux:table] projects a query's row schema into a complete table; [$ux:feed] turns a live-modes subscription into a live region. A command is discriminated exactly as the agent-tools face discriminates it — an ordinary [?def] whose [effects] clause is present — and the projection reuses the same machinery: fields from the param list, required-ness from the non-defaulted positionals, labels and help text from the same [fn-doc] / [param-doc] sources the tool descriptors consume.

      [?def place-order impure ($sku $qty)
           [preconditions [in-stock $sku $qty]]
           [effects [write "orders"]]
           …body…]

         [$ux:form place-order]   [; param schema → a complete form ]
    

Rules are enforced once, at the command layer — a refused precondition renders as a form error from the [err] channel; the projection never restates a rule, so a rule can never be enforced differently in two places. Where the tools projection fails loud on a missing [summary] (agent-facing text is load-bearing), the ux projection degrades to name-derived labels — human-facing defaults are presentational, and quality gates measure them rather than refusals.

A closed semantic vocabulary

Projection emits semantic elementsux:card, ux:field, ux:table, ux:action, ux:facet, ux:status, forty-five members in all — that assume no hover, no pointer, and no CSS-shaped layout. The set is closed and gate-enforced: an unknown member refuses (ux-unknown-element), and so does an unknown attribute on a known member (ux-unknown-attr) — an invented spelling gets a loud error, never silent acceptance. A candidate member is admitted only when it carries semantics no existing member carries and every face can honor it natively — "the same thing with a different appearance" becomes a hint, never a member.

      [ux:card
           [ux:media src="/static/art/pan.svg" alt="A 24 cm sauté pan in graphite" kind=illustration]
           [ux:heading 'Farrow 24 cm sauté pan']
           [ux:field label='Price' format=money 68.00]
           [ux:action verb=add-to-cart label='Add to cart'
             [ux:param name=sku value='FK-0119']]]
    

That value is the whole interface contract for a product card. Notice what it does not say: no widths, no colors, no columns, no click handlers. Presentation intent that varies by deployment rides hint claims keyed by (schema address, field path) — view=cards, format=money, grouping, ordering, labels per language — resolved through a four-level cascade (platform → vendor → tenant → surface), so a tenant can restyle a field everywhere it appears without touching a definition.

Two faces, one tree

Renderers lower the same semantic tree and neither is privileged. The web face lowers to HTML over a pinned hypermedia kernel — the emitter is the sole author of every kernel attribute, escaping is by construction (tree composition, never string concatenation), and strict CSP holds because the emitter never writes inline styles, scripts, or handler attributes. The terminal face lowers the same tree to a measured, keyboard-driven TUI. The pairing is load-bearing for quality: a face with no pointer cannot hide an unreachable control, so the terminal's focus ring enumerating the same affordances the web face offers is a mechanical keyboard-reachability test — accessibility as a fixture, not a review comment.

Writes are intents; reads are places

Every state change is a POST of an intent to the composed grammar's verb — the same wire agents use, with the same policy decision, the same journal append, the same refusal channel. An [ux:action] inside a collection item carries its parameters server-derived at projection time; nothing is read back out of the rendered document, so a stale page posts what it was rendered with and the command layer refuses it honestly. And every meaningful state is a URL: a facet selection, a sort, a page, a checkout step, an open basket. GET is safe and idempotent, mutations never hide behind links, and with the kernel absent every control still works as a plain form or link — full page loads instead of fragment swaps, and nothing else lost.

What is shown is what is allowed

The emitter consults the same authorization evaluator as the wire and the tools face, per session, at render time — being shown and being allowed are one computation. Commands the actor cannot invoke are not emitted; unreadable fields are omitted by default (never computed into the output at all) or masked server-side where discoverability is wanted; reading past a lens is a separate journaled command, not a rendering variant. A field masked on the web is masked in a tool result — one ruling, all surfaces.

Where it is specified

The capability's normative home is the ux spec in the XAP spec set — the projection vocabulary, the keying regimes, the emitter contract, the session/authz rulings, and the accessibility clauses, each fixture-mapped. The semantic core lives in the cx-x/ux module with the web and terminal faces beside it, and the conformance lane exercises projection, addressing, the refusal gate, and redaction against goldens. The pack is x-tier: bundled and gated in-tree, with its stability promise deliberately unfrozen until its go/kill review passes.