📁 The anatomy
<name>.json with its root — one face, no manifest, no layouts/, no states/. Simplicity is the default; structure is earned.
Components live in TWO tiers (same anatomy in both):
Names are unique across the design system and every org — a collision is a lint error (no shadowing). Both address forms are first-class — the bare URI is the canonical address for a design-system component, the org URI for an org component; uniqueness means a bare ref also resolves an org component unambiguously. Direction rule: org things may reference design-system things; design-system things never reference org things (lint-enforced, including template preview lists).
- File lookup is case-insensitive by filename;
nameis the display/type name. components/vs atoms: a shape shared across this component’s states → its owncomponents/($include). A shape shared across many components (a close button, a choice row) → a universal atom inrx/atoms/(Ref). Used once → inline it. Atoms are authoring-time only: the server always expands them before serving (channels only ever receive fully-expanded primitive trees; atoms are never served, never enumerable, and have no Studio view). ARef’spropsremaps fields; itswithpasses literals into the atom —{ "type": "Ref", "ref": "button", "with": { "label": "Learn more", "icon": "arrowRight" }, "action": { … } }hardcodes those attributes, and a truthywithkey satisfies (drops) a matchingvisibleWhenguard, so unprovided pieces stay hidden.
📜 The manifest — the render contract
defaultState= the state the component arrives in. An open name (inline,focused,product, anything). The server injects it into the component’s scope, so it renders that face the moment it streams in. Default:inline.lifetime(OPTIONAL) = how long the rendered instance survives. Default"turn": the universal reset — the instance returns to inline / retires on the next user turn (04 §Two lifetimes)."conversation": a durable, conversation-scoped surface (a cart, an itinerary, a composed page) — the platform keys the instance by the conversation (every re-call hydrates the SAME slice: merge, never re-place) and the new-turn reset skips it; it stays on screen until replaced, self-closed, or a new template loads (the template swap is the hard refresh boundary). Closed setturn | conversation, lint-checked.- Manifest presence = spatially discoverable. The discovery meta (
title/description/whenToUse) lives here and ONLY here — never duplicated in the envelope. A component that’s only ever streamed by a workflow, arrives inline, and needs no discovery can skip the manifest entirely. whenToUseis utterance-shaped — the words a user would say (“find the right product for me”), never selector-shaped dev framing (“use this when the user asks…”).- Naming is discoverability (05 §Naming — canonical:
docs/nodes/14-node-discoverability.md). Spatial embeds`title. whenToUse||description [category]`and ranks it against the user’s own words:title= the thing itself (no mechanism, no org prefix),description= one ≤120-char line of what it IS,whenToUse’s opening words carry the ranking,category= the job’s domain. Disqualify by property, never by naming a sibling.
🏠 Three homes — everything the component shows
Anything else is slop, and the linter rejects it. The tell: an array, object, or URL in the
state block is never view-state — it’s content (→ hardcode) or data (→ input: true prop). A contained microapp usually has an empty or absent props block.
🔑 Prop names are the data contract — use the writer’s names, never invent
How workflow data reaches a prop: the source object (a content row, a node’s output) is seeded into the component’s state as-is — by name, no projection, no mapping layer. Everybind looks its value up BY NAME; a name the source doesn’t carry silently renders
the preview default instead (the classic tell: title/description stream correctly while
the image and tagline stay stuck on mocks). If a bind misses, rename the component’s
prop to the source’s field name — never add mapping glue.
For content-attached cards (a row with metadata.app hydrating your card), the field
vocabulary is the content writer’s, and it is fixed:
❌
image, imageUrl, photo, subtitle, category, location — inventions; the bind
misses and the mock leaks into production. The canonical row shape and the guard live in
server/src/runtime/content-card-hydration.test.ts — it walks every layout of every
attachable component and fails the build on a bind the row can’t satisfy. Deep law:
UNOVERSE_MCP_TEMPLATE_PROTOCOL.md §Content-attached cards.
🎭 The faces — root switches on defaultState
Every faced component’s root is the same three lines: a Switch on defaultState whose cases $include a layout named after the state:
- The layout filename = the state name. A custom arrival state
productgetslayouts/product.json— no special-casing, any open name works. - The face is a state decision, never a width decision. The same
defaultStatewrite that makes a template’s surface react also flips the face. Container queries (hideBelow) are for fine adjustments inside a face, never for picking one. - A
hideBelowthreshold must be reachable by the card itself — keep it below the layout’s ownmaxWidth. A threshold at or above it can only be satisfied by the surrounding surface, so the element shows on a wide Studio stage and silently vanishes in a chat column (linted). - The component’s own buttons move it: expand =
setValue { defaultState: "focused" }, its focused face carries its own ✕ that sets it back to"inline". A component writes only its own slice — how templates react is 04 — State.
✍️ Briefed components — the design briefs the AI
A brief is metadata that tells an AI what should fill a bound element. It sits on the node that renders what it describes — next to thebind it governs, never in a separate file or the manifest:
- Shape (linted, closed): a string (just the description) or
{ description, maxLength }on a bound element,{ description, minItems, maxItems }on anEach— JSON Schema’s own vocabulary, because the brief IS the schema fragment it compiles to. A brief on a node with no bind (a face or partial root) is composition context — rules about the whole, like ordering or refinement behavior. - What it becomes — this is MCP-native, no side-channel: the platform compiles every brief into the component’s MCP tool schema (each key passes through verbatim —
description,maxLength,minItems,maxItemsare native JSON Schema, the Each’s template binds → the array’sitemsschema). An agent that discovers the component sees a rich, required schema — so it must gather real content (Spatial search) and hydrate the fields before it can render. The hydrated call’s values flow back in as the component’s state. The schema IS the instruction channel; there is no prompt to maintain anywhere else. - Grounding is part of the compiled contract: fields are filled only from search results in the conversation — never invented. The compiler injects this law into every briefed schema.
- The server referees and mirrors: invalid/empty compositions are rejected with an instructive result before anything renders (the agent self-corrects and retries); a successful render returns the page as the guest sees it in the tool result, so the agent refines surgically on later turns (“more golf” knows which section to swap).
- The single-face pattern pairs naturally: a continuously-enriched page (one named face +
default→ the same face, noinline, no ✕) arrives in its surface, can never leave it, and each refinement turn merges new data into the same instance. - Any number of briefed components can be live at once — each compiles to its own tool.
🧩 Private steps — states/ + stateOrder
A wizard’s questions are the component’s own states: one file per step in states/, listed in the envelope’s stateOrder (the exact set, in order — Studio’s state picker and the mock walk use it):
items on an Each; picking an option writes the answer + the next step in one setValue:
Switch (or the step Switch) already selects it; a visibleWhen re-checking the same discriminant inside a case is an error.
📋 Component checklist
- Flat if it can be — structure (
layouts/,states/, manifest) is earned - Manifest = render contract: arrival
defaultState(open name) + discovery meta (no envelope duplicates) - Root =
Switch on defaultState→layouts/<state>(filename = state name);defaultcase → inline - Three homes respected: content hardcoded ·
stateblock scalar view-state only ·propsallinput: true -
stateOrdernames exactly thestates/files - Publish from Studio passes lint with 0 errors — every rule above is enforced
Next: 04 — State — how components and templates interact.

