@unoverse-platform/studio 0.1.101 → 0.1.103

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 (34) hide show
  1. package/local/sample-identity/brand.yaml +2 -0
  2. package/package.json +1 -1
  3. package/screens/TemplatesLibraryView.tsx +58 -98
  4. package/screens/TemplatesView.tsx +16 -0
  5. package/vendor/base/src/definitions/load/assets.ts +65 -0
  6. package/vendor/base/src/definitions/load/compile/component.ts +1 -0
  7. package/vendor/base/src/definitions/load/compile/index.ts +1 -1
  8. package/vendor/base/src/definitions/load/compile/sections.ts +26 -116
  9. package/vendor/base/src/definitions/load/compile/template.ts +41 -25
  10. package/vendor/base/src/definitions/load/expand/walk.ts +4 -1
  11. package/vendor/base/src/definitions/load/load.ts +6 -0
  12. package/vendor/base/src/definitions/model.ts +20 -41
  13. package/vendor/base/src/definitions/registry/homes.ts +9 -1
  14. package/vendor/base/src/lint/design/file.mjs +29 -5
  15. package/vendor/base/src/lint/design/vocabulary.mjs +1 -1
  16. package/vendor/base/src/lint/design/walk.mjs +31 -156
  17. package/vendor/base/src/manifests/executor/context.ts +1 -1
  18. package/vendor/base/src/manifests/executor/toolBridge.ts +3 -29
  19. package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +35 -12
  20. package/vendor/base/src/manifests/runtime/duplex/session.ts +123 -48
  21. package/vendor/base/src/manifests/runtime/index.ts +1 -1
  22. package/vendor/base/src/platform/audioManager.ts +89 -14
  23. package/vendor/base/src/platform/clientTransport.ts +8 -0
  24. package/vendor/base/src/security/permissions.ts +2 -2
  25. package/vendor/sdk/appEngine.tsx +106 -46
  26. package/vendor/sdk/lib/UnoverseComponent.tsx +58 -5
  27. package/vendor/sdk/lib/app.tsx +9 -5
  28. package/vendor/sdk/lib/connection.tsx +14 -3
  29. package/vendor/sdk/lib/core/actions.ts +5 -2
  30. package/vendor/sdk/lib/index.ts +2 -2
  31. package/vendor/sdk/lib/primitives.tsx +38 -4
  32. package/vendor/sdk/lib/realtime/useAudioPlayback.ts +6 -1
  33. package/vendor/sdk/lib/voice.tsx +96 -13
  34. package/vendor/sdk/pageInterfaces.tsx +388 -0
@@ -9,7 +9,7 @@ import type { UnoverseDefinition } from "../../model.js";
9
9
  import { loadDefinition } from "../load.js";
10
10
  import { composeIncludes } from "../expand/index.js";
11
11
  import { expandTemplateSections } from "./sections.js";
12
- import { placesOfStates } from "./component.js";
12
+ import { placesOfStates, pointStepsAtStep, STEP } from "./component.js";
13
13
 
14
14
  /**
15
15
  * ONE PREVIEWED INSTANCE'S SEED — `preview ?? default` per prop, the same values the
@@ -30,28 +30,17 @@ function seedFor(ref: string, org?: string): Record<string, unknown> | undefined
30
30
  return Object.keys(seed).length ? seed : undefined;
31
31
  }
32
32
 
33
- /** Stamp a section list's Studio stand-ins: which refs each face previews, seeded
34
- * `preview ?? default` (keyed by REF, because a director band names several and may
35
- * name one twice). A linked part previews ITSELF; a delivery band previews whatever
36
- * its `preview:` names. */
33
+ /** Stamp a section list's Studio stand-ins: each placed part previews ITSELF, seeded
34
+ * `preview ?? default` and keyed by its ref. */
37
35
  function seedSections(
38
36
  secs: NonNullable<UnoverseDefinition["sections"]>[string],
39
37
  org: string | undefined,
40
38
  previews: Record<string, string[]>,
41
39
  ): void {
42
40
  for (const s of secs) {
43
- // A LINKED part previews ITSELF — including a director SEARCH place, which carries a
44
- // ref since the one-word merge (a kind check here hid every search place's mock the
45
- // day the kinds merged). Only an unlinked DELIVERY band previews its `preview:` list.
46
- const refs = s.ref ? [s.ref] : s.kind === "director" ? (s.preview ?? []) : [];
47
- if (s.ref) previews[s.face] = [s.ref];
48
- else if (s.preview?.length) previews[s.face] = s.preview;
49
- const seeds: Record<string, Record<string, unknown>> = {};
50
- for (const ref of new Set(refs)) {
51
- const seed = seedFor(ref, org);
52
- if (seed) seeds[ref] = seed;
53
- }
54
- if (Object.keys(seeds).length) s.seeds = seeds;
41
+ previews[s.face] = [s.ref];
42
+ const seed = seedFor(s.ref, org);
43
+ if (seed) s.seeds = { [s.ref]: seed };
55
44
  }
56
45
  }
57
46
 
@@ -60,11 +49,9 @@ export function synthesizeTemplateRoot(def: UnoverseDefinition, folderDir?: stri
60
49
  /**
61
50
  * A BLOCK — a flat template that declares a DRAWING instead of a tree (2026-08-31).
62
51
  *
63
- * Its three words still have to compile. Without this the `director:` line sat in the
64
- * served root verbatim, unexpanded and inert: the renderer speaks ComponentSlot and
65
- * knows no such word, so a block drew as an empty box wherever it was used (observed
66
- * in Studio the moment the first one was authored). The folder form compiles its
67
- * sections as it composes each state's layout; the flat form had no such pass.
52
+ * Its placed parts still have to compile: the renderer speaks ComponentSlot, and a
53
+ * part left as a bare `Ref` into `components/` would draw nothing. The folder form
54
+ * compiles its parts as it composes each state's layout; the flat form does it here.
68
55
  *
69
56
  * Compiled into the SAME projections a one-state template serves, so nothing
70
57
  * downstream has to know which form it was authored in: `layouts`, `defaultLayout`,
@@ -72,13 +59,13 @@ export function synthesizeTemplateRoot(def: UnoverseDefinition, folderDir?: stri
72
59
  */
73
60
  if ((def as unknown as { states?: unknown }).states === undefined && def.layouts === undefined) {
74
61
  const secs: NonNullable<UnoverseDefinition["sections"]>[string] = [];
75
- const compiled = expandTemplateSections(def.root, def.name, def.name, secs);
62
+ const compiled = expandTemplateSections(def.root, def.name, secs);
76
63
  if (secs.length) {
77
64
  const previews: Record<string, string[]> = {};
78
65
  seedSections(secs, def.org, previews);
79
66
  if (Object.keys(previews).length) def.preview = { ...(def.preview ?? {}), ...previews };
80
67
  }
81
- // The projections are built WHETHER OR NOT the root carries section words
68
+ // The projections are built WHETHER OR NOT the root places parts
82
69
  // (2026-09-02): a pure-arrangement block — `band`, `two-column`, style and an
83
70
  // opening, no fill lane — is still a template, and without a layout Studio drew it
84
71
  // as a blank canvas (observed the day blocks returned). Its own authored children
@@ -106,6 +93,16 @@ export function synthesizeTemplateRoot(def: UnoverseDefinition, folderDir?: stri
106
93
  : {};
107
94
  const shapeNames = Object.keys(states);
108
95
  if (!folderDir || !shapeNames.length) return;
96
+ // STEPS, exactly as a component's (compile/component.ts, "One word: state"): a state's
97
+ // nested substates are its steps. The author writes `state` for them; they live in the
98
+ // hidden step value, so a step never moves the template. Rewritten in the AUTHORED layout,
99
+ // before its sections compile, so a part's own claim (which names the part) is never
100
+ // mistaken for a step of the same name.
101
+ const steps = new Set<string>();
102
+ for (const n of shapeNames) {
103
+ const subs = (states[n] as { states?: Record<string, unknown> } | null)?.states;
104
+ if (subs && typeof subs === "object") Object.keys(subs).forEach((sub) => steps.add(sub));
105
+ }
109
106
  const layouts: Record<string, unknown> = {};
110
107
  const sections: NonNullable<UnoverseDefinition["sections"]> = {};
111
108
  const compiledPreview: Record<string, string[]> = {};
@@ -113,7 +110,8 @@ export function synthesizeTemplateRoot(def: UnoverseDefinition, folderDir?: stri
113
110
  const st = (states[name] ?? {}) as Record<string, unknown>;
114
111
  if (typeof st.layout !== "string") continue;
115
112
  const secs: NonNullable<UnoverseDefinition["sections"]>[string] = [];
116
- layouts[name] = expandTemplateSections(composeIncludes({ $include: st.layout }, folderDir), name, def.name, secs);
113
+ const authored = composeIncludes({ $include: st.layout }, folderDir);
114
+ layouts[name] = expandTemplateSections(steps.size ? pointStepsAtStep(authored, steps) : authored, def.name, secs);
117
115
  sections[name] = secs;
118
116
  // MOCK VALUES TRAVEL WITH THE SECTION — a preview host seeds instances from the wire.
119
117
  seedSections(secs, def.org, compiledPreview);
@@ -135,6 +133,24 @@ export function synthesizeTemplateRoot(def: UnoverseDefinition, folderDir?: stri
135
133
  def.initialView = shapeNames[0];
136
134
  const statePlaces = placesOfStates(states);
137
135
  if (statePlaces) def.statePlaces = statePlaces;
136
+ // THE STEP TREE, served like a component's: what an Agent navigates and a viewer lists.
137
+ // Components and templates are both tasks.
138
+ def.stateTree = shapeNames.map((n) => {
139
+ const subs = (states[n] as { states?: Record<string, { layout?: unknown } | null> } | null)?.states;
140
+ const names = subs && typeof subs === "object" ? Object.keys(subs) : [];
141
+ // A state that takes the whole screen says so; the SDK takes the browser fullscreen for it.
142
+ const full = (states[n] as { fullscreen?: unknown } | null)?.fullscreen === true ? { fullscreen: true } : {};
143
+ if (!names.length) return { name: n, ...full };
144
+ return {
145
+ name: n,
146
+ ...full,
147
+ on: STEP,
148
+ initial: names[0],
149
+ states: names.map((sub) => ({ name: sub, ...(typeof subs![sub]?.layout === "string" ? { layout: subs![sub]!.layout as string } : {}) })),
150
+ };
151
+ });
152
+ const first = def.stateTree.find((e) => e.states?.length)?.initial;
153
+ if (first) def.state = { ...(def.state ?? {}), [STEP]: first };
138
154
  if (Object.keys(sections).length) def.sections = sections;
139
155
  if (Object.keys(compiledPreview).length) def.preview = { ...(def.preview ?? {}), ...compiledPreview };
140
156
  delete (def as unknown as Record<string, unknown>).states;
@@ -25,8 +25,11 @@ export function expandNode(node: AnyNode, contextOrg?: string, inherit?: Record<
25
25
  // so an atom `Ref` inside a repeated item is inlined like anywhere else.
26
26
  // An `Each`'s `item` draws ONE ITEM, so everything inside it belongs to that
27
27
  // array's item shape (see `inheritProps`).
28
+ // An `Each` over LITERAL `items` draws the author's own content: its item fields are
29
+ // nobody's to fill, so an atom inside it hands the host no contract.
28
30
  const itemsField = node?.type === "Each" ? (node.bind as AnyNode | undefined)?.items : undefined;
29
- if (node?.item) node.item = expandNode(node.item, contextOrg, inherit, props, typeof itemsField === "string" ? [...scope, itemsField] : scope);
31
+ const literalItems = node?.type === "Each" && Array.isArray(node.items);
32
+ if (node?.item) node.item = expandNode(node.item, contextOrg, inherit, literalItems ? undefined : props, typeof itemsField === "string" ? [...scope, itemsField] : scope);
30
33
  // `Switch` carries its branches in `cases` (a value→subtree map) — expand each, so an
31
34
  // atom `Ref` inside a Switch branch is inlined like anywhere else (else the branch
32
35
  // renders blank). Parallel to children/item.
@@ -27,6 +27,7 @@ import { normalizeDeclaredKind, defaultLayoutOf } from "../model.js";
27
27
 
28
28
  import { composeIncludes, expandNode, expandRefs, referencedDefinition, inheritState, mergeInheritedProps } from "./expand/index.js";
29
29
  import { compileComponentTree, synthesizeTemplateRoot } from "./compile/index.js";
30
+ import { inlineAssets } from "./assets.js";
30
31
  type AnyNode = Record<string, any>;
31
32
 
32
33
  /** Resolve a definition by name (case-insensitive), searching the relevant folder(s).
@@ -101,6 +102,7 @@ export function loadDefinition(
101
102
  // The FLAT single-file form compiles its state tree exactly like the
102
103
  // folder form (unified 2026-08-29 — a template-local part is flat).
103
104
  if (k === "component") compileComponentTree(d);
105
+ inlineAssets(d, dirname(path));
104
106
  return form(d);
105
107
  });
106
108
  }
@@ -180,6 +182,9 @@ export function loadDefinition(
180
182
  if (m[key] != null) (def as unknown as Record<string, unknown>)[key] = m[key];
181
183
  }
182
184
  if (m.preview && typeof m.preview === "object") def.preview = m.preview as Record<string, string[]>;
185
+ // THE WORKFLOW THAT WORKS IT (UNOVERSE_HOST_PAGE.md rule 4): the manifest names it,
186
+ // and the page reads it here to start a call on it.
187
+ if (m.binding && typeof m.binding === "object") def.binding = m.binding as { workflow?: string; trigger?: string };
183
188
  // SPATIAL-CAPABLE, exactly as a component is (§9c, ruled 2026-08-30): a
184
189
  // template is found by what a guest says, through the same projection, the
185
190
  // same registry switch and the same embedded text. The difference is only
@@ -469,6 +474,7 @@ export function loadDefinition(
469
474
  }
470
475
  if (k === "component") compileComponentTree(def);
471
476
  if (k === "template") synthesizeTemplateRoot(def, folderDir);
477
+ inlineAssets(def, folderDir);
472
478
  return k === "atom" ? def : form(def);
473
479
  });
474
480
  }
@@ -17,58 +17,35 @@ export interface UnoverseDefinition {
17
17
  /** COMPONENTS AND TEMPLATES: the place each state names. An interface entering a state
18
18
  * that names a place moves there (UNOVERSE_SCREEN.md §2.3). */
19
19
  statePlaces?: Record<string, string>;
20
- /** TEMPLATES (§9b RULED 2026-08-29): the compiled SECTIONS of each state's
21
- * layout — one entry per `static:` / `copywriter:` / `director:` line, in
22
- * arrangement order. `static`/`copywriter` link a component (face = its
23
- * basename); the ONE `director` section is where deliveries land (face = its
24
- * `state:` pin, else the owning state's name). Studio's table and seeding read
25
- * this; the wire layouts carry the expanded ComponentSlots. */
20
+ /** TEMPLATES (§9b): the compiled SECTIONS of each state's layout, one entry per
21
+ * placed part (`type: Ref` + `ref: components/<name>`), in arrangement order. The
22
+ * face is the part's basename, and the wire layouts carry the expanded
23
+ * ComponentSlots. Studio's table and seeding read this; so do the fill schema and
24
+ * the Task Runner. What an Agent writes into a part is each prop's `input`. */
26
25
  sections?: Record<
27
26
  string,
28
27
  {
29
- kind: "static" | "copywriter" | "director";
28
+ kind: "part";
30
29
  face: string;
31
- /** Linked sections: the authored path (components/masthead). A DIRECTOR section
32
- * with a component is a SEARCH PLACE (§9e, one word since 2026-09-02); without
33
- * one it is a DELIVERY band. */
34
- component?: string;
35
- /** Linked sections: the resolvable scoped ref (email-digest/masthead). */
36
- ref?: string;
37
- /** SEARCH places (§9e rule 10d): the DERIVED question — never authored. Stamped at
38
- * fill time from the first voice that speaks: the writer's headings (items), the
39
- * page's pool (galleries), the component's own contract, or the guest's ask.
40
- * Present on the wire only after derivation; a layout carrying one fails to load. */
41
- ask?: string;
42
- /** SEARCH places: which `object_type`s may answer, as the six `include*` flags
43
- * (`ranking.ts`). A day is made of SERVICES; nothing should draw a need, a skill or
44
- * an app into a moment. APPS ARE ALWAYS OFF on this lane, whatever is listed: the
45
- * page's searches are SILENT — no app unlocked, no tool minted, no card lane, no
46
- * director woken — because otherwise every place would rain cards onto the screen
47
- * while the page was still assembling itself. */
48
- include?: string[];
49
- /** The previewed components' own mock values (`preview ?? default` per prop),
50
- * keyed by REF — a linked part's one component, or each of the director's
51
- * stand-ins. A preview host seeds instances with these the way the Components
52
- * screen does; without them everything draws as ghosts and a finished arrangement
53
- * reads as a broken one. Never used at runtime: a delivery fills the instance. */
30
+ /** The authored path (components/masthead). */
31
+ component: string;
32
+ /** The resolvable scoped ref (email-digest/masthead). */
33
+ ref: string;
34
+ /** The part's own mock values (`preview ?? default` per prop), keyed by REF. A
35
+ * preview host seeds instances with these the way the Components screen does;
36
+ * without them everything draws as ghosts and a finished arrangement reads as a
37
+ * broken one. Never used at runtime: a fill writes the instance. */
54
38
  seeds?: Record<string, Record<string, unknown>>;
55
- /** Delivery bands only (a director section with no component). */
56
- state?: string;
57
- rules?: string;
58
- preview?: string[];
59
- /** Both director modes: how many the director keeps — the ONE cap word. */
60
- pick?: number;
61
- /** Search places: which search answers (authored; derived when absent). */
62
- type?: "discovery" | "intent";
63
- /** An items place (§9e rule 10c): the writer shapes min–max items, each heading
64
- * arriving with its own search. */
65
- items?: { min: number; max: number };
66
39
  }[]
67
40
  >;
68
41
  /** TEMPLATES: Studio preview mocks per slot — interface refs, each rendering its
69
42
  * own defaults (the interface-model rule: mocks are the interface's OWN defaults,
70
43
  * never authored preview data). From the template's manifest.yaml. */
71
44
  preview?: Record<string, string[]>;
45
+ /** TEMPLATES: the workflow that works this template as a task, from its manifest.yaml
46
+ * (docs/unoverse/design/sdk/UNOVERSE_HOST_PAGE.md rule 4). A page starts a call on it from
47
+ * the avatar; a template without one starts nothing. */
48
+ binding?: { workflow?: string; trigger?: string };
72
49
  /** Source basename for FILE definitions (atoms) — the identity refs resolve by. */
73
50
  file?: string;
74
51
  /** The client org it belongs to (server-injected from its folder, never
@@ -176,6 +153,8 @@ export interface UnoverseDefinition {
176
153
  on?: string;
177
154
  initial?: string;
178
155
  states?: { name: string; layout?: string }[];
156
+ /** The state takes the whole screen (the authored `fullscreen: true`). */
157
+ fullscreen?: boolean;
179
158
  }[];
180
159
  }
181
160
 
@@ -302,7 +302,8 @@ export function resolveComponentDirs(
302
302
  }
303
303
 
304
304
  /** Resolve a component ref (bare or `<org>/<name>`, either tier) to its ON-DISK
305
- * FOLDER — the home of its manifest and any lifecycle handler files (onstart.js …).
305
+ * FOLDER — the home of its manifest and any lifecycle handler files (onstart.js …). A
306
+ * template resolves here too, after components: both are tasks.
306
307
  * Returns null for a flat/marketplace component with no folder. Used by the MCP
307
308
  * render path to run component lifecycle handlers. */
308
309
  export function componentFolder(ref: string): string | null {
@@ -312,5 +313,12 @@ export function componentFolder(ref: string): string | null {
312
313
  const folder = join(dir, lower);
313
314
  if (defPath(folder, "manifest") || defPath(folder, lower)) return folder;
314
315
  }
316
+ // A TEMPLATE IS A TASK TOO (ruled 2026-09-07: components and templates are both tasks), so
317
+ // its manifest's lifecycle, analytics and finish line are found the same way. A component
318
+ // of the same name wins, as it always has.
319
+ for (const { dir } of templateDirs(refOrg)) {
320
+ const folder = join(dir, lower);
321
+ if (defPath(folder, "manifest") || defPath(folder, lower)) return folder;
322
+ }
315
323
  return null;
316
324
  }
@@ -413,6 +413,28 @@ function lintFile(file) {
413
413
  }
414
414
  }
415
415
 
416
+ // AN ORG'S OWN IMAGES (definitions/load/assets.ts): `src: assets/<file>` must name a file in
417
+ // an assets/ folder from the file's own folder up to the org's (a template keeps its own), or
418
+ // the shared one, of an image type, under 256 KB.
419
+ {
420
+ const home = (orgDirs ?? []).find((d) => file.startsWith(d + sep)) ?? null;
421
+ const shared = DS ?? null;
422
+ const walk = (n) => {
423
+ if (Array.isArray(n)) return n.forEach(walk);
424
+ if (!n || typeof n !== "object") return;
425
+ if (n.type === "Image" && typeof n.src === "string" && n.src.startsWith("assets/")) {
426
+ const dirs = [];
427
+ for (let d = dirname(file); home && d.startsWith(home); d = dirname(d)) dirs.push(d);
428
+ const hit = [...dirs, shared].map((d) => d && join(d, n.src)).find((f) => f && existsSync(f));
429
+ if (!hit) report("error", file, `image "${n.src}" is not in this org's assets/ folder (docs.unoverse.ai/design/styles-and-tokens)`);
430
+ else if (!/\.(svg|png|jpe?g|webp|gif)$/i.test(n.src)) report("error", file, `image "${n.src}" is not an image type the platform serves (svg, png, jpg, webp, gif)`);
431
+ else if (statSync(hit).size > 256 * 1024) report("error", file, `image "${n.src}" is over 256 KB; a photo belongs on a CDN, not in the design folder`);
432
+ }
433
+ Object.values(n).forEach(walk);
434
+ };
435
+ walk(json);
436
+ }
437
+
416
438
  // A STATE'S PLACE (UNOVERSE_SCREEN.md §2.3) is `chat` or a Place some app declares. A name no
417
439
  // app has sends the interface to the conversation without a word, so it is refused here.
418
440
  if (placeNames && !isAppPath(file) && json && typeof json === "object" && !Array.isArray(json) && json.states && typeof json.states === "object") {
@@ -552,8 +574,10 @@ function lintFile(file) {
552
574
  report("error", file, `preview."${state}" must be an array of component names, or an object of template-state mock data`);
553
575
  continue;
554
576
  }
577
+ // `name#state` names the state the seed opens in (the address grammar, owner
578
+ // 2026-09-11); the lookup is the name before the hash.
555
579
  for (const c of list)
556
- if (comps && !comps.has(String(c).toLowerCase()))
580
+ if (comps && !comps.has(String(c).split("#")[0].toLowerCase()))
557
581
  report("error", file, `preview."${state}" names unknown component "${c}". No match in design/marketplace/components/ or this org's components/ (org-privacy: another org's components are out of reach; lookup is case-insensitive)`);
558
582
  }
559
583
  }
@@ -676,13 +700,13 @@ function lintFile(file) {
676
700
  // EVERY PROP CARRIES A DESCRIPTION (UNOVERSE_AUTHORING §3b, RULED 2026-08-30).
677
701
  //
678
702
  // It reads BOTH ways, which is why it is compulsory rather than encouraged. A
679
- // description is not only what a copywriter fills against: it is what lets an agent
703
+ // description is not only what an Agent fills against: it is what lets an agent
680
704
  // UNDERSTAND the screen it is looking at. `read` reports where the person is; the
681
705
  // descriptions say what is actually in front of them. A screen of unnamed fields is
682
706
  // one no agent can reason about, in either direction.
683
707
  //
684
- // AND IT SAYS NOTHING ABOUT PERMISSION. That is the layout's word — `static` /
685
- // `copywriter` / `director` (TEMPLATE_MODEL §9b) — never a prop's. The rule this
708
+ // AND IT SAYS NOTHING ABOUT PERMISSION. That is the prop's `input` (UNOVERSE_AUTHORING
709
+ // "input decides"), never its description. The rule this
686
710
  // replaces was "a described prop is a writable prop", which made silence the safety
687
711
  // mechanism: a field was protected because nobody had explained it, and explaining it
688
712
  // was what exposed it. Describing a field is now free of consequence, so there is no
@@ -691,7 +715,7 @@ function lintFile(file) {
691
715
  .filter(([, v]) => !(v && typeof v === "object" && typeof v.description === "string" && v.description.trim()))
692
716
  .map(([k]) => k);
693
717
  if (undescribed.length)
694
- report("error", file, `props [${undescribed.join(", ")}] carry no "description". Every prop says what it IS, so an agent can read the screen as well as fill it. A description grants nothing: permission is the layout's word (static / copywriter / director), never a prop's (UNOVERSE_AUTHORING §3b)`);
718
+ report("error", file, `props [${undescribed.join(", ")}] carry no "description". Every prop says what it IS, so an agent can read the screen as well as fill it. A description grants nothing: permission is the prop's input: true or false (UNOVERSE_AUTHORING §3b)`);
695
719
 
696
720
  // only components that ADOPTED the structure are held to the full discipline
697
721
  if (hasLayouts || stateFiles.length || hasStateBlock) {
@@ -114,7 +114,7 @@ export const STYLE_KEYS = new Set([
114
114
  "width", "height", "maxWidth", "minWidth", "minHeight", "maxHeight", "flex",
115
115
  "padding", "margin", "gap", "overflow",
116
116
  "position", "inset", "top", "right", "bottom", "left", "zIndex",
117
- "direction", "wrap", "align", "justify", "display", "columns", "span", "stackBelow", "container", "hideBelow", "hideAbove",
117
+ "direction", "wrap", "align", "justify", "display", "columns", "span", "stackBelow", "stage", "container", "hideBelow", "hideAbove",
118
118
  "background", "radial", "border", "borderTop", "borderRight", "borderBottom", "borderLeft",
119
119
  "outline", "shadow", "radius", "radiusTopLeft", "radiusTopRight", "radiusBottomLeft", "radiusBottomRight",
120
120
  "font", "weight", "lineHeight", "color", "textAlign", "fit",
@@ -13,26 +13,19 @@
13
13
  * The context is created per run by index.mjs, so nothing survives between runs.
14
14
  */
15
15
  import { existsSync, statSync, readdirSync, readFileSync } from "node:fs";
16
- import { join, dirname, basename, relative, sep } from "node:path";
16
+ import { join, dirname, relative, sep } from "node:path";
17
17
  import { PRIMITIVES, CONDITION_KEYS, STYLE_KEYS, RAW_VALUE, CHILD_NODE_KEYS, PARTIAL_DIRS, DIMENSION_KEYS, UNIVERSAL_PROPS, PRIMITIVE_PROPS, PRIMITIVE_BINDS } from "./vocabulary.mjs";
18
- import { isDefFile, defName, defPath, readDef } from "./defs.mjs";
18
+ import { isDefFile, defPath } from "./defs.mjs";
19
19
 
20
- // THE FOUR WORDS and the two closed blocks (UNOVERSE_TEMPLATE_MODEL.md §9b + §9e).
21
- const SECTION_WORDS = ["static", "copywriter", "director"];
22
- const DELIVERY_KEYS = ["state", "pick", "rules", "preview"];
23
- // §9e: a queried place says WHAT draws it, WHAT it asks, WHAT KIND may answer, HOW MANY.
24
- const SEARCH_KEYS = ["component", "type", "include", "pick", "items"];
25
- // The `object_type`s a place may admit. Apps and skills are absent by construction: the
26
- // page's own searches are SILENT (no app unlocked, no tool minted, no card lane, no
27
- // director woken), or every place would rain cards onto a page still assembling itself.
28
- // `products` since migration 035 (2026-09-05; `services` retired 2026-09-07, `actions` with it:
29
- // no row is written as one). Each word maps to a declared spatial type (guarded by a test).
30
- const QUERY_TYPES = ["products", "images", "needs"];
20
+ // RETIRED LAYOUT WORDS. A template layout places its own parts with the ordinary element,
21
+ // `type: Ref` + `ref: components/<name>`, and what an Agent writes into a part is each
22
+ // prop's `input`, never the element that places it (UNOVERSE_TEMPLATE_MODEL §9b).
23
+ const RETIRED_WORDS = ["static", "director", "copywriter", "query"];
24
+ /** The one way a layout places a part: a `Ref` into the template's own `components/`. */
25
+ const PART_PREFIX = "components/";
31
26
 
32
27
  export function makeWalkNode(ctx) {
33
- const { report, checkCondition, checkDimension, checkToken, appSizesForFile, componentNamesForFile, refResolves, canonicalRef, declaredProps, atomsDirExists, stepList, spaceSteps, isAppPath, isTemplatePath, isTemplatePart, defRoot } = ctx;
34
- // One director per LAYOUT FILE, tracked for this run only (each file is walked once).
35
- const directorsSeen = new Map();
28
+ const { report, checkCondition, checkDimension, checkToken, appSizesForFile, refResolves, canonicalRef, declaredProps, atomsDirExists, stepList, spaceSteps, isAppPath, isTemplatePath, isTemplatePart, defRoot } = ctx;
36
29
 
37
30
  function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
38
31
  if (Array.isArray(node)) return node.forEach((n) => walkNode(n, file, root, widthCap));
@@ -54,147 +47,29 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
54
47
  return; // the included file is linted on its own
55
48
  }
56
49
 
57
- // THE THREE WORDS (UNOVERSE_TEMPLATE_MODEL.md §9b): a template layout's parts say HOW
58
- // they are filled instead of naming a primitive. They compile to ComponentSlots at
59
- // serve time, so the linter checks the authored word here and hands the rest of the
60
- // node to the ordinary walk as the slot it becomes.
61
- const word = SECTION_WORDS.find((w) => node[w] !== undefined);
62
- if (word && node.type === undefined) {
63
- if (!isTemplatePath(file))
64
- report("error", file, `"${word}:" is template grammar — only a template layout's parts say how they are filled. Apps and components author ComponentSlot directly (UNOVERSE_TEMPLATE_MODEL §9b)`);
65
- if (node.query !== undefined) {
66
- report("error", file, `"query:" is retired vocabulary (§9e rule 10b REVERSED 2026-09-02) — spell the place director: with the same component/items/type/include/pick. One word, one judge`);
67
- }
68
- if (word === "director") {
69
- /**
70
- * ONE WORD, TWO MODES (§9e, 2026-09-02): a director place WITH `component` is a
71
- * SEARCH place (the page fetches its own content); WITHOUT one it is a DELIVERY
72
- * band (the conversation fills it). The key sets are mutually exclusive, so a
73
- * place is always unambiguously one or the other — behaviour never forks on a
74
- * stray key. The QUESTION is never authored (rule 10d): it is derived from the
75
- * writing, the page's pool, the component's contract, or the guest's ask.
76
- */
77
- const d = node.director;
78
- if (!d || typeof d !== "object" || Array.isArray(d)) {
79
- report("error", file, `"director:" is a block, not a path (§9b rule 1)`);
80
- } else if (d.limit !== undefined) {
81
- report("error", file, `director.limit is retired — the one cap word is "pick": how many the director keeps (§9e rule 9, renamed 2026-09-02)`);
82
- } else if (d.ask !== undefined || d.query !== undefined) {
83
- report("error", file, `a director place never states its question — it is DERIVED (§9e rule 10d): the writer's headings (items), the page's pool (a gallery), the component's own contract, or the guest's ask`);
84
- } else if (typeof d.component === "string" && d.component) {
85
- // A SEARCH PLACE. The checks here are the ones that fail SILENTLY at runtime: a
86
- // place missing include or pick is skipped, and a kind nobody stores draws
87
- // nothing — both read as "the design is broken" with no log saying why.
88
- //
89
- // THE SEARCH WORDS SIT ON THE THING THAT SEARCHES (owner ruling 2026-09-02): on
90
- // an items place each ITEM searches, so type/include/pick nest INSIDE items:
91
- // beside min/max, and pick reads unambiguously as per-item. Spelled flat, the
92
- // same word meant two scopes depending on whether items: was present.
93
- const itemed = d.items && typeof d.items === "object" && !Array.isArray(d.items);
94
- if (d.items !== undefined && !itemed)
95
- report("error", file, `director.items is a block — items: { min, max, type, include, pick } (§9e rule 10c)`);
96
- if (itemed && (d.type !== undefined || d.include !== undefined || d.pick !== undefined))
97
- report("error", file, `an items place nests its search words INSIDE items: { min, max, type, include, pick } — each ITEM searches, so pick reads per item (§9e rule 10c, 2026-09-02)`);
98
- const TOP_KEYS = itemed ? ["component", "items"] : SEARCH_KEYS;
99
- for (const k of Object.keys(d))
100
- if (!TOP_KEYS.includes(k))
101
- report("error", file, `a search place has no "${k}". It reads ${TOP_KEYS.map((x) => `"${x}"`).join(", ")} (§9e; "state"/"rules" belong to a delivery band — a place with a component is governed by the page's brief)`);
102
- if (!defPath(root, d.component) && !existsSync(join(root, d.component)))
103
- report("error", file, `director.component "${d.component}" does not resolve under ${relative(process.cwd(), root)}/ (§9e)`);
104
- const ITEM_KEYS = ["min", "max", "type", "include", "pick"];
105
- if (itemed) {
106
- for (const k of Object.keys(d.items))
107
- if (!ITEM_KEYS.includes(k))
108
- report("error", file, `director.items has no "${k}". It reads ${ITEM_KEYS.map((x) => `"${x}"`).join(", ")} (§9e rule 10c)`);
109
- const { min, max } = d.items;
110
- if (typeof min !== "number" || typeof max !== "number" || min < 1 || max < min)
111
- report("error", file, `director.items carries { min, max } — how many items the writer may shape, 1 <= min <= max (§9e rule 10c)`);
112
- }
113
- // The search words, read where they live: the place itself, or each item.
114
- const s = itemed ? d.items : d;
115
- const at = itemed ? "director.items" : "director";
116
- // REQUIRED, both (2026-09-01, carried over). The engine supplies NO defaults: a
117
- // default include or pick would be one template's answer written into the platform.
118
- if (s.include === undefined)
119
- report("error", file, `${at}.include is REQUIRED on a search place — what KIND may answer; the engine has no default kind (§9e rule 6)`);
120
- else if (!Array.isArray(s.include) || s.include.some((t) => typeof t !== "string"))
121
- report("error", file, `${at}.include is an array of result types — ${QUERY_TYPES.map((t) => `"${t}"`).join(", ")} (§9e rule 6)`);
122
- else
123
- for (const t of s.include)
124
- if (!QUERY_TYPES.includes(String(t)))
125
- report("error", file, `${at}.include has no "${t}". A place may admit ${QUERY_TYPES.map((x) => `"${x}"`).join(", ")}; apps and skills are never included, because the page's own searches are SILENT (§9e rule 6)`);
126
- if (s.pick === undefined)
127
- report("error", file, `${at}.pick is REQUIRED — how many the director keeps${itemed ? " PER ITEM" : ""}; the engine has no default count (§9e rule 9)`);
128
- else if (typeof s.pick !== "number" || s.pick < 1)
129
- report("error", file, `${at}.pick is a NUMBER — how many the director keeps${itemed ? " per item" : ""}, a cap the machine enforces (§9e rule 9)`);
130
- // `type` is OPTIONAL since the question derives (rule 10d): authored it stands;
131
- // absent, a contract subject searches as intent and the guest's ask as discovery.
132
- if (s.type !== undefined && s.type !== "discovery" && s.type !== "intent")
133
- report("error", file, `${at}.type is "discovery" (what lives around a topic) or "intent" (a named need) — or omitted, and the derivation decides (§9e rule 10d)`);
134
- } else {
135
- // A DELIVERY BAND.
136
- for (const k of Object.keys(d))
137
- if (!DELIVERY_KEYS.includes(k))
138
- report("error", file, `a delivery band has no "${k}". It reads ${DELIVERY_KEYS.map((x) => `"${x}"`).join(", ")} (§9b; a place that names a component is a search place)`);
139
- if (d.pick !== undefined && (typeof d.pick !== "number" || d.pick < 1))
140
- report("error", file, `director.pick is a NUMBER — how many the director keeps, a cap the machine enforces (§9e rule 9)`);
141
- // NOTHING READS `rules`: the director's model was removed (UNOVERSE_SCREEN.md §2.7).
142
- if (d.rules !== undefined)
143
- report("warn", file, `director.rules is ignored: no model reads it any more. A band holds what arrives in search rank, up to pick. Remove it`);
144
- if (d.preview !== undefined && (!Array.isArray(d.preview) || d.preview.some((c) => typeof c !== "string")))
145
- report("error", file, `director.preview is an array of component names — Studio's stand-ins until a real run exists (§9b rule 6)`);
146
- else if (Array.isArray(d.preview)) {
147
- const comps = componentNamesForFile(file);
148
- for (const c of d.preview)
149
- if (comps && !comps.has(String(c).toLowerCase()))
150
- report("error", file, `director.preview names unknown component "${c}". No match in the design system or this org's components (lookup is case-insensitive)`);
151
- }
152
- // A PLACE NEEDS NO NAME (§9d): the compiler addresses delivery bands by
153
- // position; an AUTHORED state: still stands (the flat-email pin), so two bands
154
- // pinned to the SAME face remains the one genuine ambiguity.
155
- const seen = directorsSeen.get(file) ?? [];
156
- const face = typeof d?.state === "string" ? d.state : null;
157
- if (face && seen.includes(face))
158
- report("error", file, `two directed bands claim the face "${face}". A face has exactly one surface, or the active one is ambiguous (§9b rule 3, the reaction contract)`);
159
- directorsSeen.set(file, [...seen, face]);
160
- }
161
- } else if (typeof node[word] !== "string") {
162
- report("error", file, `"${word}:" links a component by PATH (${word}: components/<part>) — that is what makes it a known part (§9b rule 1)`);
163
- } else {
164
- const path = node[word];
165
- if (!defPath(root, path) && !existsSync(join(root, path)))
166
- report("error", file, `${word} "${path}" does not resolve under ${relative(process.cwd(), root)}/ (§9b rule 1)`);
167
- }
168
- // Everything else on the line rides through to the slot it compiles into. The select
169
- // is SYNTHESIZED here exactly as the compiler builds it (a linked part claims its own
170
- // face, limit 1; the director's claim is its `state:` pin) so the rest of the walk
171
- // sees what will actually be served — and an authored one is the ceremony §9b rule 9
172
- // exists to abolish.
50
+ // RETIRED: the section words. A stale layout fails here with the fix in the message.
51
+ const retired = RETIRED_WORDS.find((w) => node[w] !== undefined);
52
+ if (retired) {
53
+ report("error", file, `"${retired}:" is retired. Place the part with type: Ref and ref: components/<name>, and mark each of its props input: true (an Agent fills it) or input: false (drawn from its default) (UNOVERSE_AUTHORING "input decides")`);
54
+ return;
55
+ }
56
+
57
+ // A PLACED PART (UNOVERSE_TEMPLATE_MODEL §9b): in a template's layout, a `Ref` into the
58
+ // template's own `components/` folder. It compiles to a ComponentSlot at serve time,
59
+ // claiming the part's own face (its basename) at limit 1, so the linter checks the part
60
+ // resolves and hands the rest of the node to the ordinary walk as the slot it becomes.
61
+ if (node.type === "Ref" && typeof node.ref === "string" && node.ref.startsWith(PART_PREFIX) && isTemplatePath(file) && !isTemplatePart(file)) {
62
+ const path = node.ref;
63
+ if (!defPath(root, path) && !existsSync(join(root, path)))
64
+ report("error", file, `part "${path}" does not resolve under ${relative(process.cwd(), root)}/. A layout places the template's own parts: components/<name> (§9b)`);
65
+ if (node.props !== undefined || node.with !== undefined)
66
+ report("error", file, `a placed part takes no "props" or "with": its props are its own, filled by an Agent where marked input: true and drawn from their defaults where input: false (§9b)`);
67
+ // An authored select is the ceremony §9b rule 9 abolishes: the claim is synthesized.
173
68
  if (node.select !== undefined)
174
- report("error", file, `a template part never authors "select" — the three words compile to the wire primitives at serve time (§9b rule 9)`);
175
- const { static: _s, copywriter: _c, director: _d, query: _q, select: _sel, ...rest } = node;
176
- // An unpinned director claims the OWNING STATE's name (arrivals keep their own state
177
- // and the template's states react by name — the free grid↔page dance), so the face
178
- // comes from whichever state in the envelope declares this layout.
179
- const owningState = () => {
180
- const env = defPath(root, basename(root));
181
- const states = env ? readDef(env)?.states : undefined;
182
- if (!states || typeof states !== "object") return undefined;
183
- const here = defName(basename(file));
184
- return Object.keys(states).find((s) => {
185
- const l = states[s]?.layout;
186
- return typeof l === "string" && basename(l).toLowerCase() === here.toLowerCase();
187
- });
188
- };
189
- // A QUERIED place links its component under `component:`, and claims that part's own
190
- // face at limit 1 exactly as a linked part does — its `limit:` counts ROWS inside the
191
- // part, not instances of it, so it never reaches the slot (§9e).
192
- const linkedPath = word === "query" ? node.query?.component : node[word];
193
- const face =
194
- word === "director" ? (node.director?.state ?? owningState()) : typeof linkedPath === "string" ? linkedPath.split("/").pop() : undefined;
195
- const limit = word === "director" ? node.director?.limit : 1;
196
- const select = { from: "all", ...(face ? { where: { field: "state", eq: face } } : {}), ...(typeof limit === "number" ? { limit } : {}) };
197
- return walkNode({ type: "ComponentSlot", select, ...rest }, file, root, widthCap);
69
+ report("error", file, `a placed part never authors "select". It claims its own face at serve time (§9b rule 9)`);
70
+ const { type: _t, ref: _r, props: _p, with: _w, select: _sel, ...rest } = node;
71
+ const face = path.split("/").pop();
72
+ return walkNode({ type: "ComponentSlot", select: { from: "all", where: { field: "state", eq: face }, limit: 1 }, ...rest }, file, root, widthCap);
198
73
  }
199
74
 
200
75
  const t = node.type;
@@ -56,7 +56,7 @@ export function contextFor(node: ComposedNode, inputs: any, config: any, executi
56
56
  //
57
57
  // `conversationId` and `chatId` are here because the AUDIO LANE is keyed by conversation.
58
58
  // Their absence was a silent, total failure of the voice node: `runDuplexSession` fell back
59
- // to the node TYPE as its key, so `setAudioDataHandler` compared the client's real session id
59
+ // to the node TYPE as its key, so the audio lane compared the client's real session id
60
60
  // against the string "OpenAIRealtimeVoice", dropped every microphone frame, and sent the
61
61
  // model's audio to a conversation nobody was listening on. No error anywhere — just silence.
62
62
  //