@unoverse-platform/studio 0.1.101 → 0.1.102

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 +104 -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 +383 -0
@@ -6,6 +6,8 @@ extends: brand
6
6
  props:
7
7
  name:
8
8
  default: ""
9
+ pronunciation:
10
+ default: ""
9
11
  standsFor:
10
12
  default: ""
11
13
  promise:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unoverse-platform/studio",
3
- "version": "0.1.101",
3
+ "version": "0.1.102",
4
4
  "type": "module",
5
5
  "description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
6
6
  "scripts": {
@@ -1,51 +1,37 @@
1
1
  /**
2
2
  * Templates view — the Template Model's kind (docs/unoverse/UNOVERSE_TEMPLATE_MODEL.md
3
- * §9b, the three-word grammar), made browsable. A template is states owning
4
- * arrangements; a layout's SECTIONS are `static:` (placed as designed),
5
- * `copywriter:` (written into through briefs) and ONE `director:` (decided from
6
- * what streams in). Authors write `type: template`; the app kind is `app` in code.
3
+ * §9b), made browsable. A template is states owning arrangements; a layout places its
4
+ * PARTS with `type: Ref` + `ref: components/<name>`, and an Agent writes each part's
5
+ * `input: true` props. Authors write `type: template`; the app kind is `app` in code.
7
6
  * Two tiers like components: the marketplace shelf (no chip = BASE, installed,
8
7
  * read-only) and org packs (org chip).
9
8
  *
10
9
  * Header + dock MIRROR ComponentsView/AtomsView exactly (responsive width toggle,
11
- * collapsible drag-resize dock) — the dock holds the SECTIONS table (the template's
12
- * contract: each section and who fills it) instead of prop controls, because a
13
- * template's fills come from deliveries, not typed values. The preview renders the
14
- * synthesized root: linked sections seed their own components, the director section
15
- * seeds its preview mocks (real interfaces, their own defaults).
10
+ * collapsible drag-resize dock) — the dock holds the PARTS table (the template's
11
+ * contract: each part and the face it claims) instead of prop controls, because a
12
+ * template's fills come from an Agent, not typed values. The preview renders the
13
+ * synthesized root: each part seeds its own component with its own mock values.
16
14
  *
17
15
  * NOT TemplatesView.tsx: that name is taken by the Apps screen (the legacy noun).
18
16
  */
19
17
  import { useEffect, useMemo, useState, useSyncExternalStore, useCallback, type MouseEvent as ReactMouseEvent } from "react";
20
18
  import { ComponentStore, UnoverseComponent, type ResolvedTheme } from "@unoverse/sdk";
21
19
  import { client, authedFetch, usePersistentState, useRegistryToggles, ToggleSwitch } from "studio-host";
22
- import { NAV, SCREEN, navItem } from "./chrome";
20
+ import { NAV, SCREEN, navItem, navSubItem } from "./chrome";
23
21
 
24
22
  interface TemplateSection {
25
- /** static = placed as designed; copywriter = written into through its briefs;
26
- * director = the judge: a search place (with `component`) or a delivery band
27
- * (without) — one word since 2026-09-02 (§9b + §9e). */
28
- kind: "static" | "copywriter" | "director";
29
- /** The state instances render in at this section — what the claim matches. */
23
+ /** A placed part: its props' `input` says what an Agent writes (§9b). */
24
+ kind: "part";
25
+ /** The state the part's instance renders in: what the claim matches. */
30
26
  face: string;
31
- /** Linked sections: the authored path (components/masthead). */
32
- component?: string;
33
- /** Linked sections: the resolvable scoped ref (email-digest/masthead). */
34
- ref?: string;
35
- /** The previewed components' mock values, keyed by ref, compiled by the server (only
36
- * it can resolve a template-local part). Seeded into each instance so the arrangement
37
- * previews as written and filled rather than as a page of ghosts. */
27
+ /** The authored path (components/masthead). */
28
+ component: string;
29
+ /** The resolvable scoped ref (email-digest/masthead). */
30
+ ref: string;
31
+ /** The part's mock values, keyed by ref, compiled by the server (only it can resolve a
32
+ * template-local part). Seeded into each instance so the arrangement previews as
33
+ * written and filled rather than as a page of ghosts. */
38
34
  seeds?: Record<string, Record<string, unknown>>;
39
- state?: string;
40
- rules?: string;
41
- preview?: string[];
42
- /** Director places (§9e): the grammar, echoed for the dock table. `ask` appears
43
- * only after derivation (rule 10d) — never authored. */
44
- ask?: string;
45
- type?: "discovery" | "intent";
46
- include?: string[];
47
- pick?: number;
48
- items?: { min: number; max: number };
49
35
  }
50
36
  interface TemplateDef {
51
37
  name: string;
@@ -58,10 +44,12 @@ interface TemplateDef {
58
44
  whenToUse?: string;
59
45
  category?: string;
60
46
  /** Compiled per-state sections (§9b) — the loader's projection of the layout's
61
- * three-word lines; the wire layouts carry the expanded claims. */
47
+ * placed parts; the wire layouts carry the expanded claims. */
62
48
  sections?: Record<string, TemplateSection[]>;
63
49
  /** The composed arrangements, keyed by state — the pills. */
64
50
  layouts?: Record<string, unknown>;
51
+ /** The declared tree, served like a component's: each state with its steps (`on` + `initial`). */
52
+ stateTree?: { name: string; on?: string; initial?: string; states?: { name: string }[] }[];
65
53
  defaultLayout?: string;
66
54
  /** Seeding map the loader assembles: face → interface refs. */
67
55
  preview?: Record<string, string[]>;
@@ -85,38 +73,6 @@ const VIEWPORTS: { id: string; label: string; w: number | null }[] = [
85
73
 
86
74
  const refOf = (t: TemplateDef) => (t.org ? `${t.org}/${t.name}` : t.name);
87
75
 
88
- /** One section's description for the dock table — the three words (§9b). */
89
- function sectionLabel(sec: TemplateSection): { name: string; filler: string; rules: string } {
90
- // ONE WORD, TWO MODES (§9e, 2026-09-02): with `component` = a search place; without =
91
- // a delivery band. Both show as the director's — the judge is the same.
92
- if (sec.kind === "director" && sec.component)
93
- return {
94
- name: sec.component,
95
- filler: "director",
96
- rules: [
97
- sec.items ? `items ${sec.items.min}–${sec.items.max}` : null,
98
- sec.type ?? null,
99
- sec.include?.length ? sec.include.join("+") : null,
100
- sec.pick != null ? `pick ${sec.pick}` : null,
101
- ].filter(Boolean).join(" · "),
102
- };
103
- if (sec.kind === "director")
104
- return {
105
- name: "director",
106
- filler: "director",
107
- rules: [
108
- sec.state ? `state ${sec.state}` : null,
109
- sec.pick != null ? `pick ${sec.pick}` : null,
110
- sec.rules ?? null,
111
- ].filter(Boolean).join(" · "),
112
- };
113
- return {
114
- name: sec.component ?? sec.face,
115
- filler: sec.kind === "copywriter" ? "copywriter" : "static",
116
- rules: "",
117
- };
118
- }
119
-
120
76
  export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org?: string }) {
121
77
  const [templates, setTemplates] = useState<TemplateDef[]>([]);
122
78
  const [selected, setSelected] = usePersistentState<string>("wb:template", "");
@@ -194,27 +150,14 @@ export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org
194
150
  const store = useMemo(() => new ComponentStore(), [selected]);
195
151
 
196
152
  const seeds = useMemo(() => {
197
- // THE SEED PLAN, from the BASE state's compiled sections (§9b): a linked
198
- // section (static/copywriter) seeds ONE instance of its own component at its
199
- // face — the template's FRAME, which the state pills never re-face. The
200
- // director section seeds its preview refs at its face — the items the guest
201
- // actually browses.
153
+ // THE SEED PLAN, from the BASE state's compiled sections (§9b): each placed part
154
+ // seeds ONE instance of its own component at its face, the template's FRAME,
155
+ // which the state pills never re-face.
202
156
  const base = current?.defaultLayout ?? (current?.layouts ? Object.keys(current.layouts)[0] : undefined);
203
157
  const secs = (base && current?.sections?.[base]) || [];
204
158
  const out: { nodeId: string; ref: string; face?: string; framed: boolean; seed?: Record<string, unknown> }[] = [];
205
- for (const sec of secs) {
206
- // ONE word, two modes (§9e rule 10b, 2026-09-02): a director WITH a component is a
207
- // SEARCH place — its mock is its own component's preview, seeded exactly like a
208
- // linked part. Only the BAND form (no ref) seeds stand-ins. This branch predates
209
- // the merge and read every director as a band, so search places seeded nothing and
210
- // the template previewed with those sections simply missing.
211
- if (sec.ref) {
212
- out.push({ nodeId: `${sec.face}~${sec.ref}`, ref: sec.ref, face: sec.face, framed: true, seed: sec.seeds?.[sec.ref] });
213
- } else if (sec.kind === "director") {
214
- for (const [i, ref] of (sec.preview ?? []).entries())
215
- out.push({ nodeId: `${sec.face}~${ref}~${i}`, ref, face: sec.face, framed: false, seed: sec.seeds?.[ref] });
216
- }
217
- }
159
+ for (const sec of secs)
160
+ out.push({ nodeId: `${sec.face}~${sec.ref}`, ref: sec.ref, face: sec.face, framed: true, seed: sec.seeds?.[sec.ref] });
218
161
  return out;
219
162
  }, [current]);
220
163
  const framedIds = useMemo(() => new Set(seeds.filter((s) => s.framed).map((s) => s.nodeId)), [seeds]);
@@ -226,7 +169,7 @@ export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org
226
169
  for (const { nodeId, ref, face, seed } of seeds) {
227
170
  // The part's own mock values, exactly as the Components screen seeds a component
228
171
  // (`preview ?? default`) — compiled onto the section by the server, because only it
229
- // can resolve a template-local part. Seeded with nothing, every copywriter part
172
+ // can resolve a template-local part. Seeded with nothing, every part
230
173
  // draws as skeletons and a finished arrangement reads as a broken one.
231
174
  store.apply({ type: "COMPONENT_INIT", chatId: PREVIEW_CHAT, nodeId, component: { type: ref, props: seed ?? {} } });
232
175
  if (face) store.mergeComponentState(PREVIEW_CHAT, nodeId, { state: face });
@@ -335,6 +278,28 @@ export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org
335
278
  </span>
336
279
  )}
337
280
  </button>
281
+ {/* THE STEPS, as Components lists them: the open state's steps by name, straight
282
+ from the served tree. A click writes the step onto the template's own
283
+ instance, the same write its Back and Next make. */}
284
+ {selected === refOf(t) && (() => {
285
+ const st = current?.stateTree?.find((e) => e.name === activeState);
286
+ if (!st?.on || (st.states?.length ?? 0) < 2) return null;
287
+ const own = store.get(PREVIEW_CHAT, TEMPLATE_NODE) as Record<string, unknown> | undefined;
288
+ const cur = own?.[st.on] ?? st.initial;
289
+ return (
290
+ <div className="my-0.5 ml-2 flex flex-col gap-px border-l border-gray-700 pl-2">
291
+ {st.states!.map((sub) => (
292
+ <button
293
+ key={sub.name}
294
+ onClick={() => store.mergeComponentState(PREVIEW_CHAT, TEMPLATE_NODE, { [st.on!]: sub.name })}
295
+ className={`block w-full truncate rounded px-2 py-0.5 text-left text-xs capitalize transition ${navSubItem(cur === sub.name)}`}
296
+ >
297
+ {sub.name.replace(/-/g, " ")}
298
+ </button>
299
+ ))}
300
+ </div>
301
+ );
302
+ })()}
338
303
  </div>
339
304
  ))}
340
305
  </div>
@@ -536,27 +501,22 @@ export function TemplatesLibraryView({ theme, org }: { theme: ResolvedTheme; org
536
501
  <table className="w-full text-left text-xs">
537
502
  <thead className="text-gray-500">
538
503
  <tr>
539
- <th className="pb-1 pr-4 font-medium">section</th>
540
- <th className="pb-1 pr-4 font-medium">filled by</th>
541
- <th className="pb-1 font-medium">rules</th>
504
+ <th className="pb-1 pr-4 font-medium">part</th>
505
+ <th className="pb-1 font-medium">face</th>
542
506
  </tr>
543
507
  </thead>
544
508
  <tbody className="text-gray-300">
545
- {secs.map((sec, i) => {
546
- const l = sectionLabel(sec);
547
- return (
548
- <tr key={i} className="border-t border-gray-700/60">
549
- <td className="py-1 pr-4 font-mono">{l.name}</td>
550
- <td className="py-1 pr-4">{l.filler}</td>
551
- <td className="py-1 text-gray-500">{l.rules}</td>
552
- </tr>
553
- );
554
- })}
509
+ {secs.map((sec, i) => (
510
+ <tr key={i} className="border-t border-gray-700/60">
511
+ <td className="py-1 pr-4 font-mono">{sec.component}</td>
512
+ <td className="py-1 text-gray-500">{sec.face}</td>
513
+ </tr>
514
+ ))}
555
515
  </tbody>
556
516
  </table>
557
517
  ) : null;
558
518
  })() || (
559
- <div className="text-xs text-gray-500">This template declares no sections.</div>
519
+ <div className="text-xs text-gray-500">This template places no parts.</div>
560
520
  )}
561
521
  </div>
562
522
  )}
@@ -656,6 +656,22 @@ export function TemplatesView({
656
656
  },
657
657
  });
658
658
  });
659
+ // THE MOCK PLACES WHAT IT SEEDS (UNOVERSE_SCREEN.md §2.3). A place draws only what the
660
+ // screen put there, and in mock no server sends SCREEN_PLACE, so an app of places
661
+ // previewed white around a seeded interface. Each seed goes where the state it arrived
662
+ // in names, as the server would place it; a state naming no place stays in `chat`.
663
+ const byPlace = new Map<string, { pointers: string[]; states: Record<string, string> }>();
664
+ comps.forEach(({ def: c, view: declaredView, envelope }, i) => {
665
+ const view = declaredView ?? (envelope ? initialOf(c) : undefined);
666
+ const place = view ? (c as { statePlaces?: Record<string, string> }).statePlaces?.[view] : undefined;
667
+ if (!place || place === "chat") return;
668
+ const pointer = `${chatIdFor(selected ?? "app")}:${c.name}-${i}`;
669
+ const slot = byPlace.get(place) ?? { pointers: [], states: {} };
670
+ slot.pointers.push(pointer);
671
+ slot.states[pointer] = view!;
672
+ byPlace.set(place, slot);
673
+ });
674
+ for (const [place, { pointers, states }] of byPlace) store.setPlace(place, pointers, states);
659
675
  setSeeded(comps.map((c) => c.def.name));
660
676
  // The tab strips: one per seeded interface, filed under the container whose claim
661
677
  // matches the state it was seeded into (no claim match → filed under its own name).
@@ -0,0 +1,65 @@
1
+ /**
2
+ * AN ORG'S OWN IMAGES. A definition names one by its path in the org's `assets/` folder
3
+ * (`src: assets/accelerate.svg`); the loader reads the file and serves it inside the
4
+ * definition as a data URI, so it draws wherever the definition does (Studio, an app, an
5
+ * embed on another site) with nothing to upload and no host to configure. Small files only:
6
+ * a photo belongs on a CDN, and the cap keeps a definition light.
7
+ */
8
+ import { readFileSync, existsSync, statSync } from "fs";
9
+ import { join, extname, dirname } from "path";
10
+ import { projectDir } from "../registry/homes.js";
11
+
12
+ const ASSET_PREFIX = "assets/";
13
+ const ASSET_MAX_BYTES = 256 * 1024;
14
+ const ASSET_TYPES: Record<string, string> = {
15
+ ".svg": "image/svg+xml",
16
+ ".png": "image/png",
17
+ ".jpg": "image/jpeg",
18
+ ".jpeg": "image/jpeg",
19
+ ".webp": "image/webp",
20
+ ".gif": "image/gif",
21
+ };
22
+
23
+ /** The file an `assets/…` path names: the definition's own folder upward to its org's folder
24
+ * (the one holding `styles/`), so a template's part finds its org's images; then the shared one. */
25
+ function assetFile(from: string | undefined, src: string): string | null {
26
+ const rel = src.slice(ASSET_PREFIX.length);
27
+ for (let dir = from; dir && dirname(dir) !== dir; dir = dirname(dir)) {
28
+ const f = join(dir, "assets", rel);
29
+ if (existsSync(f)) return f;
30
+ if (existsSync(join(dir, "styles"))) break;
31
+ }
32
+ const shared = join(projectDir("marketplace"), "assets", rel);
33
+ return existsSync(shared) ? shared : null;
34
+ }
35
+
36
+ /** The data URI for an `assets/…` image path, or null when it is not one this folder holds. */
37
+ function dataUri(from: string | undefined, src: string): string | null {
38
+ const file = assetFile(from, src);
39
+ const type = ASSET_TYPES[extname(src).toLowerCase()];
40
+ return file && type && statSync(file).size <= ASSET_MAX_BYTES
41
+ ? `data:${type};base64,${readFileSync(file).toString("base64")}`
42
+ : null;
43
+ }
44
+
45
+ /** Every `Image` whose src is an `assets/…` path, served as the file's data URI. So is an
46
+ * `assets/…` image a `Ref` hands its piece through `with`: the piece binds it to its Image
47
+ * only once expanded, which the referenced form leaves to the client. */
48
+ export function inlineAssets(node: unknown, from: string | undefined): void {
49
+ if (Array.isArray(node)) return node.forEach((n) => inlineAssets(n, from));
50
+ if (!node || typeof node !== "object") return;
51
+ const n = node as Record<string, unknown>;
52
+ if (n.type === "Image" && typeof n.src === "string" && n.src.startsWith(ASSET_PREFIX)) {
53
+ const uri = dataUri(from, n.src);
54
+ if (uri) n.src = uri;
55
+ }
56
+ if (n.type === "Ref" && n.with && typeof n.with === "object" && !Array.isArray(n.with)) {
57
+ const w = n.with as Record<string, unknown>;
58
+ for (const [k, v] of Object.entries(w))
59
+ if (typeof v === "string" && v.startsWith(ASSET_PREFIX)) {
60
+ const uri = dataUri(from, v);
61
+ if (uri) w[k] = uri;
62
+ }
63
+ }
64
+ for (const v of Object.values(n)) inlineAssets(v, from);
65
+ }
@@ -39,6 +39,7 @@ export function compileComponentTree(def: UnoverseDefinition): void {
39
39
  if (typeof s.on === "string") entry.on = s.on;
40
40
  else if (s.states && typeof s.states === "object" && Object.keys(s.states).length) entry.on = STEP;
41
41
  if (typeof s.initial === "string") entry.initial = s.initial;
42
+ if ((s as { fullscreen?: unknown }).fullscreen === true) entry.fullscreen = true;
42
43
  if (s.states && typeof s.states === "object")
43
44
  entry.states = Object.entries(s.states).map(([k, v]) => ({
44
45
  name: k,
@@ -3,7 +3,7 @@
3
3
  * the expand/ split of 2026-08-30):
4
4
  *
5
5
  * component.ts a component's `states:` tree → served projections
6
- * sections.ts a template layout's three words → wire claims + sections
6
+ * sections.ts a template layout's placed parts → wire claims + sections
7
7
  * template.ts a template's states (folder) or root (block) → layouts/previews
8
8
  */
9
9
 
@@ -1,138 +1,48 @@
1
1
  /**
2
- * The SECTION-WORD compiler: a template layout's three words → wire claims +
3
- * section projections. Split from compile.ts into this folder 2026-09-02; the
4
- * rules are unchanged.
2
+ * The PART compiler: a template layout's placed parts → wire claims + section
3
+ * projections.
5
4
  */
6
5
 
7
6
  import type { UnoverseDefinition } from "../../model.js";
8
7
 
9
- /** Expand a template layout's THREE-WORD sections (§9b, 2026-08-29; one director word
10
- * since 2026-09-02) into the wire primitives the renderer already speaks, collecting
11
- * the section projections as it goes. `static:`/`copywriter:` link a component
12
- * (face = its basename) and become a limit-1 claim on that face; `director:` with a
13
- * `component` is a SEARCH place (same limit-1 claim; the grammar rides the section for
14
- * the fill path), and without one it is the open DELIVERY claim — its face is the
15
- * `state:` pin when authored (arrivals are switched in and held), else the owning
16
- * state's name (the free name-match dance). The SDK stays dumb: authors never see
17
- * ComponentSlot/select/where, and the wire never sees the three words. */
8
+ /** The one way a layout places a part: a `Ref` into the template's own `components/`. */
9
+ const PART_PREFIX = "components/";
10
+
11
+ /** Expand a template layout's placed parts (§9b) into the wire primitives the renderer
12
+ * already speaks, collecting the section projections as it goes. A part is placed with
13
+ * the ordinary element, `type: Ref` + `ref: components/<name>`; it becomes a limit-1
14
+ * claim on its own face (the file's basename). The SDK stays dumb: authors never see
15
+ * ComponentSlot/select/where. What an Agent may write into a part is each prop's
16
+ * `input`, never the element that places it.
17
+ *
18
+ * RETIRED VOCABULARY IS A LOUD FAILURE, never a legacy read (the no-shims law): a stale
19
+ * layout fails at load with the fix in the message. */
18
20
  export function expandTemplateSections(
19
21
  node: unknown,
20
- owningState: string,
21
22
  templateName: string,
22
23
  out: NonNullable<UnoverseDefinition["sections"]>[string],
23
24
  ): unknown {
24
- if (Array.isArray(node)) return node.map((c) => expandTemplateSections(c, owningState, templateName, out));
25
+ if (Array.isArray(node)) return node.map((c) => expandTemplateSections(c, templateName, out));
25
26
  if (!node || typeof node !== "object") return node;
26
27
  const n = node as Record<string, unknown>;
27
- const linked = typeof n.static === "string" ? "static" : typeof n.copywriter === "string" ? "copywriter" : null;
28
- if (linked) {
29
- const path = String(n[linked]);
28
+ for (const word of ["static", "director", "copywriter", "query"] as const) {
29
+ if (n[word] !== undefined)
30
+ throw new Error(
31
+ `template "${templateName}": the ${word}: word 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")`,
32
+ );
33
+ }
34
+ if (n.type === "Ref" && typeof n.ref === "string" && n.ref.startsWith(PART_PREFIX)) {
35
+ const path = n.ref;
30
36
  const face = path.split("/").pop() ?? path;
31
- out.push({ kind: linked, face, component: path, ref: `${templateName}/${face}` });
32
- const { static: _s, copywriter: _c, ...rest } = n;
37
+ out.push({ kind: "part", face, component: path, ref: `${templateName}/${face}` });
38
+ const { type: _t, ref: _r, ...rest } = n;
33
39
  return {
34
40
  type: "ComponentSlot",
35
41
  select: { from: "all", where: { field: "state", eq: face }, limit: 1 },
36
42
  ...rest,
37
43
  };
38
44
  }
39
- // RETIRED VOCABULARY IS A LOUD FAILURE, never a legacy-read (the no-shims law).
40
- // `query:` merged into `director:` 2026-09-02 (§9e rule 10b REVERSED); `limit` and
41
- // `ask` retired the same day (`pick` is the one cap word; the question is derived,
42
- // rule 10d). A stale file fails at load with the fix in the message.
43
- if (n.query !== undefined) {
44
- throw new Error(`template "${templateName}": the query: word is retired (§9e rule 10b reversed 2026-09-02) — spell the place director: with the same component/items/type/include/pick`);
45
- }
46
- if (n.director !== undefined && (n.director === null || (typeof n.director === "object" && !Array.isArray(n.director)))) {
47
- const d = (n.director ?? {}) as Record<string, unknown>;
48
- if (d.limit !== undefined) throw new Error(`template "${templateName}": director limit is retired — the cap word is pick everywhere (§9e rule 9, renamed 2026-09-02)`);
49
- if (d.ask !== undefined || d.query !== undefined) throw new Error(`template "${templateName}": a director place never states its question — it is derived (§9e rule 10d): the writing, the page's pool, the component's contract, or the guest's ask`);
50
- /**
51
- * A SEARCH PLACE (§9e, one word since 2026-09-02) — the director given a component.
52
- *
53
- * It LINKS the part exactly as `static:`/`copywriter:` do, so the slot is identical: a
54
- * limit-1 claim on the part's own face. The content is a search the page runs itself,
55
- * before any model has spoken; the QUESTION is never here (rule 10d — the writer's
56
- * headings for an `items` place, the page's pool for a gallery, the component's own
57
- * contract, or the guest's ask). The layout says only what KIND may answer (`include`)
58
- * and how many survive (`pick`); none of it reaches the slot — it rides the section
59
- * for the fill path.
60
- *
61
- * NOTHING DECLARES WHICH PROP TAKES THE ROWS. The schema already says: the prop whose
62
- * items require only `ref`. So a searched part keeps BOTH lanes — its refs filled by
63
- * search at shell open with no model, its headings and taglines written by the
64
- * copywriter afterwards (§9e rule 10). Same part, both lanes, in that order.
65
- */
66
- if (typeof d.component === "string" && d.component) {
67
- const path = String(d.component);
68
- const face = path.split("/").pop() ?? path;
69
- // AN ITEMS PLACE (§9e rule 10c): the place holds a LIST the writer shapes — each
70
- // item's heading arrives WITH its own search, so they can never disagree.
71
- //
72
- // THE SEARCH WORDS SIT ON THE THING THAT SEARCHES (owner ruling 2026-09-02): each
73
- // ITEM searches, so type/include/pick nest INSIDE items: beside min/max, and pick
74
- // reads unambiguously as per-item. The flat spelling on an items place is a LOUD
75
- // failure — before this, flat `pick` meant two different scopes depending on
76
- // whether items: happened to be present.
77
- const items = d.items && typeof d.items === "object" && !Array.isArray(d.items) ? (d.items as Record<string, unknown>) : null;
78
- if (items && (d.type !== undefined || d.include !== undefined || d.pick !== undefined))
79
- throw new Error(
80
- `template "${templateName}": an items place nests its search words INSIDE items: (each ITEM searches) — items: { min, max, type, include, pick }`,
81
- );
82
- const src = items ?? d;
83
- // THE INCLUDE WORDS ARE CLOSED: products, images, needs. An unknown word used to
84
- // pass through and match NOTHING at fill time (or everything, with no filter), which
85
- // is a page drawn wrong with no error anywhere. `services` is retired (2026-09-07).
86
- if (Array.isArray(src.include))
87
- for (const w of src.include)
88
- if (!["products", "images", "needs"].includes(String(w)))
89
- throw new Error(`template "${templateName}": include: [${String(w)}] is not a kind a place can hold — the words are products, images, needs${String(w) === "services" ? " (services was renamed products, 2026-09-07)" : ""}`);
90
- out.push({
91
- kind: "director",
92
- face,
93
- component: path,
94
- ref: `${templateName}/${face}`,
95
- ...(src.type === "discovery" || src.type === "intent" ? { type: src.type } : {}),
96
- ...(Array.isArray(src.include) ? { include: src.include.map(String) } : {}),
97
- ...(typeof src.pick === "number" ? { pick: src.pick } : {}),
98
- ...(items ? { items: { min: Number(items.min) || 1, max: Number(items.max) || 1 } } : {}),
99
- });
100
- const { director: _d, ...rest } = n;
101
- return {
102
- type: "ComponentSlot",
103
- select: { from: "all", where: { field: "state", eq: face }, limit: 1 },
104
- ...rest,
105
- };
106
- }
107
- /**
108
- * THE COMPILER ADDRESSES THE PLACE (§9d, ruled 2026-08-31).
109
- *
110
- * A `director:` place is somewhere things may go — it carries no name, no prose and no
111
- * state, because the ONE mind reading the page's brief already knows what the page is.
112
- * But a slot still has to select SOMETHING, and two places on one layout may not claim
113
- * the same face (the reaction contract's one-view-one-surface rule). So the address is
114
- * handed out here, by position, and an author never types a face name.
115
- *
116
- * An AUTHORED `state:` still wins, for the flat-email case §9b rule 4 names — arrivals
117
- * pinned into one declared state — and for any template written before this ruling.
118
- */
119
- const nth = out.filter((sec) => sec.kind === "director" && !sec.component).length;
120
- const face = typeof d.state === "string" ? d.state : nth === 0 ? owningState : `${owningState}-${nth + 1}`;
121
- out.push({
122
- kind: "director",
123
- face,
124
- ...(typeof d.state === "string" ? { state: d.state } : {}),
125
- ...(typeof d.pick === "number" ? { pick: d.pick } : {}),
126
- ...(Array.isArray(d.preview) ? { preview: d.preview.map(String) } : {}),
127
- });
128
- const { director: _d, ...rest } = n;
129
- return {
130
- type: "ComponentSlot",
131
- select: { from: "all", where: { field: "state", eq: face }, ...(typeof d.pick === "number" ? { limit: d.pick } : {}) },
132
- ...rest,
133
- };
134
- }
135
45
  const o: Record<string, unknown> = {};
136
- for (const [k2, v] of Object.entries(n)) o[k2] = expandTemplateSections(v, owningState, templateName, out);
46
+ for (const [k2, v] of Object.entries(n)) o[k2] = expandTemplateSections(v, templateName, out);
137
47
  return o;
138
48
  }