@unoverse-platform/studio 0.1.90 → 0.1.91

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 (130) hide show
  1. package/local/plugin.ts +20 -12
  2. package/package.json +13 -3
  3. package/vendor/base/src/agent-mcp/discovery.ts +203 -0
  4. package/vendor/base/src/agent-mcp/handoff.ts +56 -0
  5. package/vendor/base/src/agent-mcp/index.ts +44 -0
  6. package/vendor/base/src/agent-mcp/invoke.ts +140 -0
  7. package/vendor/base/src/agent-mcp/lean.ts +111 -0
  8. package/vendor/base/src/agent-mcp/memoryContext.ts +104 -0
  9. package/vendor/base/src/agent-mcp/memoryIngest.ts +88 -0
  10. package/vendor/base/src/agent-mcp/rowComponents.ts +172 -0
  11. package/vendor/base/src/agent-mcp/tools.ts +134 -0
  12. package/vendor/base/src/agent-mcp/types.ts +130 -0
  13. package/vendor/base/src/auth/auth.ts +278 -0
  14. package/vendor/base/src/auth/authConfig.ts +73 -0
  15. package/vendor/base/src/boot.ts +245 -0
  16. package/vendor/base/src/definitions/definitions.ts +18 -0
  17. package/vendor/base/src/definitions/dsPackage.ts +45 -0
  18. package/vendor/base/src/definitions/fsCache.ts +128 -0
  19. package/vendor/base/src/definitions/index.ts +19 -0
  20. package/vendor/base/src/definitions/inputs.ts +30 -0
  21. package/vendor/base/src/definitions/load/compile/component.ts +80 -0
  22. package/vendor/base/src/definitions/load/compile/index.ts +12 -0
  23. package/vendor/base/src/definitions/load/compile/sections.ts +132 -0
  24. package/vendor/base/src/definitions/load/compile/template.ts +153 -0
  25. package/vendor/base/src/definitions/load/expand/includes.ts +27 -0
  26. package/vendor/base/src/definitions/load/expand/index.ts +17 -0
  27. package/vendor/base/src/definitions/load/expand/inherit.ts +169 -0
  28. package/vendor/base/src/definitions/load/expand/referenced.ts +164 -0
  29. package/vendor/base/src/definitions/load/expand/refs.ts +106 -0
  30. package/vendor/base/src/definitions/load/expand/walk.ts +91 -0
  31. package/vendor/base/src/definitions/load/load.ts +487 -0
  32. package/vendor/base/src/definitions/model.ts +238 -0
  33. package/vendor/base/src/definitions/registry/catalog.ts +643 -0
  34. package/vendor/base/src/definitions/registry/homes.ts +252 -0
  35. package/vendor/base/src/definitions/theme.ts +383 -0
  36. package/vendor/base/src/executor.ts +284 -0
  37. package/vendor/base/src/http/uwsFetchAdapter.ts +260 -0
  38. package/vendor/base/src/items/baseVersion.ts +96 -0
  39. package/vendor/base/src/items/collect.ts +199 -0
  40. package/vendor/base/src/items/fingerprint.ts +34 -0
  41. package/vendor/base/src/items/publish.ts +198 -0
  42. package/vendor/base/src/lint/context.mjs +56 -0
  43. package/vendor/base/src/lint/design/defs.mjs +46 -0
  44. package/vendor/base/src/lint/design/file.mjs +737 -0
  45. package/vendor/base/src/lint/design/index.d.mts +30 -0
  46. package/vendor/base/src/lint/design/index.mjs +996 -0
  47. package/vendor/base/src/lint/design/tokens.mjs +128 -0
  48. package/vendor/base/src/lint/design/vocabulary.mjs +181 -0
  49. package/vendor/base/src/lint/design/walk.mjs +444 -0
  50. package/vendor/base/src/lint/index.mjs +28 -0
  51. package/vendor/base/src/lint/nodes/blocks.mjs +63 -0
  52. package/vendor/base/src/lint/nodes/index.mjs +64 -0
  53. package/vendor/base/src/lint/nodes/load.mjs +126 -0
  54. package/vendor/base/src/lint/nodes/node.mjs +764 -0
  55. package/vendor/base/src/lint/nodes/package.mjs +112 -0
  56. package/vendor/base/src/lint/nodes/schema.mjs +75 -0
  57. package/vendor/base/src/logger.ts +33 -0
  58. package/vendor/base/src/manifests/compose.ts +539 -0
  59. package/vendor/base/src/manifests/executor/callback.ts +107 -0
  60. package/vendor/base/src/manifests/executor/context.ts +107 -0
  61. package/vendor/base/src/manifests/executor/index.ts +47 -0
  62. package/vendor/base/src/manifests/executor/lookup.ts +21 -0
  63. package/vendor/base/src/manifests/executor/memory.ts +62 -0
  64. package/vendor/base/src/manifests/executor/promise.ts +71 -0
  65. package/vendor/base/src/manifests/executor/refire.ts +34 -0
  66. package/vendor/base/src/manifests/executor/send.ts +65 -0
  67. package/vendor/base/src/manifests/executor/session.ts +46 -0
  68. package/vendor/base/src/manifests/executor/toolBridge.ts +266 -0
  69. package/vendor/base/src/manifests/index.ts +280 -0
  70. package/vendor/base/src/manifests/integrity.ts +93 -0
  71. package/vendor/base/src/manifests/rows.ts +33 -0
  72. package/vendor/base/src/manifests/runtime/auth/aws.ts +240 -0
  73. package/vendor/base/src/manifests/runtime/auth/oauth.ts +76 -0
  74. package/vendor/base/src/manifests/runtime/authorize.ts +159 -0
  75. package/vendor/base/src/manifests/runtime/channels/final.ts +171 -0
  76. package/vendor/base/src/manifests/runtime/channels/service.ts +125 -0
  77. package/vendor/base/src/manifests/runtime/channels/workflow.ts +310 -0
  78. package/vendor/base/src/manifests/runtime/context.ts +150 -0
  79. package/vendor/base/src/manifests/runtime/docstore/handlers.ts +547 -0
  80. package/vendor/base/src/manifests/runtime/docstore/index.ts +135 -0
  81. package/vendor/base/src/manifests/runtime/docstore/sectionizer.ts +184 -0
  82. package/vendor/base/src/manifests/runtime/docstore/store.ts +172 -0
  83. package/vendor/base/src/manifests/runtime/docstore/types.ts +60 -0
  84. package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +165 -0
  85. package/vendor/base/src/manifests/runtime/duplex/session.ts +511 -0
  86. package/vendor/base/src/manifests/runtime/events.ts +223 -0
  87. package/vendor/base/src/manifests/runtime/helpers.ts +53 -0
  88. package/vendor/base/src/manifests/runtime/http/allowedHosts.ts +118 -0
  89. package/vendor/base/src/manifests/runtime/http/request.ts +637 -0
  90. package/vendor/base/src/manifests/runtime/http/response.ts +162 -0
  91. package/vendor/base/src/manifests/runtime/index.ts +57 -0
  92. package/vendor/base/src/manifests/runtime/loops/chunk.ts +98 -0
  93. package/vendor/base/src/manifests/runtime/loops/loop.ts +191 -0
  94. package/vendor/base/src/manifests/runtime/loops/paginate.ts +143 -0
  95. package/vendor/base/src/manifests/runtime/loops/poll.ts +169 -0
  96. package/vendor/base/src/manifests/runtime/narrate.ts +53 -0
  97. package/vendor/base/src/manifests/runtime/state.ts +171 -0
  98. package/vendor/base/src/manifests/runtime/templating.ts +110 -0
  99. package/vendor/base/src/manifests/runtime/tools/regionSkills.ts +125 -0
  100. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +542 -0
  101. package/vendor/base/src/manifests/runtime/usage.ts +87 -0
  102. package/vendor/base/src/manifests/source.ts +187 -0
  103. package/vendor/base/src/manifests/watch.ts +67 -0
  104. package/vendor/base/src/markdown/document.ts +141 -0
  105. package/vendor/base/src/paths.ts +180 -0
  106. package/vendor/base/src/platform/audioManager.ts +100 -0
  107. package/vendor/base/src/platform/clientTransport.ts +65 -0
  108. package/vendor/base/src/platform/executionContext.ts +154 -0
  109. package/vendor/base/src/platform/index.ts +10 -0
  110. package/vendor/base/src/platform/pluginAPI.ts +146 -0
  111. package/vendor/base/src/platform/redis.ts +54 -0
  112. package/vendor/base/src/platform/serviceCalls.ts +222 -0
  113. package/vendor/base/src/pluginBase.ts +248 -0
  114. package/vendor/base/src/plugins/README.md +57 -0
  115. package/vendor/base/src/plugins/discovery.ts +87 -0
  116. package/vendor/base/src/plugins/index.ts +16 -0
  117. package/vendor/base/src/plugins/loader.ts +179 -0
  118. package/vendor/base/src/plugins/redis.ts +36 -0
  119. package/vendor/base/src/plugins/state.ts +53 -0
  120. package/vendor/base/src/registry.ts +95 -0
  121. package/vendor/base/src/security/permissions.ts +93 -0
  122. package/vendor/base/src/security/rules.ts +73 -0
  123. package/vendor/base/src/template/SafeExpression.ts +556 -0
  124. package/vendor/base/src/template/StringTemplateResolver.ts +75 -0
  125. package/vendor/base/src/template/index.ts +22 -0
  126. package/vendor/base/src/types.ts +50 -0
  127. package/vendor/base/src/workflow/GetMemoryService.ts +158 -0
  128. package/vendor/base/src/workflow/PlatformMemoryCore.ts +438 -0
  129. package/vendor/base/src/workflow/routing.ts +122 -0
  130. package/vendor/sdk/lib/core/store.ts +26 -6
@@ -0,0 +1,128 @@
1
+ /**
2
+ * mtime-validated definition file cache.
3
+ *
4
+ * The definition + theme stores (definitions.ts, theme.ts) read the same small rx/ files
5
+ * on EVERY MCP resource read/list and every theme fetch — and a single
6
+ * `listDefinitions()` re-reads the same atoms once per composing component. Re-reading +
7
+ * re-parsing them per request is the server's hottest avoidable cost.
8
+ *
9
+ * This caches the PARSED value keyed by absolute path, re-reading ONLY when the file's
10
+ * mtime changes. So hot-reload still works (edit a definition, refresh → mtime moves →
11
+ * re-parse), but a steady-state request pays a single `stat()` instead of open+read+parse.
12
+ *
13
+ * Returned objects are SHARED, not cloned — callers that MUTATE the parsed value MUST
14
+ * clone first (the definition loader does, to expand atom `Ref`s in place); theme reads
15
+ * are read-only. Call only for files known to exist (callers already guard with existsSync);
16
+ * `statSync` throws on a missing path.
17
+ *
18
+ * ---
19
+ *
20
+ * FORMAT: rx definitions are authored as `.yaml` OR `.json` and the two are
21
+ * interchangeable everywhere. YAML is the authoring format (it carries comments and reads
22
+ * without punctuation noise); JSON stays the WIRE format, because definitions are served
23
+ * verbatim as MCP resources to clients that `JSON.parse` them. Nothing downstream of this
24
+ * module knows which extension a definition was authored in.
25
+ *
26
+ * YAML parses to the same data model, so the JSON Schema at `rx/_schema/` validates both
27
+ * with no change. `$schema` is stripped on read: an editor needs the pointer in the file,
28
+ * a client must never receive it in the payload.
29
+ */
30
+ import { readFileSync, readdirSync, statSync, existsSync } from "fs";
31
+ import { join } from "path";
32
+ import { parse as parseYaml } from "yaml";
33
+
34
+ /** Authored definition extensions, in RESOLUTION ORDER. `.yaml` wins so a converted file
35
+ * takes precedence over a `.json` left behind mid-migration. `defPath` is the only place
36
+ * that order is applied — never re-implement it at a call site. */
37
+ export const DEF_EXTS = [".yaml", ".json"] as const;
38
+
39
+ const cache = new Map<string, { mtimeMs: number; data: unknown }>();
40
+
41
+ /** Parsed definition for `path`, cached until the file's mtime changes. Parser is chosen
42
+ * by extension; `$schema` (an editor affordance, never wire data) is dropped. */
43
+ export function readDefCached<T = any>(path: string): T {
44
+ const mtimeMs = statSync(path).mtimeMs;
45
+ const hit = cache.get(path);
46
+ if (hit && hit.mtimeMs === mtimeMs) return hit.data as T;
47
+ const text = readFileSync(path, "utf8");
48
+ const parsed = path.endsWith(".yaml") ? parseYaml(text) : JSON.parse(text);
49
+ if (parsed && typeof parsed === "object" && "$schema" in parsed) delete (parsed as Record<string, unknown>).$schema;
50
+ cache.set(path, { mtimeMs, data: parsed });
51
+ return parsed as T;
52
+ }
53
+
54
+ /**
55
+ * Resolve an EXTENSION-LESS definition path to the file that actually exists, or
56
+ * undefined. `defPath(dir, "card")` finds `card.yaml` then `card.json`.
57
+ *
58
+ * Every definition lookup goes through here so the migration stays invisible: a folder may
59
+ * hold YAML and JSON side by side while it is being converted. Authoring BOTH for one name
60
+ * is an error the linter reports — this resolver silently prefers `.yaml` rather than
61
+ * failing, because a half-converted tree must keep serving.
62
+ */
63
+ export function defPath(dir: string, name: string): string | undefined {
64
+ for (const ext of DEF_EXTS) {
65
+ const p = join(dir, name + ext);
66
+ if (existsSync(p)) return p;
67
+ }
68
+ return undefined;
69
+ }
70
+
71
+ /** True when `file` is an authored definition (either format). Replaces the
72
+ * `endsWith(".json")` filters that used to gate directory scans. */
73
+ export const isDefFile = (file: string): boolean => DEF_EXTS.some((e) => file.endsWith(e));
74
+
75
+ /** A definition filename without its extension: `welcome.yaml` → `welcome`. */
76
+ export const defName = (file: string): string => file.replace(/\.(yaml|json)$/, "");
77
+
78
+ const textCache = new Map<string, { mtimeMs: number; text: string }>();
79
+
80
+ /** File text for `path`, cached until the file's mtime changes. Same contract as
81
+ * readDefCached — for large non-JSON reads (e.g. the single-file webSDK bundle)
82
+ * that would otherwise sync-read megabytes off disk per request. */
83
+ export function readTextCached(path: string): string {
84
+ const mtimeMs = statSync(path).mtimeMs;
85
+ const hit = textCache.get(path);
86
+ if (hit && hit.mtimeMs === mtimeMs) return hit.text;
87
+ const text = readFileSync(path, "utf8");
88
+ textCache.set(path, { mtimeMs, text });
89
+ return text;
90
+ }
91
+
92
+ /**
93
+ * Change signature over a set of directories: every definition file's name+mtime,
94
+ * recursing into subdirectories (definition folders carry `states/` etc. — all
95
+ * shallow). A stat sweep is tens of µs for the rx/ dirs — versus re-running full
96
+ * token/definition resolution per request. Missing dirs contribute their absence
97
+ * (so creating one invalidates).
98
+ *
99
+ * MUST cover both extensions: a signature that only saw `.json` would leave every YAML
100
+ * edit invisible to hot reload, and the staleness would look like a caching bug rather
101
+ * than a missing filter.
102
+ */
103
+ export function dirSignature(dirs: string[]): string {
104
+ const parts: string[] = [];
105
+ const walk = (dir: string) => {
106
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
107
+ if (e.isDirectory()) walk(join(dir, e.name));
108
+ else if (isDefFile(e.name)) parts.push(`${dir}/${e.name}:${statSync(join(dir, e.name)).mtimeMs}`);
109
+ }
110
+ };
111
+ for (const dir of dirs) {
112
+ if (!existsSync(dir)) parts.push(`${dir}:absent`);
113
+ else walk(dir);
114
+ }
115
+ return parts.join(";");
116
+ }
117
+
118
+ const computed = new Map<string, { sig: string; value: unknown }>();
119
+
120
+ /** Memoize `compute()` under `key`, re-running only when `sig` changes.
121
+ * Returned values are SHARED — callers must not mutate them. */
122
+ export function cachedBySignature<T>(key: string, sig: string, compute: () => T): T {
123
+ const hit = computed.get(key);
124
+ if (hit && hit.sig === sig) return hit.value as T;
125
+ const value = compute();
126
+ computed.set(key, { sig, value });
127
+ return value;
128
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * rx DEFINITIONS: components, templates, atoms, themes.
3
+ *
4
+ * The other half of "read the YAML and make it mean something". `manifests/` turns node
5
+ * YAML into a runnable node; this turns rx YAML into the components, templates and tokens
6
+ * an interface is assembled from.
7
+ *
8
+ * IT LIVES HERE SO STUDIO CAN BE PUBLISHED. These four files sat in the platform server, so
9
+ * anything outside this monorepo that wanted to list or render rx content had to reach into
10
+ * `apps/unoverse/server/src` — which nothing installed from npm can do. Studio was blocked
11
+ * on exactly that.
12
+ *
13
+ * READING, not rendering. These read rx off disk; `@unoverse/sdk` renders it. The split
14
+ * matters: the SDK is a dumb renderer and must stay one.
15
+ */
16
+ export { resolveStringTemplate } from "../template/StringTemplateResolver.js";
17
+ export * from "./definitions.js";
18
+ export * from "./theme.js";
19
+ export * from "./inputs.js";
@@ -0,0 +1,30 @@
1
+ /**
2
+ * THE single derivation of "which definition props are workflow INPUTS".
3
+ *
4
+ * A node's `configSchema` is derived FROM the component's `props`, and that derivation has
5
+ * to live in ONE place so node synthesis, the workbench CONTROLS panel and the server never
6
+ * disagree. Consumers do not re-decide what an input is.
7
+ *
8
+ * IMPLEMENTED HERE, not re-exported. This file used to be a single line pulling it out of
9
+ * `@unoverse-platform/plugin-base`, which made the new runtime depend on the legacy node
10
+ * library it exists to replace. Backwards, and it would have travelled into every consumer
11
+ * of this package as a real npm dependency.
12
+ *
13
+ * `plugin-base` still carries its own copy, for the TypeScript nodes that import it. A
14
+ * knowing duplicate: eight lines of a rule that has not changed, and that copy dies with the
15
+ * last code node. THIS is the canonical one.
16
+ *
17
+ * The rule (mirroring the design system's `workflowInput`): a prop is an input when it
18
+ * declares `input: true`. A definition declaring NO `input` flags at all makes every prop an
19
+ * input, which is back-compat for the single-object components that legitimately expose all
20
+ * of them.
21
+ */
22
+ export interface InputPropLike {
23
+ input?: boolean;
24
+ }
25
+
26
+ export function inputPropKeys(props: Record<string, InputPropLike> | undefined): string[] {
27
+ const entries = Object.entries(props ?? {});
28
+ const explicit = entries.some(([, p]) => p?.input !== undefined);
29
+ return entries.filter(([, p]) => (explicit ? p?.input === true : true)).map(([k]) => k);
30
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The COMPONENT-TREE compiler: `states:` → the served projections.
3
+ * Split from compile.ts into this folder 2026-09-02; the rules are unchanged.
4
+ */
5
+
6
+ import type { UnoverseDefinition } from "../../model.js";
7
+
8
+ /** STATE MODEL v2 compile for a COMPONENT def (both authored forms — folder and
9
+ * FLAT single-file, unified 2026-08-29): the tree (top-level `states:`; legacy
10
+ * `state.view`) becomes the served projections (publicStates / initialView /
11
+ * stateTree), the flat scalars the render scope reads, and the SYNTHESIZED root
12
+ * where none is authored. */
13
+ export function compileComponentTree(def: UnoverseDefinition): void {
14
+ // THE RULED FORM (2026-08-29): the tree lives at TOP-LEVEL `states:`
15
+ // like every kind; `state.view` is the legacy wrapper, still read.
16
+ const topStates = (def as unknown as { states?: unknown }).states;
17
+ const topForm = topStates && typeof topStates === "object" && !Array.isArray(topStates)
18
+ ? { states: topStates as Record<string, never> }
19
+ : undefined;
20
+ const tree = (topForm ?? (def.state as Record<string, unknown> | undefined)?.view) as
21
+ | {
22
+ initial?: unknown;
23
+ states?: Record<string, { on?: unknown; initial?: unknown; states?: Record<string, unknown> } | null>;
24
+ }
25
+ | undefined;
26
+ if (!(tree && typeof tree === "object" && tree.states && typeof tree.states === "object")) return;
27
+ const names = Object.keys(tree.states);
28
+ const initial = typeof tree.initial === "string" ? tree.initial : names[0];
29
+ def.publicStates = names;
30
+ def.initialView = initial;
31
+ // The nested projection viewers render from (never scan layouts for it).
32
+ def.stateTree = names.map((n) => {
33
+ const s = (tree.states as Record<string, { layout?: unknown; on?: unknown; initial?: unknown; states?: Record<string, { layout?: unknown } | null> } | null>)[n];
34
+ const entry: NonNullable<UnoverseDefinition["stateTree"]>[number] = { name: n };
35
+ if (s && typeof s === "object") {
36
+ if (typeof s.layout === "string") entry.layout = s.layout;
37
+ if (typeof s.on === "string") entry.on = s.on;
38
+ if (typeof s.initial === "string") entry.initial = s.initial;
39
+ if (s.states && typeof s.states === "object")
40
+ entry.states = Object.entries(s.states).map(([k, v]) => ({
41
+ name: k,
42
+ ...(v && typeof v === "object" && typeof v.layout === "string" ? { layout: v.layout } : {}),
43
+ }));
44
+ }
45
+ return entry;
46
+ });
47
+ // ONE AXIS ON A MIGRATED SLICE. The manifest's `defaultState` is seeded above
48
+ // for legacy components; a component with a tree has already said where it
49
+ // arrives, so the alias is dropped here rather than shipped alongside `view`.
50
+ // Two spellings on one slice is how a stale reader keeps working by accident:
51
+ // it reads the alias, sees the ARRIVAL state forever, and never notices the
52
+ // component moved. (The manifest keeps the key: at app level it is the load
53
+ // mode the router branches on, a different axis with the same name.)
54
+ // STARTING VALUES (owner ruling 2026-08-29 late): the authored home is
55
+ // `values:` — the initial values of keys the component writes (the
56
+ // setValue vocabulary). `state:` scalars are the legacy spelling,
57
+ // still read; either way the SERVED shape is unchanged (def.state).
58
+ const rawValues = (def as unknown as { values?: unknown }).values;
59
+ const { defaultState: _legacyAlias, view: _legacyTree, ...legacyScalars } = (def.state ?? {}) as Record<string, unknown>;
60
+ const authored =
61
+ rawValues && typeof rawValues === "object" && !Array.isArray(rawValues)
62
+ ? (rawValues as Record<string, unknown>)
63
+ : legacyScalars;
64
+ delete (def as unknown as Record<string, unknown>).values;
65
+ // ORDER IS THE PRIORITY, at BOTH levels (2026-08-15). The first state
66
+ // declared is the one the component arrives in, and the first substate
67
+ // declared is the step its `on` axis starts on. An `initial` key said the
68
+ // same thing a second time and was free to contradict the list, which is
69
+ // how a component could declare one arrival and ship another. It is still
70
+ // READ where a def has not been swept, and lint-forbidden in rx.
71
+ const flat: Record<string, unknown> = { ...authored, state: initial };
72
+ delete (def as unknown as Record<string, unknown>).states; // the tree compiled; the raw block never rides the served def
73
+ for (const s of Object.values(tree.states))
74
+ if (s && typeof s === "object" && typeof s.on === "string") {
75
+ const subs = s.states && typeof s.states === "object" ? Object.keys(s.states) : [];
76
+ const step = typeof s.initial === "string" ? s.initial : subs[0];
77
+ if (step) flat[s.on] = step;
78
+ }
79
+ def.state = flat;
80
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The COMPILERS, one file per job (split from compile.ts 2026-09-02, mirroring
3
+ * the expand/ split of 2026-08-30):
4
+ *
5
+ * component.ts a component's `states:` tree → served projections
6
+ * sections.ts a template layout's three words → wire claims + sections
7
+ * template.ts a template's states (folder) or root (block) → layouts/previews
8
+ */
9
+
10
+ export { compileComponentTree } from "./component.js";
11
+ export { expandTemplateSections } from "./sections.js";
12
+ export { synthesizeTemplateRoot } from "./template.js";
@@ -0,0 +1,132 @@
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.
5
+ */
6
+
7
+ import type { UnoverseDefinition } from "../../model.js";
8
+
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. */
18
+ export function expandTemplateSections(
19
+ node: unknown,
20
+ owningState: string,
21
+ templateName: string,
22
+ out: NonNullable<UnoverseDefinition["sections"]>[string],
23
+ ): unknown {
24
+ if (Array.isArray(node)) return node.map((c) => expandTemplateSections(c, owningState, templateName, out));
25
+ if (!node || typeof node !== "object") return node;
26
+ 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]);
30
+ 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;
33
+ return {
34
+ type: "ComponentSlot",
35
+ select: { from: "all", where: { field: "state", eq: face }, limit: 1 },
36
+ ...rest,
37
+ };
38
+ }
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
+ out.push({
84
+ kind: "director",
85
+ face,
86
+ component: path,
87
+ ref: `${templateName}/${face}`,
88
+ ...(src.type === "discovery" || src.type === "intent" ? { type: src.type } : {}),
89
+ ...(Array.isArray(src.include) ? { include: src.include.map(String) } : {}),
90
+ ...(typeof src.pick === "number" ? { pick: src.pick } : {}),
91
+ ...(items ? { items: { min: Number(items.min) || 1, max: Number(items.max) || 1 } } : {}),
92
+ });
93
+ const { director: _d, ...rest } = n;
94
+ return {
95
+ type: "ComponentSlot",
96
+ select: { from: "all", where: { field: "state", eq: face }, limit: 1 },
97
+ ...rest,
98
+ };
99
+ }
100
+ /**
101
+ * THE COMPILER ADDRESSES THE PLACE (§9d, ruled 2026-08-31).
102
+ *
103
+ * A `director:` place is somewhere things may go — it carries no name, no prose and no
104
+ * state, because the ONE mind reading the page's brief already knows what the page is.
105
+ * But a slot still has to select SOMETHING, and two places on one layout may not claim
106
+ * the same face (the reaction contract's one-view-one-surface rule). So the address is
107
+ * handed out here, by position, and an author never types a face name.
108
+ *
109
+ * An AUTHORED `state:` still wins, for the flat-email case §9b rule 4 names — arrivals
110
+ * pinned into one declared state — and for any template written before this ruling.
111
+ */
112
+ const nth = out.filter((sec) => sec.kind === "director" && !sec.component).length;
113
+ const face = typeof d.state === "string" ? d.state : nth === 0 ? owningState : `${owningState}-${nth + 1}`;
114
+ out.push({
115
+ kind: "director",
116
+ face,
117
+ ...(typeof d.state === "string" ? { state: d.state } : {}),
118
+ ...(typeof d.pick === "number" ? { pick: d.pick } : {}),
119
+ ...(typeof d.rules === "string" ? { rules: d.rules } : {}),
120
+ ...(Array.isArray(d.preview) ? { preview: d.preview.map(String) } : {}),
121
+ });
122
+ const { director: _d, ...rest } = n;
123
+ return {
124
+ type: "ComponentSlot",
125
+ select: { from: "all", where: { field: "state", eq: face }, ...(typeof d.pick === "number" ? { limit: d.pick } : {}) },
126
+ ...rest,
127
+ };
128
+ }
129
+ const o: Record<string, unknown> = {};
130
+ for (const [k2, v] of Object.entries(n)) o[k2] = expandTemplateSections(v, owningState, templateName, out);
131
+ return o;
132
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * The TEMPLATE-ROOT compiler: a template's states (folder form) or root (a flat
3
+ * BLOCK) → the served layouts, sections, previews and root. Split from compile.ts
4
+ * into this folder 2026-09-02; the dead `parts:`/`shapes:` legacy tails (retired
5
+ * §9b vocabulary no definition authored any more) were deleted in the same move.
6
+ */
7
+
8
+ import type { UnoverseDefinition } from "../../model.js";
9
+ import { loadDefinition } from "../load.js";
10
+ import { composeIncludes } from "../expand/index.js";
11
+ import { expandTemplateSections } from "./sections.js";
12
+
13
+ /**
14
+ * ONE PREVIEWED INSTANCE'S SEED — `preview ?? default` per prop, the same values the
15
+ * Components screen has always seeded. An instance seeded with nothing draws as a page of
16
+ * ghosts, which is how a finished arrangement comes to read as a broken one.
17
+ *
18
+ * Shared by both authored forms (a folder's states and a flat block's root): a mock that
19
+ * differed by which form a template happened to be written in would be a preview nobody
20
+ * could trust.
21
+ */
22
+ function seedFor(ref: string, org?: string): Record<string, unknown> | undefined {
23
+ const part = loadDefinition(ref, "component", "expanded", org);
24
+ const seed = Object.fromEntries(
25
+ Object.entries((part?.props ?? {}) as Record<string, { preview?: unknown; default?: unknown; type?: string }>).map(
26
+ ([k, v]) => [k, v?.preview ?? v?.default ?? (v?.type === "array" ? [] : v?.type === "object" ? {} : "")],
27
+ ),
28
+ );
29
+ return Object.keys(seed).length ? seed : undefined;
30
+ }
31
+
32
+ /** Stamp a section list's Studio stand-ins: which refs each face previews, seeded
33
+ * `preview ?? default` (keyed by REF, because a director band names several and may
34
+ * name one twice). A linked part previews ITSELF; a delivery band previews whatever
35
+ * its `preview:` names. */
36
+ function seedSections(
37
+ secs: NonNullable<UnoverseDefinition["sections"]>[string],
38
+ org: string | undefined,
39
+ previews: Record<string, string[]>,
40
+ ): void {
41
+ for (const s of secs) {
42
+ // A LINKED part previews ITSELF — including a director SEARCH place, which carries a
43
+ // ref since the one-word merge (a kind check here hid every search place's mock the
44
+ // day the kinds merged). Only an unlinked DELIVERY band previews its `preview:` list.
45
+ const refs = s.ref ? [s.ref] : s.kind === "director" ? (s.preview ?? []) : [];
46
+ if (s.ref) previews[s.face] = [s.ref];
47
+ else if (s.preview?.length) previews[s.face] = s.preview;
48
+ const seeds: Record<string, Record<string, unknown>> = {};
49
+ for (const ref of new Set(refs)) {
50
+ const seed = seedFor(ref, org);
51
+ if (seed) seeds[ref] = seed;
52
+ }
53
+ if (Object.keys(seeds).length) s.seeds = seeds;
54
+ }
55
+ }
56
+
57
+ export function synthesizeTemplateRoot(def: UnoverseDefinition, folderDir?: string): void {
58
+ if (def.root) {
59
+ /**
60
+ * A BLOCK — a flat template that declares a DRAWING instead of a tree (2026-08-31).
61
+ *
62
+ * Its three words still have to compile. Without this the `director:` line sat in the
63
+ * served root verbatim, unexpanded and inert: the renderer speaks ComponentSlot and
64
+ * knows no such word, so a block drew as an empty box wherever it was used (observed
65
+ * in Studio the moment the first one was authored). The folder form compiles its
66
+ * sections as it composes each state's layout; the flat form had no such pass.
67
+ *
68
+ * Compiled into the SAME projections a one-state template serves, so nothing
69
+ * downstream has to know which form it was authored in: `layouts`, `defaultLayout`,
70
+ * `sections`, `publicStates`. The author still writes only `root:`.
71
+ */
72
+ if ((def as unknown as { states?: unknown }).states === undefined && def.layouts === undefined) {
73
+ const secs: NonNullable<UnoverseDefinition["sections"]>[string] = [];
74
+ const compiled = expandTemplateSections(def.root, def.name, def.name, secs);
75
+ if (secs.length) {
76
+ const previews: Record<string, string[]> = {};
77
+ seedSections(secs, def.org, previews);
78
+ if (Object.keys(previews).length) def.preview = { ...(def.preview ?? {}), ...previews };
79
+ }
80
+ // The projections are built WHETHER OR NOT the root carries section words
81
+ // (2026-09-02): a pure-arrangement block — `band`, `two-column`, style and an
82
+ // opening, no fill lane — is still a template, and without a layout Studio drew it
83
+ // as a blank canvas (observed the day blocks returned). Its own authored children
84
+ // are its preview.
85
+ def.root = compiled as UnoverseDefinition["root"];
86
+ def.layouts = { [def.name]: compiled as never };
87
+ def.defaultLayout = def.name;
88
+ if (secs.length) def.sections = { [def.name]: secs };
89
+ def.publicStates = [def.name];
90
+ def.initialView = def.name;
91
+ }
92
+ return;
93
+ }
94
+ // STATES SERVE THE APP-RUNTIME CONTRACT (§9b RULED 2026-08-29): a template's states
95
+ // compose into def.layouts — sections expanded to wire ComponentSlots, no baked
96
+ // mocks — with the FIRST state as the base (def.root + defaultLayout) and the rest
97
+ // as the reaction ladder (def.stateOrder). The SDK then does everything the rules
98
+ // say: an instance's face change surfaces the name-matched layout, its claim catches
99
+ // the instance, steps stay contained in the instance's own slice. Studio seeds real
100
+ // instances from the compiled sections and the same runtime runs.
101
+ const rawStates = (def as unknown as { states?: unknown }).states;
102
+ const states: Record<string, { layout?: unknown } | null> =
103
+ rawStates && typeof rawStates === "object" && !Array.isArray(rawStates)
104
+ ? (rawStates as Record<string, { layout?: unknown } | null>)
105
+ : {};
106
+ const shapeNames = Object.keys(states);
107
+ if (!folderDir || !shapeNames.length) return;
108
+ const layouts: Record<string, unknown> = {};
109
+ const sections: NonNullable<UnoverseDefinition["sections"]> = {};
110
+ const compiledPreview: Record<string, string[]> = {};
111
+ // THE LATCH IS A FACT ABOUT A STATE (MCP guide §The Component Latch). Collected on the
112
+ // same pass as the layouts and stamped as `def.latches`, because `states` is deleted
113
+ // below: a state that says nothing here can never be addressed, and an instance moving
114
+ // between states gains and loses the pill with no eviction and nothing to unregister.
115
+ const latches: NonNullable<UnoverseDefinition["latches"]> = {};
116
+ for (const name of shapeNames) {
117
+ const st = (states[name] ?? {}) as Record<string, unknown>;
118
+ const latch = st.latch as { title?: unknown; icon?: unknown } | undefined;
119
+ // TITLE OR NOTHING: the pill's whole content is the title, so a latch without one
120
+ // draws a nameless pill the guest cannot read. Declared but empty is an authoring
121
+ // slip, and taking it literally is what makes the slip visible.
122
+ if (latch && typeof latch === "object" && typeof latch.title === "string" && latch.title.trim())
123
+ latches[name] = { title: latch.title, ...(typeof latch.icon === "string" ? { icon: latch.icon } : {}) };
124
+ if (typeof st.layout !== "string") continue;
125
+ const secs: NonNullable<UnoverseDefinition["sections"]>[string] = [];
126
+ layouts[name] = expandTemplateSections(composeIncludes({ $include: st.layout }, folderDir), name, def.name, secs);
127
+ sections[name] = secs;
128
+ // MOCK VALUES TRAVEL WITH THE SECTION — a preview host seeds instances from the wire.
129
+ seedSections(secs, def.org, compiledPreview);
130
+ }
131
+ if (!Object.keys(layouts).length) return;
132
+ def.layouts = layouts;
133
+ def.defaultLayout = shapeNames[0];
134
+ def.stateOrder = shapeNames.slice(1);
135
+ // THE FACE IT ARRIVES WEARING (2026-08-30). A template's states compiled into
136
+ // layouts and stopped there, so it carried no `publicStates`/`initialView` — the
137
+ // two projections that say "this is my interface, and this is the face I arrive
138
+ // in". Every consumer of a face was therefore blind to it: a preview could not
139
+ // seed it, a state chip could not activate it, and — the one that matters — an
140
+ // app's ladder had nothing to react to when a template STREAMED in, so a
141
+ // discovered page landed inline in the conversation instead of taking its
142
+ // surface. Stamped exactly as the component tier stamps them, because it is the
143
+ // same contract: states are the interface, the first declared one is arrival.
144
+ def.publicStates = shapeNames;
145
+ def.initialView = shapeNames[0];
146
+ if (Object.keys(latches).length) def.latches = latches;
147
+ if (Object.keys(sections).length) def.sections = sections;
148
+ if (Object.keys(compiledPreview).length) def.preview = { ...(def.preview ?? {}), ...compiledPreview };
149
+ delete (def as unknown as Record<string, unknown>).states;
150
+ // The base arrangement, wrapped in a container so responsive columns can
151
+ // query their surface width in any host.
152
+ def.root = { type: "Box", style: { width: "full", container: "inline-size" }, children: [layouts[shapeNames[0]]] };
153
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `$include` COMPOSITION: a definition's siblings, inlined.
3
+ *
4
+ * The authoring-time half of composition — a layout pulls in `components/header`, a state
5
+ * pulls in a shared shape. Resolved against the definition's own folder, recursively.
6
+ *
7
+ * Split from expand.ts 2026-08-30; the rules are unchanged.
8
+ */
9
+ import { readDefCached, defPath } from "../../fsCache.js";
10
+ import type { AnyNode } from "./refs.js";
11
+
12
+ export function composeIncludes(value: any, folderDir: string): any {
13
+ if (Array.isArray(value)) return value.map((v) => composeIncludes(v, folderDir));
14
+ if (value && typeof value === "object") {
15
+ if (typeof value.$include === "string") {
16
+ const incPath = defPath(folderDir, value.$include);
17
+ // Name the missing include: the raw ENOENT names a `.yaml` that was never meant to
18
+ // exist, which reads as a resolver bug rather than a broken reference.
19
+ if (!incPath) throw new Error(`$include "${value.$include}" resolves to no file in ${folderDir}`);
20
+ return composeIncludes(readDefCached(incPath), folderDir);
21
+ }
22
+ const out: AnyNode = {};
23
+ for (const [k, v] of Object.entries(value)) out[k] = composeIncludes(v, folderDir);
24
+ return out;
25
+ }
26
+ return value;
27
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * COMPOSITION — the surface. Split by JOB 2026-08-30, the way `markdown/` and
3
+ * `agent-mcp/` are: `expand.ts` had grown to five unrelated jobs in one file.
4
+ *
5
+ * refs.ts inlining a Ref: the field remaps and the `with` literals
6
+ * inherit.ts what an embedded piece carries up (its state, its prop contracts)
7
+ * walk.ts the expansion itself
8
+ * referenced.ts the reference-don't-copy form (each atom shipped once)
9
+ * includes.ts `$include` composition
10
+ *
11
+ * Every export keeps its name, so nothing that imports composition had to change.
12
+ */
13
+ export { remapFields, applyLiterals, type AnyNode } from "./refs.js";
14
+ export { machineryKeys, inheritState, inheritProps, mergeInheritedProps } from "./inherit.js";
15
+ export { expandNode, expandRefs } from "./walk.js";
16
+ export { referencedDefinition } from "./referenced.js";
17
+ export { composeIncludes } from "./includes.js";