@unoverse-platform/studio 0.1.85 → 0.1.86

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.
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The commit decision — the Transition Model's pure heart (UNOVERSE_TEMPLATE_MODEL §13).
3
+ *
4
+ * The mount double-buffers its arrangement and asks ONE question per render:
5
+ * may the committed arrangement change now? The answer depends only on the two
6
+ * arrangements — no store, no DOM, no time — so the contract is a pure function
7
+ * under conformance tests and can never regress silently.
8
+ *
9
+ * - "immediate": commit with no ceremony (first paint; the guest's own
10
+ * navigation changed the layout; an un-directed mount — no
11
+ * verdict governs it, so its behaviour is exactly pre-contract).
12
+ * - "transition": a BOUNDARY happened — the governing verdict changed (settle
13
+ * or clear) — commit as the one designed, animatable change.
14
+ * - "hold": membership moved with no boundary (a data ripple). The shelf
15
+ * does not move. The caller owns the fail-open grace (§13.5).
16
+ * - "none": nothing changed.
17
+ */
18
+
19
+ export interface Arrangement {
20
+ /** The template's active layout (shape) name. */
21
+ layoutName: string | null;
22
+ /** Held pointers, in display order. */
23
+ held: string[];
24
+ /** Identity of the governing verdict (sorted held-pointer join), null when no
25
+ * verdict governs — the un-directed mount. */
26
+ verdictSig: string | null;
27
+ }
28
+
29
+ export type CommitDecision = "immediate" | "transition" | "hold" | "none";
30
+
31
+ const sameHeld = (a: string[], b: string[]) => a.length === b.length && a.every((p, i) => p === b[i]);
32
+
33
+ /**
34
+ * `directed` = the placement grants a director, so every delivery WILL settle.
35
+ * A directed mount's membership therefore moves only at the settle — including
36
+ * the very first batch, whose cards arrive as separate messages before any
37
+ * verdict exists (committing each arrival painted the shelf in six pops, the
38
+ * first-appearance flicker observed live). Un-directed mounts keep committing
39
+ * immediately: no settle is ever coming for them.
40
+ */
41
+ export function decideCommit(committed: Arrangement | null, next: Arrangement, directed: boolean): CommitDecision {
42
+ if (!committed) return "immediate";
43
+ // The guest's own navigation (name-sync layout change) is never held back.
44
+ if (next.layoutName !== committed.layoutName) return "immediate";
45
+ if (sameHeld(next.held, committed.held)) return "none";
46
+ // Membership moved. A changed governing verdict IS the boundary (settle/clear).
47
+ if (next.verdictSig !== committed.verdictSig) return "transition";
48
+ // Directed: the settle is coming — hold until it lands (grace fails open).
49
+ if (directed) return "hold";
50
+ // No verdict governs an un-directed mount: commit as it always did.
51
+ if (next.verdictSig === null) return "immediate";
52
+ return "hold";
53
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * THE GUEST'S HAND OUTRANKS THE DIRECTOR (UNOVERSE_TEMPLATE_MODEL §6b rule 3,
3
+ * state priority): a pointer whose LATEST write is the guest's own interaction,
4
+ * holding a non-base face the mount claims (an open Detail View), stays in the
5
+ * mount's scope regardless of deliveries and settles. Observed live without
6
+ * this: a new batch settled and yanked the guest out of an open detail page.
7
+ *
8
+ * The hold releases exactly as the model says: the guest's ✕ writes the face
9
+ * back to the base — no longer a non-base guest hold — and the current
10
+ * arrangement takes over. A re-delivered instance is an ARRIVAL (its merge
11
+ * out-touches the guest write) and may be re-faced, per §4 level 2.
12
+ *
13
+ * Pure over reads so the priority law is conformance-testable.
14
+ */
15
+
16
+ export interface GuestHoldReads {
17
+ touchOf(p: string): number;
18
+ interactionTouchOf(p: string): number;
19
+ viewOf(p: string): string | undefined;
20
+ }
21
+
22
+ export function guestHeldPointers(
23
+ pointers: readonly string[],
24
+ reads: GuestHoldReads,
25
+ claimedLayouts: readonly string[],
26
+ baseLayout: string | null,
27
+ ): string[] {
28
+ return pointers.filter((p) => {
29
+ const it = reads.interactionTouchOf(p);
30
+ if (!it || it < reads.touchOf(p)) return false; // latest write is not the guest's
31
+ const v = reads.viewOf(p);
32
+ return !!v && v !== baseLayout && claimedLayouts.includes(v);
33
+ });
34
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The Transition Model (UNOVERSE_TEMPLATE_MODEL §13) — the render layer's
3
+ * designed way of moving interfaces in and out of a mounted template:
4
+ * `decide.ts` (the pure commit decision), `MountArrangement.tsx` (the
5
+ * double-buffer), `viewTransition.ts` (the theme-gated animated commit).
6
+ */
7
+ export { decideCommit, type Arrangement, type CommitDecision } from "./decide";
8
+ export { MountArrangement } from "./MountArrangement";
9
+ export { commitArrangement } from "./viewTransition";
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The commit IS the animation (UNOVERSE_TEMPLATE_MODEL §13.4): when the theme
3
+ * asks for motion, the boundary commit is wrapped in the browser's View
4
+ * Transitions API — the browser diffs old and new and animates; the THEME's CSS
5
+ * owns every duration and ease. The SDK contributes only this generic behaviour
6
+ * and NO styles. Engines without the API, and themes that author no motion, get
7
+ * the instant commit — today's behaviour, gracefully.
8
+ */
9
+
10
+ import { flushSync } from "react-dom";
11
+
12
+ type WithViewTransition = { startViewTransition?: (cb: () => void) => unknown };
13
+
14
+ export function commitArrangement(commit: () => void, motion: boolean): void {
15
+ const doc = typeof document !== "undefined" ? (document as unknown as WithViewTransition) : undefined;
16
+ if (!motion || typeof doc?.startViewTransition !== "function") {
17
+ commit();
18
+ return;
19
+ }
20
+ // From a microtask, outside React's lifecycle, so flushSync is legal: the
21
+ // API snapshots, the commit flushes the new DOM, the browser animates.
22
+ queueMicrotask(() => {
23
+ try {
24
+ doc.startViewTransition!(() => flushSync(commit));
25
+ } catch {
26
+ commit(); // fail open — the change lands even if the ceremony breaks
27
+ }
28
+ });
29
+ }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: unoverse-create
3
- description: "Create or edit Unoverse platform artifacts: UI components (including briefed/AI-filled components), apps, atoms and styles (design/), brand and org packs (themes, tokens, rebranding, cloning a universe for a new client), agent skills and prompt blocks (prompts/), custom workflow nodes (nodes/), and workflows built live on the Canvas via the builder MCP. Use whenever the user wants to create, add, build, or modify a component, app, card, page, theme, brand, org pack, agent skill, prompt block, custom node, or workflow in an Unoverse project, even if they don't say 'Unoverse' (they'll describe UI like 'a card with a button and an image' or 'a page the AI fills in')."
3
+ description: "Create or edit Unoverse platform artifacts: UI components (including briefed/AI-filled components), apps, templates (arrangements with open parts a delivery fills — shelf grids, emails, comparison pages), atoms and styles (design/), brand and org packs (themes, tokens, rebranding, cloning a universe for a new client), agent skills and prompt blocks (prompts/), custom workflow nodes (nodes/), and workflows built live on the Canvas via the builder MCP. Use whenever the user wants to create, add, build, or modify a component, app, card, page, theme, brand, org pack, agent skill, prompt block, custom node, or workflow in an Unoverse project, even if they don't say 'Unoverse' (they'll describe UI like 'a card with a button and an image' or 'a page the AI fills in')."
4
4
  ---
5
5
 
6
6
  # Creating Unoverse Artifacts
@@ -30,8 +30,10 @@ that is the workspace folder itself; in the platform monorepo it is `apps/unover
30
30
  ([components](https://github.com/unoverse-platform/marketplace/tree/main/definitions/components), [atoms](https://github.com/unoverse-platform/marketplace/tree/main/definitions/atoms), [styles](https://github.com/unoverse-platform/marketplace/tree/main/definitions/styles)).
31
31
  Study the closest exemplar there, then compose those atoms and reference those
32
32
  components; never hand-roll a shape the base already ships. Everything is plain
33
- YAML files — no connection, no running platform needed. (In the platform monorepo
33
+ YAML files: no connection, no running platform needed. (In the platform monorepo
34
34
  the same material is on disk at `design/marketplace/`.)
35
+ [The design system](https://docs.unoverse.ai/design/design-system.md) is the guide to
36
+ what ships and how to use it.
35
37
 
36
38
  ## Step 1: Identify the artifact, read its playbook
37
39
 
@@ -42,6 +44,7 @@ anything**:
42
44
  |---|---|
43
45
  | A UI component or shared atom, incl. a page/card the AI fills with content (briefed) | `references/component.md` (§Briefs for AI-filled) |
44
46
  | An app (chat surface, wizard shell) | `references/app.md` |
47
+ | A template: an arrangement with open parts a delivery fills (a shelf grid, an email, a comparison page) | `references/template.md` |
45
48
  | A style / theme / token change on ONE component | `references/component.md` §Tokens |
46
49
  | A brand: an org pack, a rebrand, a new client universe, cloning an org | `references/brand.md` |
47
50
  | An agent skill (behavior guide the AI follows) | `references/agent-skill.md` |
@@ -59,15 +62,18 @@ If it's ambiguous (e.g. "add a card that shows weather"), it's usually a **compo
59
62
  **New to design artifacts?** The guided learning journey at
60
63
  [docs.unoverse.ai/design](https://docs.unoverse.ai/design/overview.md)
61
64
  ([quick-start](https://docs.unoverse.ai/design/quick-start.md) →
65
+ [essentials](https://docs.unoverse.ai/design/essentials.md) →
66
+ [interface-data](https://docs.unoverse.ai/design/interface-data.md) →
62
67
  [components](https://docs.unoverse.ai/design/components.md) →
63
68
  [state](https://docs.unoverse.ai/design/state.md) →
69
+ [templates](https://docs.unoverse.ai/design/templates.md) →
64
70
  [apps](https://docs.unoverse.ai/design/apps.md) →
65
71
  [styles-and-tokens](https://docs.unoverse.ai/design/styles-and-tokens.md) →
66
72
  [studio](https://docs.unoverse.ai/design/studio.md) →
67
73
  [validate-and-ship](https://docs.unoverse.ai/design/validate-and-ship.md) →
68
- [troubleshooting](https://docs.unoverse.ai/design/troubleshooting.md)) teaches
69
- components, apps, the state model (incl. locked voice/native state), tokens,
70
- Studio, and validation in order;
74
+ [troubleshooting](https://docs.unoverse.ai/design/troubleshooting.md)) teaches the
75
+ shared grammar every artifact uses, the four kinds, the state model (incl. locked
76
+ voice/native state), tokens, **studio**, and validation in order;
71
77
  [design/CLAUDE.md](https://docs.unoverse.ai/design/CLAUDE.md) is the condensed agent
72
78
  rulebook. The playbooks here assume that material. Append `.md` to any docs URL to
73
79
  fetch it as raw markdown.
@@ -16,15 +16,16 @@ already in the workspace. (Monorepo: `design/sab/apps/sab-chat`.)
16
16
  A component is a state tree whose public states are its whole interface; an app
17
17
  declares its OWN state tree in the manifest and reacts to components by name:
18
18
 
19
- > A component enters a public view → the app asks "do I have a state with that
19
+ > A component enters a public state → the app asks "do I have a state with that
20
20
  > name?" Match: the app enters that state and its layout draws. No match: the
21
21
  > component renders inline in the flow. Always.
22
22
 
23
23
  Derived off the store, never stored, no wiring. The component never knows apps
24
24
  exist, and post-spawn the app NEVER writes a component's state. Name-match is the
25
- default convention (`reactsTo:` on an app state is the rare escape hatch for
26
- vocabulary mismatches). **Rule of thumb: component causes it = a reaction state;
27
- app knows it = a base substate or app state.**
25
+ ONLY mechanism: an app state reacts because its name matches, so give the app the
26
+ name the component writes. (`reactsTo:` is DESIGN-ONLY: nothing in the runtime
27
+ reads it, so never author it.) **Rule of thumb: component causes it = a reaction
28
+ state; app knows it = a base substate or app state.**
28
29
 
29
30
  ## The manifest states tree: one declaration, everything follows
30
31
 
@@ -33,13 +34,21 @@ The canonical shape (a chat layout: a base arrangement plus a reaction ladder):
33
34
  ```yaml
34
35
  states:
35
36
  main: # the base arrangement, FIRST
37
+ layout: layouts/main
36
38
  states:
37
- welcome: {} # contained substate; first substate = the landing default
38
- focus: {} # then the reaction ladder, in priority order
39
- products: {}
40
- detail: {}
39
+ welcome: # contained substate; first substate = the landing default
40
+ layout: layouts/main-welcome
41
+ focus: # then the reaction ladder, in priority order
42
+ layout: layouts/focus
43
+ products:
44
+ layout: layouts/products
45
+ detail:
46
+ layout: layouts/detail
41
47
  ```
42
48
 
49
+ Every state DECLARES its drawing as a path relative to the app folder (2026-08-29
50
+ ruling): no same-name default, no assumed folder, `{}` is not a complete state.
51
+
43
52
  - **Top-level order IS the priority ladder**, base first. The app can be in ONE
44
53
  state at a time: it walks the list top-down and enters the first of its states that
45
54
  ANY hosted component currently matches. The ladder is DERIVED from the tree; never
@@ -59,19 +68,22 @@ states:
59
68
  - **A higher state CANCELS the claims below it**: while active, hosted components in
60
69
  lower-ranked states retract to inline (durable `lifetime: "conversation"` slices opt
61
70
  out), so releasing it lands on the BASE arrangement, never a stale lower one.
62
- - Ties (two components in the same view): most recent write wins (`limit: 1`).
71
+ - Ties (two components in the same state): most recent write wins (`limit: 1`).
63
72
  - **No word is special.** `focus` outranks `products` by list position only; the engine
64
73
  matches names and walks the list. Even priority is data.
65
74
  - The app's own condition-guarded moods (a welcome hero on an empty conversation)
66
75
  are private substates of the base, never ladder entries: the app does not
67
76
  rearrange between them.
68
77
 
69
- ## The anatomy: manifest-only (NO `<name>.yaml` envelope)
78
+ ## The anatomy: ONE FOLDER GRAMMAR (2026-08-29 — apps match components/templates)
70
79
 
71
80
  ```
72
81
  design/<project>/apps/<name>/
73
- manifest.yaml # THE ENVELOPE: the whole app contract, incl. the states tree
74
- layouts/ # the ARRANGEMENTS: one per state that owns one (same-name default)
82
+ <name>.yaml # THE ENVELOPE: type: app + the states tree (what the app IS);
83
+ # each state declares its layout path and carries its preview
84
+ manifest.yaml # THE MCP FACE: description/whenToUse/inputSchema/binding/
85
+ # makeMCP/assetDomains — how the outside finds and calls it
86
+ layouts/ # the ARRANGEMENTS: one per state, each DECLARED by path in the tree
75
87
  main.yaml # the base: the core alone
76
88
  products.yaml # state "products" → core + cards rail
77
89
  detail.yaml # state "detail" → core + full record page
@@ -84,7 +96,7 @@ design/<project>/apps/<name>/
84
96
  Each layout = a complete arrangement: `$include: components/core` + that state's
85
97
  surface. Shared chrome lives ONCE in `components/`; every layout includes it. The tree
86
98
  claims the instance, the layout's surface renders it: a reaction state's layout holds a
87
- ComponentSlot selecting the matching view.
99
+ ComponentSlot selecting the matching state.
88
100
 
89
101
  ## manifest.yaml (the app contract)
90
102
 
@@ -121,27 +133,29 @@ ComponentSlot. Lint-enforced:
121
133
  ## Surfaces: apps REACT, they never own
122
134
 
123
135
  One instance → one placeholder: the flow is the placeholder for `inline`; a reaction
124
- state's layout (via its inner surface) is the placeholder for a matched view. The SDK
136
+ state's layout (via its inner surface) is the placeholder for a matched state. The SDK
125
137
  keeps a claimed instance out of the flow; never hide a flow copy yourself.
126
138
 
127
139
  - **Flow slot**: `{ type: ComponentSlot, select: {} }`, always present.
128
- - **Reaction surface**: select by the public `view`, never by type/id:
140
+ - **Reaction surface**: select by the public `state`, never by type/id:
129
141
  ```yaml
130
142
  type: ComponentSlot
131
143
  appWidth: panel
132
144
  select:
133
145
  from: all
134
- where: { field: view, eq: detail }
146
+ where:
147
+ field: state
148
+ eq: page
135
149
  limit: 1
136
150
  frame: # chrome; an inner bare ComponentSlot marks the placement
137
151
  ```
138
- ❌ `select: { type: [...] }`, ❌ `where.field` ≠ `view`, ❌ app chrome
139
- writing a component's slice, all lint-flagged. (`defaultState` as the select field
140
- is the v1 legacy spelling.)
152
+ ❌ `select: { type: [...] }`, ❌ `where.field` ≠ `state`, ❌ app chrome
153
+ writing a component's slice, all lint-flagged. (`view` and `defaultState` are the
154
+ legacy spellings of the select field.)
141
155
  - **Input tools are app chrome** (unchanged rule): a composer, an edit form, a
142
156
  picker is owned by the app, never a streamed component state; chrome never
143
157
  "arrives". A card's Edit stages its data into the app's one input tool.
144
- - **App state** (`setTemplateValue`) is ONLY for the app's own chrome (a
158
+ - **App state** (`setAppValue`) is ONLY for the app's own chrome (a
145
159
  disclosure, the composer `draft`). "Is something focused" is DERIVED, never stored.
146
160
  - **Chrome reacts via `surfacedView`** (SDK-projected active view name, `""` = all
147
161
  inline): `visibleWhen { field: surfacedView, in: ["", "products"] }`.
@@ -180,5 +194,5 @@ whole, see brand.md). Addresses: apps
180
194
  4. **See it**: in Studio, the state switcher reads the tree (base first, ladder order),
181
195
  base substates beneath, freely combinable; acting inside the preview transitions
182
196
  like the runtime. Then run it live: the bound workflow streams components, a card
183
- entering a public view walks the ladder and rearranges the app end-to-end,
197
+ entering a public state walks the ladder and rearranges the app end-to-end,
184
198
  and its ✕ hands the lower state back.
@@ -16,10 +16,11 @@ folder shape. (In the platform monorepo the richest multi-state exemplar is on d
16
16
 
17
17
  ```
18
18
  design/<project>/components/<name>/
19
- <name>.yaml # ROOT: envelope (name/category/nodeSize/outputs/props) + the STATE TREE + root
19
+ <name>.yaml # the envelope: name/category/nodeSize/outputs/props + `states:` + `values:` (root only as a real shell)
20
20
  manifest.yaml # RENDER CONTRACT + discovery meta (title / description ≤120 /
21
- # whenToUse utterance-shaped). Presence = spatially discoverable;
22
- # the envelope NEVER duplicates the meta.
21
+ # whenToUse utterance-shaped). REQUIRED on every artifact EXCEPT
22
+ # a flat file nothing discovers (2026-08-30); the envelope NEVER
23
+ # duplicates the meta.
23
24
  layouts/ # the DRAWINGS: one file per state that owns an arrangement
24
25
  components/ # component-local shared partials (earned: 2+ layouts share a shape)
25
26
  ```
@@ -32,14 +33,14 @@ manifest, no folders. Structure is EARNED; start flat.
32
33
  A component is **data plus a state tree**; each state owns the layout(s) that draw it.
33
34
  The root's `state:` block IS the design, readable top to bottom.
34
35
 
35
- > **THE GATE — answer this before adding ANY state to `view`:**
36
+ > **THE GATE, answer this before adding ANY state to the tree:**
36
37
  > *"Would a hosting app rearrange the page to show this?"*
37
38
  > **No → it's a private step**: nest it under `on:` inside an existing public state.
38
39
  > **Yes → public.**
39
40
  > ⚠ "The user asked for N states" never means N public states. In-place swaps
40
41
  > (confirmation, success, empty, error, loading, a form's next step) are almost always
41
- > PRIVATE. Privacy is the default; promotion to `view` is a deliberate act with a
42
- > app consequence.
42
+ > PRIVATE. Privacy is the default; promotion to a PUBLIC state is a deliberate act
43
+ > with an app consequence.
43
44
 
44
45
  The canonical shape (a product-card component). Note where each state landed and why:
45
46
  `products` and `detail` are PUBLIC because an app genuinely rearranges for each (a
@@ -47,35 +48,30 @@ grid rail vs a full page); `detail`'s inner `detail`/`apply` are PRIVATE steps b
47
48
  applying swaps content in place — the page does not move:
48
49
 
49
50
  ```yaml
50
- state:
51
- view: # the PUBLIC axis
52
- initial: products
51
+ states: # TOP LEVEL (2026-08-29), like every kind; the word is
52
+ products: # `state` everywhere (setValue/Switch on/select field)
53
+ layout: layouts/products # REQUIRED path: nothing assumed
54
+ detail: # the full page
55
+ layout: layouts/product
56
+ on: step # a PRIVATE axis: the steps inside detail
53
57
  states:
54
- products: {} # no layout key: same-name default, {} is complete
55
- detail: # the full page
56
- layout: product # declared ONLY because the filename differs
57
- on: step # a PRIVATE axis: the steps inside detail
58
- initial: detail
59
- states:
60
- detail: { layout: product-detail }
61
- apply: { layout: product-apply }
62
- root:
63
- type: Box
64
- children:
65
- - type: Switch
66
- on: view # the root switches on `view`, ONE case active
67
- cases:
68
- products: { $include: layouts/products }
69
- detail: { $include: layouts/product }
58
+ detail: { layout: layouts/product-detail }
59
+ apply: { layout: layouts/product-apply }
60
+ values: # STARTING VALUES of keys the component writes (scalars)
61
+ progressPct: 10%
70
62
  ```
71
63
 
64
+ **No `root`**: the compiler synthesizes the `Switch on state` from the tree. Author
65
+ a root ONLY as a real shell (chrome beyond the plain Switch); a restated Switch is
66
+ linted as redundant.
67
+
72
68
  The rules, in the order they bite:
73
69
 
74
- - **The axis is `view`.** The component's public state lives in its slice under `view`.
75
- The root is a `Switch on view`; buttons transition with
76
- `setValue { values: [{ key: view, value: detail }] }`. That write is the ENTIRE
70
+ - **The word is `state`** (ruled 2026-08-29). The component's public state lives in
71
+ its slice under `state`. The root is a `Switch on state`; buttons transition with
72
+ `setValue { values: [{ key: state, value: detail }] }`. That write is the ENTIRE
77
73
  transition machinery: no guards, no events, no per-component machines.
78
- - **Public states are the top level of the `view` axis** and they are the app's
74
+ - **Public states are the top level of `states:`** and they are the app's
79
75
  WHOLE vocabulary: everything apps can react to, nothing more. Steps nest
80
76
  privately (`on: <key>` + `initial` + `states`), invisible outside the component.
81
77
  The placement rule is THE GATE above: **if the app must rearrange to show it,
@@ -84,11 +80,11 @@ The rules, in the order they bite:
84
80
  **The tell in Studio:** non-initial public states preview FULL-BLEED (no stage
85
81
  padding), precisely because they are app-liftable surfaces. If a state you
86
82
  meant as an in-place swap suddenly renders edge-to-edge, you promoted a private
87
- step to `view` — move it into the nest.
88
- - **Each state owns its layout(s).** `layout:` is OPTIONAL: a state with no declaration
89
- draws the layout of its own name, and `{}` is a complete state. Write `layout:` only
90
- when the filename differs. A layout is never addressable on its own; only its owning
91
- state selects it. Name states for what they MEAN (`detail`), never for their drawing. **Substate drawings are axis-prefixed**: a step's file carries its axis key
83
+ step to the public menu — move it into the nest.
84
+ - **Each state owns its layout(s), DECLARED as a path** (`layout: layouts/<file>`,
85
+ REQUIRED — 2026-08-29; no same-name default, `{}` is not complete; the one
86
+ exception is a FLAT component with no layouts/ folder). A layout is never
87
+ addressable on its own; only its owning state selects it. Name states for what they MEAN (`detail`), never for their drawing. **Substate drawings are axis-prefixed**: a step's file carries its axis key
92
88
  (`step-register`, `callState-idle`) and is declared via `layout:`; unprefixed
93
89
  files are arrangements (exemplars: `course-application`, `voice-chat`).
94
90
  - **Shell vs steps.** A state's own layout is its SHELL, always on while the state is
@@ -106,20 +102,20 @@ The rules, in the order they bite:
106
102
  inline. The server compiles the tree to the public menu + initials it serves
107
103
  (`publicStates`, `initialView`). Post-spawn, the app NEVER writes a component's
108
104
  state.
109
- - **Unmatched view = inline. Always.** An ignored public state does not stay put, it
105
+ - **Unmatched state = inline. Always.** An ignored public state does not stay put, it
110
106
  FALLS INLINE in the flow. A step that must never move the page belongs in the nest,
111
107
  not on the public menu.
112
- - Legacy: v1's `defaultState` field and layout-name-sync are superseded; the SDK reads
113
- `defaultState` only as a migration alias for `view`. New authoring writes `view`.
108
+ - Legacy: `view` (the v2 intermediate) and v1's `defaultState` are read-only
109
+ aliases; layout-name-sync is superseded. New authoring writes `state`.
114
110
 
115
111
  ## The rules (Studio's publish lint enforces all of these; 0 errors required)
116
112
 
117
113
  1. **Three homes for everything it shows** (AUTHORING §3):
118
114
  - static content → **hardcoded literals** in the layout (`value`, literal `items: []` on
119
115
  Each, `src`), never props, never `state`;
120
- - internal view-state → the **`state` block, SCALARS ONLY** (`step`, `phase`,
121
- `progressPct`): an array/object/URL in `state` is slop (workflow data → props;
122
- static → hardcode);
116
+ - starting values of keys it writes → the **`values:` block, SCALARS ONLY**
117
+ (`step`, `phase`, `progressPct`; `state:` scalars = legacy): an array/object/URL
118
+ there is slop (workflow data → props; static → hardcode);
123
119
  - workflow-fed data → **`props` with `input: true`** (the `default` is the preview mock).
124
120
  Usually empty.
125
121
  - **Projected-field preview rule:** any LOCKED/projected field a
@@ -127,7 +123,7 @@ The rules, in the order they bite:
127
123
  a prop with a preview default: Studio's bare component preview supplies ONLY prop
128
124
  defaults, so an undeclared projected read renders NOTHING there; live, the projected
129
125
  value wins and the default never applies. A Ref-embedded component's root Switch
130
- likewise needs a `default:` case (`view` is absent without a spawned slice).
126
+ likewise needs a `default:` case (`state` is absent without a spawned slice).
131
127
  - **Who owns a mood: the DRIVER decides.** Ask what WRITES the discriminant:
132
128
  (1) the component's own buttons (`setValue`) → component substates on a private axis,
133
129
  names from the design (`course-application`); (2) a service projecting a NAMED value
@@ -143,12 +139,15 @@ The rules, in the order they bite:
143
139
  - **Prop names = the data contract. Never invent them.** Source data hydrates props
144
140
  BY NAME (as-is, no mapping); a name the source doesn't carry silently renders the
145
141
  preview default (tell: title streams, image stays mock). Content-attached cards use
146
- the writer vocabulary: `title` `tagline` `description` `bodyCopy` `introParagraph`
147
- `primaryImage` `images` `link` `callToAction`. Law:
142
+ the writer vocabulary, listed layer by layer in
143
+ [design/interface-data](https://docs.unoverse.ai/design/interface-data.md)
144
+ (`title` `tagline` `description` `bodyCopy` `primaryImage` `images` …). Law:
148
145
  `UNOVERSE_MCP_APP_PROTOCOL.md` §Content-attached cards (platform spec; a
149
146
  guard test in the platform server enforces it).
150
- 2. **Root = `Switch on view`**, cases per PUBLIC state, each `$include` of the state's
151
- layout (the tree above). Keep an `inline` state for any component that lives in a
147
+ 2. **NO root: the compiler synthesizes the `Switch on state`** from `states:` (the tree
148
+ above), so a hand-written root that merely restates the tree is lint-flagged as
149
+ redundant. Author a `root:` ONLY for real always-on chrome beyond that Switch.
150
+ Keep an `inline` state for any component that lives in a
152
151
  conversation flow: the flow is its placeholder there. One EXCEPTION is the
153
152
  **single-state (permanent) component**: exactly ONE public state, NO inline. It
154
153
  arrives in its surface, cannot leave it (no ✕, no other state), and is enriched in
@@ -164,14 +163,14 @@ The rules, in the order they bite:
164
163
  takes over. Default `"turn"`: the instance returns to inline / retires on the next
165
164
  user turn. Values are a closed set: `turn | conversation` (lint-checked;
166
165
  [design/components](https://docs.unoverse.ai/design/components.md)).
167
- 3. **State is local; the `view` is the interface** (STATE_MODEL §5). Private axes
166
+ 3. **Private state is local; the public `state` is the interface** (STATE_MODEL §5). Private axes
168
167
  (`step`, `phase`, …) are the component's own business; the app never sees them.
169
- The only thing that crosses is the public `view`. The component writes ONLY its own
170
- slice (`setValue`): expand = `setValue { view: "detail" }`; its expanded layout
168
+ The only thing that crosses is the public `state`. The component writes ONLY its own
169
+ slice (`setValue`): expand = `setValue { state: "page" }`; its expanded layout
171
170
  carries its own ✕ back down. When a hosting app has a state of that name, the
172
171
  instance **lifts out of the flow into that state's slot**. The SDK renders each
173
172
  instance in exactly ONE placeholder, so you never hide a flow copy yourself (no
174
- `hideBelow`, no overlay hack). ❌ NEVER `setTemplateValue` to open a surface
173
+ `hideBelow`, no overlay hack). ❌ NEVER `setAppValue` to open a surface
175
174
  (the deprecated bridge, linted). ❌ Never guard a layout against its own state:
176
175
  the tree owns "which state is active"; a layout re-checking its discriminant is
177
176
  the double-guard smell the model removes.
@@ -182,7 +181,7 @@ The rules, in the order they bite:
182
181
  5. **Atoms** (the base design system's shared shapes, used via `Ref`; browse them at
183
182
  [marketplace/definitions/atoms](https://github.com/unoverse-platform/marketplace/tree/main/definitions/atoms), or on disk at `design/marketplace/atoms/` in the
184
183
  platform monorepo) are for shapes shared across components, authoring-time
185
- only (the server expands them before serving; no Studio view). `Ref props` remaps FIELDS;
184
+ only (the server expands them before serving; no Studio entry). `Ref props` remaps FIELDS;
186
185
  **`Ref with` passes LITERALS**: `{ "type": "Ref", "ref": "button", "with": { "label":
187
186
  "Learn more", "icon": "arrowRight" }, "action": { … } }`. A bind whose field is a `with`
188
187
  key becomes a hardcoded attribute, a truthy `with` key drops a matching `visibleWhen`
@@ -225,7 +224,7 @@ The rules, in the order they bite:
225
224
  `onEnterView` only. Omit it and every state fires.
226
225
  - **Name the handler for what it DOES, never for the phase** (`fetchPlaceDetails`, not
227
226
  `onEnterView`): the phase already said the moment.
228
- - **Nothing to wire up.** A component changing its own view writes `view`, and that
227
+ - **Nothing to wire up.** A component changing its own face writes `state`, and that
229
228
  write IS the event the hook reacts to. No signal to author, no action to chain.
230
229
  - **Prefer a platform-ready handler.** `getDetail` (zero code) reads the instance's
231
230
  `universal_id` and merges the stored record's fields in by name, so authored copy
@@ -311,9 +310,11 @@ component. Its key nouns/verbs must appear in `whenToUse`'s first sentence.
311
310
  1. Study the closest published exemplar ([marketplace/definitions/components](https://github.com/unoverse-platform/marketplace/tree/main/definitions/components);
312
311
  monorepo: `design/sab/components/product-card`); copy its folder shape.
313
312
  2. Declare the state tree first: public states on top, steps nested, each state owning
314
- its layout (same-name default, `layout:` only when the filename differs).
315
- 3. Write the envelope + manifest + layouts; root = `Switch on view`; every shown thing
316
- in its ONE home; transitions are `setValue` writes to `view` (or a private axis).
313
+ its layout as a root-relative path (`layout: layouts/<file>`, required; a FLAT
314
+ component with no layouts/ folder declares face-only states with no layout).
315
+ 3. Write the envelope + manifest + layouts; the root is synthesized from `states:`; every
316
+ shown thing in its ONE home; transitions are `setValue` writes to `state` (or a private
317
+ axis).
317
318
  4. Publish from Studio; its lint must report 0 errors (the rules are v2-aware and cite
318
319
  the doc for every rule).
319
320
  5. Restart unoverse → Studio: mock (prop defaults + the state switcher reads the tree:
@@ -5,7 +5,9 @@
5
5
  1. [nodes/CLAUDE.md](https://docs.unoverse.ai/nodes/CLAUDE.md): the authoritative agent
6
6
  guide. The folder, the eight non-negotiable rules, every file with an example, and
7
7
  the lint-error-to-fix table.
8
- 2. [nodes/manifest-nodes](https://docs.unoverse.ai/nodes/manifest-nodes.md): the format in full, including the run context.
8
+ 2. [nodes/manifest-nodes](https://docs.unoverse.ai/nodes/manifest-nodes.md): the format in full.
9
+ [nodes/expressions](https://docs.unoverse.ai/nodes/expressions.md) is the template and
10
+ expression grammar, the sandbox, and every root the run context carries.
9
11
  3. [nodes/node-discoverability](https://docs.unoverse.ai/nodes/node-discoverability.md): **read before writing `whenToUse`**.
10
12
  4. As needed: [credentials](https://docs.unoverse.ai/nodes/credentials.md),
11
13
  [who-can-run-it](https://docs.unoverse.ai/nodes/who-can-run-it.md),