@unoverse-platform/studio 0.1.89 → 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 (132) hide show
  1. package/local/plugin.ts +20 -12
  2. package/package.json +13 -3
  3. package/screens/TemplatesLibraryView.tsx +31 -6
  4. package/screens/TemplatesView.tsx +1 -1
  5. package/vendor/base/src/agent-mcp/discovery.ts +203 -0
  6. package/vendor/base/src/agent-mcp/handoff.ts +56 -0
  7. package/vendor/base/src/agent-mcp/index.ts +44 -0
  8. package/vendor/base/src/agent-mcp/invoke.ts +140 -0
  9. package/vendor/base/src/agent-mcp/lean.ts +111 -0
  10. package/vendor/base/src/agent-mcp/memoryContext.ts +104 -0
  11. package/vendor/base/src/agent-mcp/memoryIngest.ts +88 -0
  12. package/vendor/base/src/agent-mcp/rowComponents.ts +172 -0
  13. package/vendor/base/src/agent-mcp/tools.ts +134 -0
  14. package/vendor/base/src/agent-mcp/types.ts +130 -0
  15. package/vendor/base/src/auth/auth.ts +278 -0
  16. package/vendor/base/src/auth/authConfig.ts +73 -0
  17. package/vendor/base/src/boot.ts +245 -0
  18. package/vendor/base/src/definitions/definitions.ts +18 -0
  19. package/vendor/base/src/definitions/dsPackage.ts +45 -0
  20. package/vendor/base/src/definitions/fsCache.ts +128 -0
  21. package/vendor/base/src/definitions/index.ts +19 -0
  22. package/vendor/base/src/definitions/inputs.ts +30 -0
  23. package/vendor/base/src/definitions/load/compile/component.ts +80 -0
  24. package/vendor/base/src/definitions/load/compile/index.ts +12 -0
  25. package/vendor/base/src/definitions/load/compile/sections.ts +132 -0
  26. package/vendor/base/src/definitions/load/compile/template.ts +153 -0
  27. package/vendor/base/src/definitions/load/expand/includes.ts +27 -0
  28. package/vendor/base/src/definitions/load/expand/index.ts +17 -0
  29. package/vendor/base/src/definitions/load/expand/inherit.ts +169 -0
  30. package/vendor/base/src/definitions/load/expand/referenced.ts +164 -0
  31. package/vendor/base/src/definitions/load/expand/refs.ts +106 -0
  32. package/vendor/base/src/definitions/load/expand/walk.ts +91 -0
  33. package/vendor/base/src/definitions/load/load.ts +487 -0
  34. package/vendor/base/src/definitions/model.ts +238 -0
  35. package/vendor/base/src/definitions/registry/catalog.ts +643 -0
  36. package/vendor/base/src/definitions/registry/homes.ts +252 -0
  37. package/vendor/base/src/definitions/theme.ts +383 -0
  38. package/vendor/base/src/executor.ts +284 -0
  39. package/vendor/base/src/http/uwsFetchAdapter.ts +260 -0
  40. package/vendor/base/src/items/baseVersion.ts +96 -0
  41. package/vendor/base/src/items/collect.ts +199 -0
  42. package/vendor/base/src/items/fingerprint.ts +34 -0
  43. package/vendor/base/src/items/publish.ts +198 -0
  44. package/vendor/base/src/lint/context.mjs +56 -0
  45. package/vendor/base/src/lint/design/defs.mjs +46 -0
  46. package/vendor/base/src/lint/design/file.mjs +737 -0
  47. package/vendor/base/src/lint/design/index.d.mts +30 -0
  48. package/vendor/base/src/lint/design/index.mjs +996 -0
  49. package/vendor/base/src/lint/design/tokens.mjs +128 -0
  50. package/vendor/base/src/lint/design/vocabulary.mjs +181 -0
  51. package/vendor/base/src/lint/design/walk.mjs +444 -0
  52. package/vendor/base/src/lint/index.mjs +28 -0
  53. package/vendor/base/src/lint/nodes/blocks.mjs +63 -0
  54. package/vendor/base/src/lint/nodes/index.mjs +64 -0
  55. package/vendor/base/src/lint/nodes/load.mjs +126 -0
  56. package/vendor/base/src/lint/nodes/node.mjs +764 -0
  57. package/vendor/base/src/lint/nodes/package.mjs +112 -0
  58. package/vendor/base/src/lint/nodes/schema.mjs +75 -0
  59. package/vendor/base/src/logger.ts +33 -0
  60. package/vendor/base/src/manifests/compose.ts +539 -0
  61. package/vendor/base/src/manifests/executor/callback.ts +107 -0
  62. package/vendor/base/src/manifests/executor/context.ts +107 -0
  63. package/vendor/base/src/manifests/executor/index.ts +47 -0
  64. package/vendor/base/src/manifests/executor/lookup.ts +21 -0
  65. package/vendor/base/src/manifests/executor/memory.ts +62 -0
  66. package/vendor/base/src/manifests/executor/promise.ts +71 -0
  67. package/vendor/base/src/manifests/executor/refire.ts +34 -0
  68. package/vendor/base/src/manifests/executor/send.ts +65 -0
  69. package/vendor/base/src/manifests/executor/session.ts +46 -0
  70. package/vendor/base/src/manifests/executor/toolBridge.ts +266 -0
  71. package/vendor/base/src/manifests/index.ts +280 -0
  72. package/vendor/base/src/manifests/integrity.ts +93 -0
  73. package/vendor/base/src/manifests/rows.ts +33 -0
  74. package/vendor/base/src/manifests/runtime/auth/aws.ts +240 -0
  75. package/vendor/base/src/manifests/runtime/auth/oauth.ts +76 -0
  76. package/vendor/base/src/manifests/runtime/authorize.ts +159 -0
  77. package/vendor/base/src/manifests/runtime/channels/final.ts +171 -0
  78. package/vendor/base/src/manifests/runtime/channels/service.ts +125 -0
  79. package/vendor/base/src/manifests/runtime/channels/workflow.ts +310 -0
  80. package/vendor/base/src/manifests/runtime/context.ts +150 -0
  81. package/vendor/base/src/manifests/runtime/docstore/handlers.ts +547 -0
  82. package/vendor/base/src/manifests/runtime/docstore/index.ts +135 -0
  83. package/vendor/base/src/manifests/runtime/docstore/sectionizer.ts +184 -0
  84. package/vendor/base/src/manifests/runtime/docstore/store.ts +172 -0
  85. package/vendor/base/src/manifests/runtime/docstore/types.ts +60 -0
  86. package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +165 -0
  87. package/vendor/base/src/manifests/runtime/duplex/session.ts +511 -0
  88. package/vendor/base/src/manifests/runtime/events.ts +223 -0
  89. package/vendor/base/src/manifests/runtime/helpers.ts +53 -0
  90. package/vendor/base/src/manifests/runtime/http/allowedHosts.ts +118 -0
  91. package/vendor/base/src/manifests/runtime/http/request.ts +637 -0
  92. package/vendor/base/src/manifests/runtime/http/response.ts +162 -0
  93. package/vendor/base/src/manifests/runtime/index.ts +57 -0
  94. package/vendor/base/src/manifests/runtime/loops/chunk.ts +98 -0
  95. package/vendor/base/src/manifests/runtime/loops/loop.ts +191 -0
  96. package/vendor/base/src/manifests/runtime/loops/paginate.ts +143 -0
  97. package/vendor/base/src/manifests/runtime/loops/poll.ts +169 -0
  98. package/vendor/base/src/manifests/runtime/narrate.ts +53 -0
  99. package/vendor/base/src/manifests/runtime/state.ts +171 -0
  100. package/vendor/base/src/manifests/runtime/templating.ts +110 -0
  101. package/vendor/base/src/manifests/runtime/tools/regionSkills.ts +125 -0
  102. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +542 -0
  103. package/vendor/base/src/manifests/runtime/usage.ts +87 -0
  104. package/vendor/base/src/manifests/source.ts +187 -0
  105. package/vendor/base/src/manifests/watch.ts +67 -0
  106. package/vendor/base/src/markdown/document.ts +141 -0
  107. package/vendor/base/src/paths.ts +180 -0
  108. package/vendor/base/src/platform/audioManager.ts +100 -0
  109. package/vendor/base/src/platform/clientTransport.ts +65 -0
  110. package/vendor/base/src/platform/executionContext.ts +154 -0
  111. package/vendor/base/src/platform/index.ts +10 -0
  112. package/vendor/base/src/platform/pluginAPI.ts +146 -0
  113. package/vendor/base/src/platform/redis.ts +54 -0
  114. package/vendor/base/src/platform/serviceCalls.ts +222 -0
  115. package/vendor/base/src/pluginBase.ts +248 -0
  116. package/vendor/base/src/plugins/README.md +57 -0
  117. package/vendor/base/src/plugins/discovery.ts +87 -0
  118. package/vendor/base/src/plugins/index.ts +16 -0
  119. package/vendor/base/src/plugins/loader.ts +179 -0
  120. package/vendor/base/src/plugins/redis.ts +36 -0
  121. package/vendor/base/src/plugins/state.ts +53 -0
  122. package/vendor/base/src/registry.ts +95 -0
  123. package/vendor/base/src/security/permissions.ts +93 -0
  124. package/vendor/base/src/security/rules.ts +73 -0
  125. package/vendor/base/src/template/SafeExpression.ts +556 -0
  126. package/vendor/base/src/template/StringTemplateResolver.ts +75 -0
  127. package/vendor/base/src/template/index.ts +22 -0
  128. package/vendor/base/src/types.ts +50 -0
  129. package/vendor/base/src/workflow/GetMemoryService.ts +158 -0
  130. package/vendor/base/src/workflow/PlatformMemoryCore.ts +438 -0
  131. package/vendor/base/src/workflow/routing.ts +122 -0
  132. package/vendor/sdk/lib/core/store.ts +26 -6
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Boot report — ONE readout at the end of startup, instead of a line per thing loaded.
3
+ *
4
+ * Boot used to narrate itself: 84 node ticks, 24 credential ticks, 27 "local wins"
5
+ * lines, each registration announcing itself as it happened. Two problems with that.
6
+ * It reads as noise rather than as a product, and a real failure (the plugin-state
7
+ * fetch that fails silently below) sits in the middle of it styled exactly like a
8
+ * success tick, so nobody sees it.
9
+ *
10
+ * The rule here: COUNT the healthy, NAME the anomalous. A node that loaded is a
11
+ * number; a node that was skipped, shadowed, disabled or missing metadata is a line
12
+ * with its name on it. Nothing diagnostic is lost, because the per-item detail is
13
+ * still emitted at debug level (LOG_LEVEL=debug) and the full catalog is one
14
+ * `GET /nodes` away.
15
+ *
16
+ * Collect from anywhere during boot, print once from index.ts.
17
+ */
18
+
19
+ const DEBUG = (process.env.LOG_LEVEL ?? "").toLowerCase() === "debug";
20
+
21
+ // The readout is a product surface: labels recede, values read, addresses invite the
22
+ // click. One accent, no boxes, and everything still greps clean because whole segments
23
+ // are wrapped rather than words. NO_COLOR is the standard opt-out.
24
+ const COLOR = !process.env.NO_COLOR;
25
+ const paint = (code: string) => (s: string) => (COLOR ? `\x1b[${code}m${s}\x1b[0m` : s);
26
+ const bold = paint("1");
27
+ const dim = paint("2");
28
+ const cyan = paint("36");
29
+ const yellow = paint("33");
30
+ const red = paint("31");
31
+
32
+ type Notice = { level: "warn" | "error"; text: string };
33
+ type Endpoint = { label: string; url: string; note?: string };
34
+
35
+ const state = {
36
+ startedAt: Date.now(),
37
+ packages: 0,
38
+ nodes: [] as string[],
39
+ credentials: [] as string[],
40
+ credentialsSkipped: 0,
41
+ components: null as { node: string; count: number } | null,
42
+ manifests: null as { nodes: number; credentials: number; shadowed: string[]; disabled: string[] } | null,
43
+ pluginStates: 0,
44
+ pluginStateError: null as string | null,
45
+ pluginsLocalWin: [] as string[],
46
+ pluginsFromNpm: [] as string[],
47
+ pluginsDisabled: [] as string[],
48
+ auth: null as string | null,
49
+ redis: null as string | null,
50
+ engine: null as string | null,
51
+ watching: null as string | null,
52
+ endpoints: [] as Endpoint[],
53
+ notices: [] as Notice[],
54
+ printed: false,
55
+ };
56
+
57
+ /** Per-item detail, kept for LOG_LEVEL=debug. Silent otherwise. */
58
+ function detail(line: string): void {
59
+ if (DEBUG) console.log(line);
60
+ }
61
+
62
+ export const boot = {
63
+ packages(n: number) {
64
+ state.packages = n;
65
+ detail(`[boot] discovered ${n} node packages`);
66
+ },
67
+
68
+ node(type: string) {
69
+ state.nodes.push(type);
70
+ detail(`[boot] node ${type}`);
71
+ },
72
+
73
+ credential(name: string, skipped = false) {
74
+ if (skipped) {
75
+ // First-wins is the DESIGNED behaviour for a shared credential (several AWS
76
+ // packages legitimately declare awsCredential), so it is not a warning and
77
+ // never belonged in the readout. It was 48 warning lines a boot.
78
+ state.credentialsSkipped++;
79
+ detail(`[boot] credential ${name} already registered, kept the first`);
80
+ return;
81
+ }
82
+ state.credentials.push(name);
83
+ detail(`[boot] credential ${name}`);
84
+ },
85
+
86
+ components(node: string, count: number) {
87
+ state.components = { node, count };
88
+ },
89
+
90
+ manifests(m: { nodes: number; credentials: number; shadowed: string[]; disabled: string[] }) {
91
+ state.manifests = m;
92
+ },
93
+
94
+ pluginStates(n: number) {
95
+ // Loaded once at boot and again after the engine comes up; the count is the same
96
+ // fact both times, so the report holds one of it rather than printing twice.
97
+ state.pluginStates = n;
98
+ },
99
+
100
+ /**
101
+ * A failed read of the plugin state table. Not a notice on its own: in merged mode the
102
+ * first read races the engine binding :4101 and is retried. It becomes a notice only
103
+ * if no state was ever loaded, which is the case where plugins really are missing.
104
+ */
105
+ pluginStateFetchFailed(message: string) {
106
+ state.pluginStateError = message;
107
+ detail(`[boot] plugin state read failed: ${message}`);
108
+ },
109
+
110
+ pluginLocalWin(name: string, installedVersion?: string) {
111
+ state.pluginsLocalWin.push(name);
112
+ detail(`[boot] ${name}: local source wins over npm${installedVersion ? ` (npm has ${installedVersion})` : ""}`);
113
+ },
114
+
115
+ pluginFromNpm(name: string, source = "npm") {
116
+ state.pluginsFromNpm.push(name);
117
+ detail(`[boot] ${name}: loaded from ${source}`);
118
+ },
119
+
120
+ pluginDisabled(name: string) {
121
+ state.pluginsDisabled.push(name);
122
+ },
123
+
124
+ auth(posture: string) {
125
+ state.auth = posture;
126
+ },
127
+
128
+ redis(status: string) {
129
+ state.redis = status;
130
+ },
131
+
132
+ engine(where: string) {
133
+ state.engine = where;
134
+ },
135
+
136
+ /** Manifest hot reload is a dev affordance worth one line, because it changes what a save does. */
137
+ watching(dir: string) {
138
+ state.watching = dir;
139
+ },
140
+
141
+ endpoint(label: string, url: string, note?: string) {
142
+ state.endpoints.push({ label, url, note });
143
+ },
144
+
145
+ /**
146
+ * Addresses are listed in the order someone reaches for them, not the order the
147
+ * listeners happened to bind. Studio is what a person opens; the internal runtime is
148
+ * what a machine calls.
149
+ */
150
+ endpointOrder: ["Studio", "MCP", "Runtime", "Builder MCP"],
151
+
152
+ notice(level: "warn" | "error", text: string) {
153
+ state.notices.push({ level, text });
154
+ },
155
+
156
+ /**
157
+ * True until the report prints. Loaders that run BOTH at boot and later (the manifest
158
+ * watcher, install/uninstall) ask this: during boot they hand their result to the
159
+ * report, afterwards they log it themselves, because a reload with nothing else on
160
+ * screen is a real event and should say so.
161
+ */
162
+ isBooting() {
163
+ return !state.printed;
164
+ },
165
+
166
+ /** Everything the report knows, for a status route or a test. */
167
+ snapshot() {
168
+ return { ...state };
169
+ },
170
+
171
+ print() {
172
+ if (state.printed) return;
173
+ state.printed = true;
174
+ const ready = ((Date.now() - state.startedAt) / 1000).toFixed(1);
175
+
176
+ const rows: Array<[string, string]> = [];
177
+ rows.push(["Nodes", `${state.nodes.length} registered from ${state.packages} packages`]);
178
+ if (state.manifests) {
179
+ const m = state.manifests;
180
+ const parts = [`${m.nodes} loaded`];
181
+ if (m.shadowed.length) parts.push(`${m.shadowed.length} shadowed by code`);
182
+ if (m.disabled.length) parts.push(`${m.disabled.length} disabled`);
183
+ rows.push(["Manifests", parts.join(", ")]);
184
+ }
185
+ // The skipped count is every re-declaration of a credential that already exists,
186
+ // which is the shared-credential design working. It is a debug fact, not a headline.
187
+ rows.push(["Credentials", `${state.credentials.length} types`]);
188
+ if (state.components) rows.push(["Components", `${state.components.count} in one ${state.components.node} node`]);
189
+ if (state.pluginStates) {
190
+ const parts = [`${state.pluginStates} states`];
191
+ if (state.pluginsLocalWin.length) parts.push(`${state.pluginsLocalWin.length} local over npm`);
192
+ if (state.pluginsFromNpm.length) parts.push(`${state.pluginsFromNpm.length} from npm`);
193
+ if (state.pluginsDisabled.length) parts.push(`${state.pluginsDisabled.length} disabled`);
194
+ rows.push(["Plugins", parts.join(", ")]);
195
+ }
196
+ if (state.engine) rows.push(["Engine", state.engine]);
197
+ if (state.redis) rows.push(["Redis", state.redis]);
198
+ // Last row before the addresses, because it qualifies every one of them.
199
+ if (state.auth) rows.push(["Auth", state.auth]);
200
+ // Trimmed to the repo-relative path: absolute would be the longest string in the
201
+ // block, and the identifying part is the tail. cwd is not usable for this because
202
+ // the server is started from several directories (turbo, docker, the CLI).
203
+ if (state.watching)
204
+ rows.push(["Watching", `${state.watching.replace(/^.*?(?=apps\/)/, "")}, manifests reload on save`]);
205
+
206
+ const labelWidth = Math.max(...rows.map((r) => r[0].length), ...state.endpoints.map((e) => e.label.length));
207
+ const urlWidth = Math.max(0, ...state.endpoints.map((e) => e.url.length));
208
+ // Pad BEFORE painting: ANSI codes have width in padEnd's eyes and none on screen.
209
+ const out: string[] = ["", ` ${bold("unoverse")} ${dim(`ready in ${ready}s`)}`, ""];
210
+ for (const [label, value] of rows) out.push(` ${dim(label.padEnd(labelWidth))} ${value}`);
211
+ if (state.endpoints.length) {
212
+ const rank = (l: string) => {
213
+ const i = boot.endpointOrder.indexOf(l);
214
+ return i === -1 ? boot.endpointOrder.length : i;
215
+ };
216
+ state.endpoints.sort((a, b) => rank(a.label) - rank(b.label));
217
+ out.push("");
218
+ for (const e of state.endpoints) {
219
+ out.push(` ${dim(e.label.padEnd(labelWidth))} ${cyan(e.note ? e.url.padEnd(urlWidth) : e.url)}${e.note ? ` ${dim(e.note)}` : ""}`);
220
+ }
221
+ }
222
+
223
+ // Anomalies are the point of the readout, so they sit at the bottom where the eye
224
+ // lands and they are the only thing here that is not a count.
225
+ if (state.pluginStateError && !state.pluginStates)
226
+ state.notices.push({
227
+ level: "error",
228
+ text: `no plugin state could be read (${state.pluginStateError}), so npm-installed packages did not load`,
229
+ });
230
+
231
+ const anomalies = [
232
+ ...state.notices.map((n) => ` ${n.level === "error" ? red("✗") : yellow("!")} ${n.text}`),
233
+ ...(state.manifests?.shadowed.length
234
+ ? [` ${yellow("!")} code wins over manifest for: ${state.manifests.shadowed.join(", ")}`]
235
+ : []),
236
+ ...(state.pluginsDisabled.length
237
+ ? [` ${yellow("!")} disabled, so not loaded: ${state.pluginsDisabled.join(", ")}`]
238
+ : []),
239
+ ];
240
+ if (anomalies.length) out.push("", ...anomalies);
241
+ out.push("");
242
+ if (!DEBUG) out.push(dim(" LOG_LEVEL=debug lists every node, credential and package as it loads."), "");
243
+ console.log(out.join("\n"));
244
+ },
245
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Unoverse definition store — the FAÇADE. Split 2026-08-30 into organised
3
+ * subfolders (owner: files stay ~500-800 lines, folders group by concern);
4
+ * every existing import of `definitions/definitions.js` keeps working.
5
+ *
6
+ * registry/homes.ts where every kind lives; ref → folder resolution
7
+ * registry/catalog.ts listing, app manifests, state order, workbench views
8
+ * model.ts the served shapes + kind/word normalizers
9
+ * load/load.ts loadDefinition — the load core
10
+ * load/expand.ts Ref/atom composition, $include, state inheritance
11
+ * load/compile/ the component-tree and template compilers, one file per job
12
+ */
13
+ export * from "./registry/homes.js";
14
+ export * from "./registry/catalog.js";
15
+ export * from "./model.js";
16
+ export * from "./load/load.js";
17
+ export * from "./load/expand/index.js";
18
+ export * from "./load/compile/index.js";
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The design system as an INSTALLED PACKAGE, wherever the HOST put it.
3
+ *
4
+ * Every other tier in `definitions.ts` and `theme.ts` is a path under the content home,
5
+ * which is the developer's own project. That is right for a universe, where the design
6
+ * system is installed INTO the deployment, and wrong for Studio, which is a TOOL run
7
+ * against somebody else's folder. Studio depends on `@unoverse-platform/marketplace`, so
8
+ * it already carries the design system in its own node_modules, and then went looking for
9
+ * it in a project that will never have one.
10
+ *
11
+ * A new Studio project therefore had NO design system at all: zero components, zero atoms,
12
+ * no foundation styles (measured against an empty home: 17 components and 30 atoms resolve
13
+ * with this, none without). Primitives still render, so the scaffolded Welcome component
14
+ * looks fine and hides it; the failure surfaces later, on the first `Card` or token. And it
15
+ * is silent either way, because both callers fall through to a path rather than raising.
16
+ *
17
+ * ASKED OF NODE rather than walked, so it is found from wherever the host is installed.
18
+ * NOT a dependency: `marketplace` depends on THIS package, so declaring it would be a
19
+ * cycle. Absent is a normal answer (a universe resolves from a tier above), hence the
20
+ * catch, and the answer is memoized because a failed resolve is the expensive case.
21
+ *
22
+ * ITS OWN MODULE so `definitions.ts` and `theme.ts` share one implementation without
23
+ * importing each other. They resolve different kinds out of the same bundle
24
+ * (components/atoms, and styles), and two copies of this would drift into two answers
25
+ * about where the design system is.
26
+ *
27
+ * SEARCHED LAST by both callers, after every project-local tier, so nothing changes for a
28
+ * host that already resolves: this fires only where the alternative was nothing at all.
29
+ */
30
+ import { join, dirname } from "path";
31
+ import { createRequire } from "module";
32
+
33
+ let cached: string | null | undefined;
34
+
35
+ /** The bundle's `definitions/` folder, or null when the package is not installed. */
36
+ export function packagedDesignSystem(): string | null {
37
+ if (cached !== undefined) return cached;
38
+ try {
39
+ const pkg = createRequire(import.meta.url).resolve("@unoverse-platform/marketplace/package.json");
40
+ cached = join(dirname(pkg), "definitions");
41
+ } catch {
42
+ cached = null;
43
+ }
44
+ return cached;
45
+ }
@@ -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";