@unoverse-platform/studio 0.1.26 → 0.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/package.json +1 -1
  2. package/screens/AtomsView.tsx +40 -2
  3. package/screens/ComponentsView.tsx +112 -21
  4. package/screens/NativeAppHost.tsx +8 -4
  5. package/screens/TemplatesView.tsx +51 -21
  6. package/screens/states.ts +27 -4
  7. package/vendor/sdk/appEngine.tsx +20 -4
  8. package/vendor/sdk/lib/UnoverseComponent.tsx +8 -2
  9. package/vendor/sdk/lib/core/actions.ts +10 -6
  10. package/vendor/sdk/lib/core/client.ts +39 -12
  11. package/vendor/sdk/lib/core/index.ts +21 -0
  12. package/vendor/sdk/lib/core/refs.ts +117 -0
  13. package/vendor/sdk/lib/core/store.ts +65 -38
  14. package/vendor/sdk/lib/core/templates.ts +270 -0
  15. package/vendor/sdk/lib/core/types.ts +27 -7
  16. package/vendor/sdk/lib/orb/ThinkingOrb.tsx +131 -0
  17. package/vendor/sdk/lib/orb/engine/braid.ts +50 -0
  18. package/vendor/sdk/lib/orb/engine/core.ts +167 -0
  19. package/vendor/sdk/lib/orb/engine/lattice.ts +205 -0
  20. package/vendor/sdk/lib/orb/engine/morph.ts +138 -0
  21. package/vendor/sdk/lib/orb/engine/orbits.ts +80 -0
  22. package/vendor/sdk/lib/orb/engine/profiles.ts +162 -0
  23. package/vendor/sdk/lib/orb/engine/registry.ts +31 -0
  24. package/vendor/sdk/lib/orb/engine/ribbon.ts +93 -0
  25. package/vendor/sdk/lib/orb/engine/rings.ts +239 -0
  26. package/vendor/sdk/lib/orb/engine/types.ts +14 -0
  27. package/vendor/sdk/lib/orb/engine/web.ts +98 -0
  28. package/vendor/sdk/lib/orb/index.ts +20 -0
  29. package/vendor/sdk/lib/orb/presets.ts +181 -0
  30. package/vendor/sdk/lib/orb/theme.ts +83 -0
  31. package/vendor/sdk/lib/orb/types.ts +84 -0
  32. package/vendor/sdk/lib/primitives.tsx +116 -2
  33. package/vendor/sdk/lib/render.tsx +32 -8
  34. package/vendor/sdk/lib/style.ts +6 -2
  35. package/vendor/sdk/lib/template.tsx +86 -224
  36. package/vendor/skills/unoverse-create/SKILL.md +18 -18
  37. package/vendor/skills/unoverse-create/references/agent-skill.md +12 -12
  38. package/vendor/skills/unoverse-create/references/block.md +5 -5
  39. package/vendor/skills/unoverse-create/references/component.md +150 -75
  40. package/vendor/skills/unoverse-create/references/node.md +1 -1
  41. package/vendor/skills/unoverse-create/references/template.md +142 -82
  42. package/vendor/skills/unoverse-create/references/workflow.md +15 -15
@@ -1,116 +1,186 @@
1
- # Playbook — Components & Atoms (rx data)
1
+ # Playbook: Components & Atoms (rx data)
2
2
 
3
3
  **Read first:** `docs/design/components.md` (the microapp anatomy) + `docs/design/state.md`
4
- (the reaction contract). Deep law: [UNOVERSE_AUTHORING](https://github.com/unoverse-platform/docs/blob/main/unoverse/UNOVERSE_AUTHORING.md) §3 (three homes),
5
- `UNOVERSE_STATE_MODEL.md` §5b. **Study the exemplars before writing**: `rx/components/journeyfinder`
6
- and cardfinder — mirror them exactly.
4
+ (the state model). Deep law: [UNOVERSE_AUTHORING](https://github.com/unoverse-platform/docs/blob/main/unoverse/UNOVERSE_AUTHORING.md) §3 (three homes),
5
+ `UNOVERSE_STATE_MODEL.md` §5 (the six rules), `UNOVERSE_LAYERS.md` (states own layouts).
6
+ **Study the exemplar before writing**: `rx/sab/components/product-card` is the canonical
7
+ State Model v2 component; mirror it exactly.
7
8
 
8
9
  ## The anatomy
9
10
 
10
11
  ```
11
- apps/unoverse/rx/components/<name>/ # DESIGN SYSTEM tier (generic, universal)
12
- apps/unoverse/rx/orgs/<org>/components/<name>/ # ORG tier (the client's own microapp, org-private)
13
- <name>.json # envelope: name/category/nodeSize/outputs/props/state/stateOrder/root
14
- manifest.json # RENDER CONTRACT: arrival defaultState (open name, default "inline")
15
- # + discovery meta (title / description ≤120 / whenToUse utterance-shaped).
16
- # Presence = spatially discoverable; envelope NEVER duplicates the meta.
17
- layouts/ # the FACES — filename = state name: inline.json, focused.json, (custom).json
18
- states/ # the component's PRIVATE steps (wizard questions) — listed in stateOrder
19
- components/ # component-local shared partials (earned: 2+ states share a shape)
12
+ apps/unoverse/rx/<project>/components/<name>/
13
+ <name>.yaml # ROOT: envelope (name/category/nodeSize/outputs/props) + the STATE TREE + root
14
+ manifest.yaml # RENDER CONTRACT + discovery meta (title / description ≤120 /
15
+ # whenToUse utterance-shaped). Presence = spatially discoverable;
16
+ # the envelope NEVER duplicates the meta.
17
+ layouts/ # the DRAWINGS: one file per state that owns an arrangement
18
+ components/ # component-local shared partials (earned: 2+ layouts share a shape)
20
19
  ```
21
20
 
22
- A **flat component** (simple card/chart) is just `<name>.json` + `root` — one face, no manifest,
23
- no folders. Structure is EARNED; start flat.
21
+ A **flat component** (simple card/chart) is just `<name>.yaml` + `root`: one drawing, no
22
+ manifest, no folders. Structure is EARNED; start flat.
24
23
 
25
- ## The rules (Studio's publish lint enforces all of these — 0 errors required)
24
+ ## The state model (v2): data + a state tree
25
+
26
+ A component is **data plus a state tree**; each state owns the layout(s) that draw it.
27
+ The root's `state:` block IS the design, readable top to bottom. The canonical shape
28
+ (`rx/sab/components/product-card/product-card.yaml`):
29
+
30
+ ```yaml
31
+ state:
32
+ view: # the PUBLIC axis
33
+ initial: products
34
+ states:
35
+ products: {} # no layout key: same-name default, {} is complete
36
+ detail: # the full page
37
+ layout: product # declared ONLY because the filename differs
38
+ on: step # a PRIVATE axis: the steps inside detail
39
+ initial: detail
40
+ states:
41
+ detail: { layout: product-detail }
42
+ apply: { layout: product-apply }
43
+ root:
44
+ type: Box
45
+ children:
46
+ - type: Switch
47
+ on: view # the root switches on `view`, ONE case active
48
+ cases:
49
+ products: { $include: layouts/products }
50
+ detail: { $include: layouts/product }
51
+ ```
52
+
53
+ The rules, in the order they bite:
54
+
55
+ - **The axis is `view`.** The component's public state lives in its slice under `view`.
56
+ The root is a `Switch on view`; buttons transition with
57
+ `setValue { values: [{ key: view, value: detail }] }`. That write is the ENTIRE
58
+ transition machinery: no guards, no events, no per-component machines.
59
+ - **Public states are the top level of the `view` axis** and they are the template's
60
+ WHOLE vocabulary: everything templates can react to, nothing more. Steps nest
61
+ privately (`on: <key>` + `initial` + `states`), invisible outside the component.
62
+ The placement rule: **if the template must rearrange to show it, make it public;
63
+ if the template wouldn't move, nest it privately.** Privacy is the default;
64
+ promotion is deliberate.
65
+ - **Each state owns its layout(s).** `layout:` is OPTIONAL: a state with no declaration
66
+ draws the layout of its own name, and `{}` is a complete state. Write `layout:` only
67
+ when the filename differs. A layout is never addressable on its own; only its owning
68
+ state selects it. Name states for what they MEAN (`detail`), never for their drawing.
69
+ - **Shell vs steps.** A state's own layout is its SHELL, always on while the state is
70
+ active. Its nested substates are the STEPS, the only choices inside it, addressed by
71
+ state names (layout filenames are plumbing).
72
+ - **Variants**: one meaning, several arrangements:
73
+ `focused: { layouts: { horizontal: focused-h, vertical: focused-v } }`. Variants bind
74
+ the SAME fields; anything that changes meaning or bound data is a second state
75
+ (lint-enforced). The component picks its variant by the space it finds itself in
76
+ (container queries); templates influence it only by controlling the container.
77
+ - **Arrival: nobody sends a state.** The host template places the spawned actor: it
78
+ honors the component's declared `initial` when it has a matching state; otherwise it
79
+ scans its own states in declared order and takes the first name on the component's
80
+ public menu; no overlap and the component wakes in its own initial chain and renders
81
+ inline. The server compiles the tree to the public menu + initials it serves
82
+ (`publicStates`, `initialView`). Post-spawn, the template NEVER writes a component's
83
+ state.
84
+ - **Unmatched view = inline. Always.** An ignored public state does not stay put, it
85
+ FALLS INLINE in the flow. A step that must never move the page belongs in the nest,
86
+ not on the public menu.
87
+ - Legacy: v1's `defaultState` field and layout-name-sync are superseded; the SDK reads
88
+ `defaultState` only as a migration alias for `view`. New authoring writes `view`.
89
+
90
+ ## The rules (Studio's publish lint enforces all of these; 0 errors required)
26
91
 
27
92
  1. **Three homes for everything it shows** (AUTHORING §3):
28
93
  - static content → **hardcoded literals** in the layout (`value`, literal `items: []` on
29
- Each, `src`) — never props, never `state`;
94
+ Each, `src`), never props, never `state`;
30
95
  - internal view-state → the **`state` block, SCALARS ONLY** (`step`, `phase`,
31
- `progressPct`) — an array/object/URL in `state` is slop (workflow data → props;
96
+ `progressPct`): an array/object/URL in `state` is slop (workflow data → props;
32
97
  static → hardcode);
33
98
  - workflow-fed data → **`props` with `input: true`** (the `default` is the preview mock).
34
99
  Usually empty.
35
- - **Prop names = the data contract — never invent them.** Source data hydrates props
100
+ - **Prop names = the data contract. Never invent them.** Source data hydrates props
36
101
  BY NAME (as-is, no mapping); a name the source doesn't carry silently renders the
37
102
  preview default (tell: title streams, image stays mock). Content-attached cards use
38
103
  the writer vocabulary: `title` `tagline` `description` `bodyCopy` `introParagraph`
39
104
  `primaryImage` `images` `link` `callToAction`. Guard:
40
105
  `server/src/runtime/content-card-hydration.test.ts`; law:
41
106
  `UNOVERSE_MCP_TEMPLATE_PROTOCOL.md` §Content-attached cards.
42
- 2. **Root = `Switch on defaultState`** — the component is a switch of **views**, ONE active.
43
- Cases `$include layouts/<state>` (filename = view name); `default` → the inline layout.
44
- Arrival view = **manifest.defaultState**. Keep an `inline` case for any component that
45
- lives in a conversation flow — it is its placeholder there. EXCEPTION — the
46
- **single-face (permanent) component**: exactly ONE named view + `default` pointing at
47
- the SAME face, NO inline. It arrives in its surface, cannot leave it (no ✕, no other
48
- view), and is enriched in place via data merges to one stable instance — e.g. a
49
- continuously-updated page living in a template's focus surface. Permanence is by
50
- construction, never by a pinning rule.
51
- **Lifetime (OPTIONAL manifest flag)** — `"lifetime": "conversation"` marks a durable,
107
+ 2. **Root = `Switch on view`**, cases per PUBLIC state, each `$include` of the state's
108
+ layout (the tree above). Keep an `inline` state for any component that lives in a
109
+ conversation flow: the flow is its placeholder there. One EXCEPTION is the
110
+ **single-state (permanent) component**: exactly ONE public state, NO inline. It
111
+ arrives in its surface, cannot leave it (no ✕, no other state), and is enriched in
112
+ place via data merges to one stable instance, e.g. a continuously-updated page
113
+ living in a template's focus state. Permanence is by construction, never by a
114
+ pinning rule.
115
+ **Lifetime (OPTIONAL manifest flag).** `"lifetime": "conversation"` marks a durable,
52
116
  conversation-scoped surface (a cart, an itinerary, a composed page): the platform keys
53
- its instance by the CONVERSATION (every re-call hydrates the SAME slice — merge, not
117
+ its instance by the CONVERSATION (every re-call hydrates the SAME slice: merge, not
54
118
  re-place) and the new-turn reset skips it, so it stays on screen until it is replaced,
55
119
  closes itself, or a NEW TEMPLATE loads (the template swap is the hard refresh boundary).
56
- Default `"turn"`: the instance returns to inline / retires on the next user turn.
57
- Values are a closed set — `turn | conversation` (lint-checked; docs/design/04).
58
- 3. **State is local; the VIEW is the interface** (STATE_MODEL §5b). Internal `state`
59
- (`step`, `phase`, …) is the component's own business — the template never sees it. The
60
- only thing that crosses to the template is the **active view** (`defaultState`). The
61
- component writes ONLY its own slice (`setValue`): expand = `setValue { defaultState:
62
- "course" }`; its expanded face carries its own ✕ back to `"inline"`. When a template has
63
- a surface for that view, the instance **lifts out of the flow into the surface** — the
64
- SDK renders each instance in exactly ONE placeholder, so you never hide a flow copy
65
- yourself (no `hideBelow`, no overlay hack). ❌ NEVER `setTemplateValue` to open a surface
66
- (the deprecated bridge — linted).
67
- 4. **`stateOrder`** names exactly the `states/*.json` files, in order.
68
- 5. **Atoms** (`rx/atoms/`, via `Ref`) are for shapes shared across components — authoring-time
120
+ Durable slices also opt out of `cancelBelow` retraction when a higher template state
121
+ takes over. Default `"turn"`: the instance returns to inline / retires on the next
122
+ user turn. Values are a closed set: `turn | conversation` (lint-checked; docs/design/04).
123
+ 3. **State is local; the `view` is the interface** (STATE_MODEL §5). Private axes
124
+ (`step`, `phase`, …) are the component's own business; the template never sees them.
125
+ The only thing that crosses is the public `view`. The component writes ONLY its own
126
+ slice (`setValue`): expand = `setValue { view: "detail" }`; its expanded layout
127
+ carries its own ✕ back down. When a hosting template has a state of that name, the
128
+ instance **lifts out of the flow into that state's slot**. The SDK renders each
129
+ instance in exactly ONE placeholder, so you never hide a flow copy yourself (no
130
+ `hideBelow`, no overlay hack). ❌ NEVER `setTemplateValue` to open a surface
131
+ (the deprecated bridge, linted). ❌ Never guard a layout against its own state:
132
+ the tree owns "which state is active"; a layout re-checking its discriminant is
133
+ the double-guard smell the model removes.
134
+ 4. **A new layout file is earned only by a different arrangement** (or a different set
135
+ of bound fields). Same shape, different values = ONE state, data-driven: seven wizard
136
+ questions sharing one shape are one `question` state whose data changes, never seven
137
+ files (LAYERS §4, §7).
138
+ 5. **Atoms** (`rx/atoms/`, via `Ref`) are for shapes shared across components, authoring-time
69
139
  only (the server expands them before serving; no Studio view). `Ref props` remaps FIELDS;
70
140
  **`Ref with` passes LITERALS**: `{ "type": "Ref", "ref": "button", "with": { "label":
71
- "Learn more", "icon": "arrowRight" }, "action": { … } }` — a bind whose field is a `with`
141
+ "Learn more", "icon": "arrowRight" }, "action": { … } }`. A bind whose field is a `with`
72
142
  key becomes a hardcoded attribute, a truthy `with` key drops a matching `visibleWhen`
73
143
  guard (unprovided ⇒ that piece stays hidden), `{{key}}` style bindings take the value.
74
144
  ⚠ Icon: literal = `icon: "phone"`; bound = `bind: { name: field }`.
75
- 6. **Briefs — components an AI fills** (docs/design/03 §Briefed components; deep law: [UNOVERSE_AUTHORING](https://github.com/unoverse-platform/docs/blob/main/unoverse/UNOVERSE_AUTHORING.md) §3b). A `brief`
76
- sits ON the node that renders what it describes, next to its `bind` — never in the
145
+ 6. **Briefs: components an AI fills** (docs/design/03 §Briefed components; deep law: [UNOVERSE_AUTHORING](https://github.com/unoverse-platform/docs/blob/main/unoverse/UNOVERSE_AUTHORING.md) §3b). A `brief`
146
+ sits ON the node that renders what it describes, next to its `bind`, never in the
77
147
  manifest, never a separate file:
78
148
  - bound element → `"brief": { "description": "…", "maxLength": 60 }`
79
149
  - Each → `"brief": { "description": "…", "minItems": 3, "maxItems": 5 }` (its template's
80
- binds define the item shape — brief those fields too)
81
- - face/partial ROOT (no bind) → a plain-string brief = composition context
150
+ binds define the item shape; brief those fields too)
151
+ - layout/partial ROOT (no bind) → a plain-string brief = composition context
82
152
  (ordering, refinement rules like "update, never discard").
83
153
  The platform compiles briefs into the component's MCP tool schema (keys pass through
84
- verbatim — the brief uses JSON Schema's OWN vocabulary) and injects the grounding law (fill ONLY from
85
- spatial search results — never invent). The server REFEREES (invalid/empty args →
154
+ verbatim; the brief uses JSON Schema's OWN vocabulary) and injects the grounding law (fill ONLY from
155
+ spatial search results, never invent). The server REFEREES (invalid/empty args →
86
156
  instructive result, no render) and MIRRORS (success result returns the rendered page
87
- for surgical refinement). **The schema IS the instruction channel** —
88
- write briefs, never side-channel prompts/skills for filling a component. Shape is
89
- closed + linted. Pairs with the single-face permanent pattern (rule 2) for
157
+ for surgical refinement). **The schema IS the instruction channel.**
158
+ Write briefs, never side-channel prompts/skills for filling a component. Shape is
159
+ closed + linted. Pairs with the single-state permanent pattern (rule 2) for
90
160
  continuously-enriched pages; exemplar: `rx/components/perfectday`.
91
161
  7. **Tokens only** (LAW 1) + closed style keys + real space-scale steps
92
- (`docs/design/06`) — an invented step is silently broken CSS.
93
- 8. **Lifecycle handlers — a component's own server-side code** (deep law:
162
+ (`docs/design/06`): an invented step is silently broken CSS.
163
+ 8. **Lifecycle handlers: a component's own server-side code** (deep law:
94
164
  [UNOVERSE_AUTHORING](https://github.com/unoverse-platform/docs/blob/main/unoverse/UNOVERSE_AUTHORING.md) §3c). When a component needs live data from an
95
165
  API or a computation (not from an AI = brief, not from the workflow = `input` props), it
96
166
  may bring a small server-side script that runs when the component renders. It's for DATA,
97
167
  not styling or logic-in-the-def.
98
168
  - **Opt in via the manifest** (authorizes execution; auditable): `"lifecycle":
99
- ["onStart"]` in `manifest.json`. A handler file with no opt-in is INERT.
100
- - **The code is a sibling file named for the lifecycle** — `onStart` → `onstart.js`,
101
- beside `<name>.json`. Its **default export** is the handler.
169
+ ["onStart"]` in the manifest. A handler file with no opt-in is INERT.
170
+ - **The code is a sibling file named for the lifecycle**: `onStart` → `onstart.js`,
171
+ beside the root file. Its **default export** is the handler.
102
172
  - **`onStart`** is the only lifecycle today; it runs SERVER-SIDE when the instance is
103
173
  created (the MCP render path), and whatever it returns is **merged into the instance
104
- by prop name** (unknown keys inert — same contract as workflow-fed props). Return only
174
+ by prop name** (unknown keys inert, same contract as workflow-fed props). Return only
105
175
  the fields you're filling; **never overwrite a curated prop** you didn't intend to.
106
176
  - **`ctx` is a bounded sandbox**: `ctx.props` (the instance's current data),
107
177
  `ctx.instanceId`, `ctx.env(name)` (server config). The **API key comes from
108
- `ctx.env`, NEVER hardcoded** — `rx/` is a shared design folder; a secret in it leaks.
178
+ `ctx.env`, NEVER hardcoded**: `rx/` is a shared design folder; a secret in it leaks.
109
179
  - Self-contained (the external call lives in the handler); errors never break the render.
110
- Exemplar: `rx/orgs/yasisland/components/restaurantcard/onstart.js` (Google Maps details).
180
+ Exemplar: `rx/yasisland/components/restaurantcard/onstart.js` (Google Maps details).
111
181
 
112
182
  ```js
113
- // onstart.js — manifest opts in with "lifecycle": ["onStart"]
183
+ // onstart.js: manifest opts in with "lifecycle": ["onStart"]
114
184
  export default async function onStart(ctx) {
115
185
  const key = ctx.env("SEARCHAPI_KEY"); // secret from server config
116
186
  const res = await fetch(`https://…&q=${encodeURIComponent(ctx.props.title)}&api_key=${key}`);
@@ -119,34 +189,39 @@ no folders. Structure is EARNED; start flat.
119
189
  }
120
190
  ```
121
191
 
122
- ## Discovery meta — write it to RANK (docs/nodes/node-discoverability.md)
192
+ ## Discovery meta: write it to RANK (docs/nodes/node-discoverability.md)
123
193
 
124
194
  A component with a manifest is an **MCP app**: spatial embeds `` `title. whenToUse||description
125
195
  [category]` `` and ranks it against the USER'S OWN WORDS (`findIntent`). The meta is the
126
- selection text, not documentation — weak meta makes a working component invisible. The
196
+ selection text, not documentation. Weak meta makes a working component invisible. The
127
197
  strict contract (doc 14, applies verbatim):
128
198
 
129
- - **`title`** = the thing itself, human display name — no mechanism, no org prefix.
130
- - **`description`** = what it IS, ONE line ≤120 chars — the listing subtitle; never
199
+ - **`title`** = the thing itself, human display name, no mechanism, no org prefix.
200
+ - **`description`** = what it IS, ONE line ≤120 chars, the listing subtitle; never
131
201
  "use when…" inside it.
132
202
  - **`whenToUse`** = the selection text, **utterance-shaped**: the words a user would
133
- actually say, outcome first — the OPENING words dominate the embedding. Never
203
+ actually say, outcome first; the OPENING words dominate the embedding. Never
134
204
  selector-shaped dev framing ("Pick when the user asks…"), never layout/mechanism first
135
- ("A single-face page that…" ranks near rendering concepts and loses every intent query).
205
+ ("A single-state page that…" ranks near rendering concepts and loses every intent query).
136
206
  - **Disqualify by PROPERTY, never by naming a sibling** ("Not for answering a single
137
- question, not for one item on its own") — naming rivals dates and tangles; the ranking
207
+ question, not for one item on its own"). Naming rivals dates and tangles; the ranking
138
208
  surfaces the alternative. A fallback/home surface never enumerates siblings' jobs
139
209
  (the generalist trap).
140
210
  - **`category`** = the domain of the JOB (Travel, Payments…), not the implementation.
141
211
  - Meta embeds **AS-IS** (no LLM rewrite); editing `whenToUse` re-embeds on the next train.
142
212
 
143
213
  Self-test before shipping: write the sentence the user would actually say to want this
144
- component — its key nouns/verbs must appear in `whenToUse`'s first sentence.
214
+ component. Its key nouns/verbs must appear in `whenToUse`'s first sentence.
145
215
 
146
216
  ## Workflow
147
217
 
148
- 1. Study the closest exemplar; copy its folder shape.
149
- 2. Write the envelope + manifest + layouts; put every shown thing in its ONE home.
150
- 3. Publish from Studio — its lint must report 0 errors (it cites the doc for every rule).
151
- 4. Restart unoverse → Studio: mock (prop defaults + state picker + Inline/Focused
152
- toggle), then live. Debug: stream log → state inspector → definition; never guess.
218
+ 1. Study `rx/sab/components/product-card`; copy its folder shape.
219
+ 2. Declare the state tree first: public states on top, steps nested, each state owning
220
+ its layout (same-name default, `layout:` only when the filename differs).
221
+ 3. Write the envelope + manifest + layouts; root = `Switch on view`; every shown thing
222
+ in its ONE home; transitions are `setValue` writes to `view` (or a private axis).
223
+ 4. Publish from Studio; its lint must report 0 errors (the rules are v2-aware and cite
224
+ the doc for every rule).
225
+ 5. Restart unoverse → Studio: mock (prop defaults + the state switcher reads the tree:
226
+ public states on top, steps beneath by state name), then live. Debug: stream log →
227
+ state inspector → definition; never guess.
@@ -161,7 +161,7 @@ the manifest describes the real service rather than something merely coherent.
161
161
  returns: "return response.items.map(helpers.row)"
162
162
  ```
163
163
 
164
- A helper sees ONLY its arguments and its sibling helpers — never `config`, `credentials`
164
+ A helper sees ONLY its arguments and its sibling helpers, never `config`, `credentials`
165
165
  or the caller's scope. Same sandbox, no extra authority.
166
166
 
167
167
  ## Do not
@@ -1,115 +1,175 @@
1
- # Playbook — Templates (MCP apps)
1
+ # Playbook: Templates (MCP apps)
2
2
 
3
3
  **Read first:** `docs/design/templates.md` (layouts + manifest-as-envelope) +
4
- `docs/design/state.md` (the reaction contract + name-sync). Deep law:
5
- `UNOVERSE_STATE_MODEL.md` §5b, `UNOVERSE_LAYERS.md`. Everything in the component
6
- playbook (closed vocabulary, tokens, scale steps) applies here too.
7
-
8
- ## THE model — layouts sync by name
9
-
10
- A component is a Switch of **faces**; a template is a set of **layouts** — same
11
- mechanism, two tiers, connected by ONE rule:
12
-
13
- > A component enters a view → the template presents the layout of the same name.
14
- > No matching layout → the default layout; the component renders inline.
15
-
16
- Derived off the store, never stored, no wiring — the component never knows templates
17
- exist. **Rule of thumb: component causes it = layout; template knows it = state.**
4
+ `docs/design/state.md` (the state model). Deep law: `UNOVERSE_STATE_MODEL.md` §5
5
+ (the six rules), `UNOVERSE_LAYERS.md`. Everything in the component playbook (closed
6
+ vocabulary, tokens, scale steps) applies here too. **Study the exemplar before
7
+ writing**: `rx/sab/templates/sab-chat-layout` is the canonical State Model v2 template.
8
+
9
+ ## THE model: the component drives; the template reacts by name
10
+
11
+ A component is a state tree whose public states are its whole interface; a template
12
+ declares its OWN state tree in the manifest and reacts to components by name:
13
+
14
+ > A component enters a public view → the template asks "do I have a state with that
15
+ > name?" Match: the template enters that state and its layout draws. No match: the
16
+ > component renders inline in the flow. Always.
17
+
18
+ Derived off the store, never stored, no wiring. The component never knows templates
19
+ exist, and post-spawn the template NEVER writes a component's state. Name-match is the
20
+ default convention (`reactsTo:` on a template state is the rare escape hatch for
21
+ vocabulary mismatches). **Rule of thumb: component causes it = a reaction state;
22
+ template knows it = a base substate or template state.**
23
+
24
+ ## The manifest states tree: one declaration, everything follows
25
+
26
+ The canonical shape (`rx/sab/templates/sab-chat-layout/manifest.yaml`):
27
+
28
+ ```yaml
29
+ states:
30
+ main: # the base arrangement, FIRST
31
+ states:
32
+ welcome: {} # contained substate; first substate = the landing default
33
+ focus: {} # then the reaction ladder, in priority order
34
+ products: {}
35
+ detail: {}
36
+ ```
18
37
 
19
- ## The anatomy — manifest-only (NO `<name>.json` envelope)
38
+ - **Top-level order IS the priority ladder**, base first. The template can be in ONE
39
+ state at a time: it walks the list top-down and enters the first of its states that
40
+ ANY hosted component currently matches. The ladder is DERIVED from the tree; never
41
+ author `stateOrder` (that field survives only as the legacy form for templates
42
+ without a tree).
43
+ - **Nesting IS containment, enforced by compilation.** A declared base substate
44
+ (`welcome`) is stripped from every non-base arrangement; no hand-written guard ever
45
+ polices it. The substate's file keeps its own condition (`visibleWhen: isEmpty`).
46
+ - **The shell rule.** Base content drawn in every arrangement is NOT declared as a
47
+ substate. sab's conversation timeline is main's own shell, included by the shared
48
+ chat chrome and legitimately drawn beside the rail, the page and under focus;
49
+ declaring it would contain it to `main` and empty the chat column in every reaction
50
+ state. Only states the template must scope get declared.
51
+ - **Arrival honors the component's declared initial** when the template has a matching
52
+ state; otherwise the template scans its own states in declared order and takes the
53
+ first name on the component's public menu; no overlap and the component rides inline.
54
+ - **A higher state CANCELS the claims below it**: while active, hosted components in
55
+ lower-ranked states retract to inline (durable `lifetime: "conversation"` slices opt
56
+ out), so releasing it lands on the BASE arrangement, never a stale lower one.
57
+ - Ties (two components in the same view): most recent write wins (`limit: 1`).
58
+ - **No word is special.** `focus` outranks `products` by list position only; the engine
59
+ matches names and walks the list. Even priority is data.
60
+ - The template's own condition-guarded moods (a welcome hero on an empty conversation)
61
+ are private substates of the base, never ladder entries: the template does not
62
+ rearrange between them.
63
+
64
+ ## The anatomy: manifest-only (NO `<name>.yaml` envelope)
20
65
 
21
66
  ```
22
- apps/unoverse/rx/orgs/<org>/templates/<name>/
23
- manifest.json # THE ENVELOPE — the whole app contract (below)
24
- layouts/ # the ARRANGEMENTS — one per component view + the default
25
- standard.json # the default (manifest.layout): the core alone
26
- products.json # view "products" → core + cards rail
27
- product.json # view "product" → core + detail panel
67
+ apps/unoverse/rx/<project>/templates/<name>/
68
+ manifest.yaml # THE ENVELOPE: the whole app contract, incl. the states tree
69
+ layouts/ # the ARRANGEMENTS: one per state that owns one (same-name default)
70
+ main.yaml # the base: the core alone
71
+ products.yaml # state "products" → core + cards rail
72
+ detail.yaml # state "detail" → core + full record page
28
73
  components/ # template-local partials (core, header, composer-bar, turns…)
29
- states/ # LOCAL states only (welcome, conversation, call phases) —
30
- # available in exactly the layouts whose trees include them
74
+ states/ # base substates' drawings (welcome): included plainly by the
75
+ # chrome, own condition inside, containment compiled from the tree
31
76
  ```
32
77
 
33
- Each layout = a complete arrangement: `{ "$include": "components/core" }` + that view's
34
- surface. Shared chrome lives ONCE in `components/`; every layout includes it. A layout
35
- named after a view MUST contain a surface selecting that view (guard-enforced) — the
36
- name claims the instance, the surface renders it.
78
+ Each layout = a complete arrangement: `$include: components/core` + that state's
79
+ surface. Shared chrome lives ONCE in `components/`; every layout includes it. The tree
80
+ claims the instance, the layout's surface renders it: a reaction state's layout holds a
81
+ ComponentSlot selecting the matching view.
37
82
 
38
- ## manifest.json (the app contract)
83
+ ## manifest.yaml (the app contract)
39
84
 
40
- Required: `name` · `description` (what it IS, ≤120) · `whenToUse` (utterance-shaped — the
41
- USER's words; findIntent ranks on it — the OPENING words carry the ranking; disqualify by
85
+ Required: `name` · `description` (what it IS, ≤120) · `whenToUse` (utterance-shaped: the
86
+ USER's words; findIntent ranks on it, the OPENING words carry the ranking; disqualify by
42
87
  property, never by naming siblings; full contract: docs/design/05 §Naming) ·
43
- `category` · `defaultState` (`"template"` = the full surface) · `inputSchema` ·
44
- `layout` (the DEFAULT layout name) · `stateOrder` (local states + layouts, picker order) ·
45
- `binding` (`workflow` + `trigger` — the app OWNS its workflow; without a real binding it
46
- is not done). Optional: `preview` (the per-LAYOUT mock map for Studio —
47
- `{ "products": ["productcard", "productcard"] }`; repeated name = several instances),
48
- `autoTrigger`, `expose`, `service` (`"voice"` — the channel instantiates the native
49
- service, which projects `callState` into scope).
88
+ `category` · `defaultState: "template"` (the app-level LOAD state, a protocol field,
89
+ nothing to do with component views) · `inputSchema` · `layout` (the BASE layout name) ·
90
+ `states` (the tree above) · `binding` (`workflow` + `trigger`: the app OWNS its
91
+ workflow; without a real binding it is not done). Optional: `preview` (the per-state
92
+ mock map for Studio: `{ "products": ["product-card", "product-card"] }`; repeated
93
+ name = several instances), `autoTrigger`, `expose`, `service` (`"voice"`: the channel
94
+ instantiates the native service, which projects `callState` into scope).
50
95
 
51
- **The generalist trap:** a home/fallback surface must NOT enumerate its siblings' jobs —
52
- it owns general help and cedes specific jobs by property, naming none.
96
+ **The generalist trap:** a home/fallback surface must NOT enumerate its siblings' jobs.
97
+ It owns general help and cedes specific jobs by property, naming none.
53
98
 
54
- ## Sizing — each layout owns its widths
99
+ ## Sizing: each layout owns its widths
55
100
 
56
101
  The app = the ACTIVE layout's total, nothing else ever. Inside a layout: the core panel
57
102
  is always open (`appWidth` on the core box), the surface panel declares its width on the
58
103
  ComponentSlot. Lint-enforced:
59
- - `appWidth` = a NAMED size from the org's `styles/semantic/app-sizes.json`
60
- (`chat` · `chat-slim` · `rail` · `panel`) — raw CSS is an ERROR.
61
- - ONE declaration per panel — the frame never declares width/flex.
104
+ - `appWidth` = a NAMED size from the project's `styles/semantic/app-sizes`
105
+ (`chat` · `chat-slim` · `rail` · `panel` · `flex`). Raw CSS is an ERROR.
106
+ - ONE declaration per panel; the frame never declares width/flex.
62
107
  - NEVER on a layout root; NEVER `visibleWhen`-guarded (a conditional arrangement is a
63
- LAYOUT, selected by name).
64
- - An OVERLAY surface (`inset: 0` over the core) declares NO appWidth and needs NO layout
65
- of its own — reach for a layout when the ARRANGEMENT changes.
66
- - Nothing in the manifest sizes the app (`width`/`focusWidth` dead — lint error).
108
+ STATE with its own layout).
109
+ - An OVERLAY surface (`inset: 0` over the core) declares NO appWidth and needs NO state
110
+ of its own. Reach for a state when the ARRANGEMENT changes.
111
+ - Nothing in the manifest sizes the app (`width`/`focusWidth` dead, a lint error).
67
112
  - Layout root: `overflow: hidden`.
68
113
 
69
- ## Surfaces — templates REACT, they never own
70
-
71
- One instance → one placeholder: the flow is the placeholder for `inline`; a surface (or
72
- a layout, via its inner surface) is the placeholder for a named view. The SDK keeps a
73
- claimed instance out of the flow — never hide a flow copy yourself.
74
-
75
- - **Flow slot**: `{ "type": "ComponentSlot", "select": {} }` — always present.
76
- - **Reaction surface**: select by the VIEW, never by type/id:
77
- ```jsonc
78
- { "type": "ComponentSlot", "appWidth": "panel",
79
- "select": { "from": "all", "where": { "field": "defaultState", "eq": "product" }, "limit": 1 },
80
- "frame": { /* chrome; an inner bare ComponentSlot marks the placement */ } }
114
+ ## Surfaces: templates REACT, they never own
115
+
116
+ One instance → one placeholder: the flow is the placeholder for `inline`; a reaction
117
+ state's layout (via its inner surface) is the placeholder for a matched view. The SDK
118
+ keeps a claimed instance out of the flow; never hide a flow copy yourself.
119
+
120
+ - **Flow slot**: `{ type: ComponentSlot, select: {} }`, always present.
121
+ - **Reaction surface**: select by the public `view`, never by type/id:
122
+ ```yaml
123
+ type: ComponentSlot
124
+ appWidth: panel
125
+ select:
126
+ from: all
127
+ where: { field: view, eq: detail }
128
+ limit: 1
129
+ frame: # chrome; an inner bare ComponentSlot marks the placement
81
130
  ```
82
- ❌ `select: { type: [...] }`, ❌ `where.field` ≠ `defaultState`, ❌ template chrome
83
- writing a component's slice — all lint-flagged.
131
+ ❌ `select: { type: [...] }`, ❌ `where.field` ≠ `view`, ❌ template chrome
132
+ writing a component's slice, all lint-flagged. (`defaultState` as the select field
133
+ is the v1 legacy spelling.)
134
+ - **Input tools are template chrome** (unchanged rule): a composer, an edit form, a
135
+ picker is owned by the template, never a streamed component state; chrome never
136
+ "arrives". A card's Edit stages its data into the template's one input tool.
84
137
  - **Template state** (`setTemplateValue`) is ONLY for the template's own chrome (a
85
- disclosure, the composer `draft`). "Is something focused" is DERIVED — never stored.
138
+ disclosure, the composer `draft`). "Is something focused" is DERIVED, never stored.
86
139
  - **Chrome reacts via `surfacedView`** (SDK-projected active view name, `""` = all
87
- inline): `visibleWhen { "field": "surfacedView", "in": ["", "products"] }`.
88
- - **A `limit: 1` surface's occupant FILLS it** automatically — never sprinkle
89
- `minHeight: full` through faces. Rails (no limit) keep content-sized cards.
140
+ inline): `visibleWhen { field: surfacedView, in: ["", "products"] }`.
141
+ - **A `limit: 1` surface's occupant FILLS it** automatically; never sprinkle
142
+ `minHeight: full` through component layouts. Rails (no limit) keep content-sized cards.
90
143
  - A full layer caps its height and scrolls its BODY only (`flex: 1` + `minHeight: 0` +
91
144
  `overflow: auto`; header/footer pinned).
145
+ - `pinned: true` on a reaction layout keeps late-arriving components from yanking the
146
+ page from under a reading guest; the guest's own tap still moves it.
92
147
 
93
148
  ## Voice templates
94
149
 
95
- `"service": "voice"` in the manifest. Call phases (`states/idle…user-speaking`) are
96
- LOCAL states included by the cores; typically a wide core in the default layout and a
97
- slim core (`chat-slim`) in the card layouts. Cards during a call select layouts by name
98
- exactly as in chat. Audio is never wired in a definition.
150
+ `"service": "voice"` in the manifest. Call phases (idle…user-speaking) branch off the
151
+ projected `callState` discriminant inside the base; typically a wide core in the base
152
+ layout and a slim core (`chat-slim`) in the card layouts. Cards during a call match
153
+ template states by name exactly as in chat. Audio is never wired in a definition.
99
154
 
100
- ## New org?
155
+ ## New project?
101
156
 
102
- `rx/orgs/<org>/` = `templates/` + optional `components/` (org-private microapps, names
103
- unique across tiers) + a **complete self-contained** `styles/` (copy
104
- `rx/orgs/default/styles` and re-value the base scales). Addresses: apps
105
- `unoverse://apps/<org>/<name>`, org components `unoverse://components/<org>/<name>`,
106
- themes `unoverse://theme/<org>/<theme>`.
157
+ `rx/<project>/` = `templates/` + optional `components/` (project-private microapps,
158
+ names unique across tiers) + a **complete self-contained** `styles/` (copy an existing
159
+ project's `styles/` and re-value the base scales). Addresses: apps
160
+ `unoverse://apps/<project>/<name>`, project components
161
+ `unoverse://components/<project>/<name>`, themes `unoverse://theme/<project>/<theme>`.
107
162
 
108
163
  ## Validate & ship
109
164
 
110
- 1. Publish from Studio — its lint must report 0 errors (layout/name-sync rules, widths, reaction rules, tokens).
111
- 2. Restart unoverse — the node re-synthesizes.
112
- 3. **See it**: Studio — layout pills on top (default first, stateOrder order), local
113
- states down the left, freely combinable; acting inside the preview transitions like
114
- the runtime. Then run it live: the bound workflow streams components, and entering a
115
- view swaps the layout end-to-end.
165
+ 1. Manifest declares the states tree: base first, reaction states in priority order,
166
+ base-only moods nested inside the base, shell content NOT declared. No authored
167
+ `stateOrder`, no hand-written containment guards.
168
+ 2. Publish from Studio; its lint must report 0 errors (the rules are v2-aware:
169
+ state/layout ownership, widths, reaction rules, tokens).
170
+ 3. Restart unoverse; the node re-synthesizes.
171
+ 4. **See it**: in Studio, the state switcher reads the tree (base first, ladder order),
172
+ base substates beneath, freely combinable; acting inside the preview transitions
173
+ like the runtime. Then run it live: the bound workflow streams components, a card
174
+ entering a public view walks the ladder and rearranges the template end-to-end,
175
+ and its ✕ hands the lower state back.