@unoverse-platform/studio 0.1.97 → 0.1.99

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 (120) hide show
  1. package/bin/studio.mjs +2 -2
  2. package/local/catalog.ts +5 -1
  3. package/local/nodes.ts +28 -41
  4. package/local/plugin.ts +71 -13
  5. package/local/sample-identity/brand.yaml +24 -0
  6. package/local/sample-identity/organisation.yaml +14 -0
  7. package/local/sample-identity/purpose.yaml +14 -0
  8. package/local/sample-identity/story.yaml +18 -0
  9. package/local/scaffold.mjs +43 -23
  10. package/local/source.ts +3 -8
  11. package/package.json +1 -1
  12. package/screens/AgentsView.tsx +173 -0
  13. package/screens/ComponentsView.tsx +2 -2
  14. package/screens/EmptyLibrary.tsx +1 -1
  15. package/screens/IdentityView.tsx +3 -3
  16. package/screens/PromptBlocksView.tsx +1 -1
  17. package/screens/TemplatesLibraryView.tsx +91 -6
  18. package/screens/TemplatesView.tsx +24 -9
  19. package/screens/index.ts +2 -0
  20. package/src/App.tsx +8 -3
  21. package/src/host.ts +1 -14
  22. package/vendor/base/src/agent-mcp/discovery.ts +76 -18
  23. package/vendor/base/src/agent-mcp/handoff.ts +11 -31
  24. package/vendor/base/src/agent-mcp/index.ts +5 -5
  25. package/vendor/base/src/agent-mcp/invoke.ts +37 -19
  26. package/vendor/base/src/agent-mcp/lean.ts +19 -0
  27. package/vendor/base/src/agent-mcp/memoryContext.ts +15 -9
  28. package/vendor/base/src/agent-mcp/memoryIngest.ts +8 -3
  29. package/vendor/base/src/agent-mcp/rowComponents.ts +184 -72
  30. package/vendor/base/src/agent-mcp/tools.ts +136 -7
  31. package/vendor/base/src/agent-mcp/types.ts +47 -10
  32. package/vendor/base/src/auth/auth.ts +35 -5
  33. package/vendor/base/src/definitions/isolated.ts +37 -0
  34. package/vendor/base/src/definitions/load/compile/component.ts +59 -4
  35. package/vendor/base/src/definitions/load/compile/sections.ts +7 -1
  36. package/vendor/base/src/definitions/load/compile/template.ts +19 -15
  37. package/vendor/base/src/definitions/load/expand/inherit.ts +5 -1
  38. package/vendor/base/src/definitions/load/expand/referenced.ts +21 -2
  39. package/vendor/base/src/definitions/load/expand/refs.ts +14 -0
  40. package/vendor/base/src/definitions/load/expand/walk.ts +88 -38
  41. package/vendor/base/src/definitions/load/load.ts +37 -28
  42. package/vendor/base/src/definitions/model.ts +8 -12
  43. package/vendor/base/src/definitions/registry/agents.ts +117 -0
  44. package/vendor/base/src/definitions/registry/catalog.ts +40 -24
  45. package/vendor/base/src/definitions/registry/datasetSql.ts +149 -0
  46. package/vendor/base/src/definitions/registry/homes.ts +69 -5
  47. package/vendor/base/src/definitions/registry/objects.ts +413 -0
  48. package/vendor/base/src/definitions/registry/orgDocuments.ts +69 -29
  49. package/vendor/base/src/definitions/registry/orgPack.ts +11 -11
  50. package/vendor/base/src/definitions/registry/pipelines.ts +197 -0
  51. package/vendor/base/src/definitions/theme.ts +1 -2
  52. package/vendor/base/src/items/collect.ts +29 -25
  53. package/vendor/base/src/items/publish.ts +30 -6
  54. package/vendor/base/src/lint/context.mjs +17 -0
  55. package/vendor/base/src/lint/design/file.mjs +199 -2
  56. package/vendor/base/src/lint/design/index.mjs +66 -73
  57. package/vendor/base/src/lint/design/vocabulary.mjs +5 -2
  58. package/vendor/base/src/lint/design/walk.mjs +14 -4
  59. package/vendor/base/src/lint/index.mjs +15 -9
  60. package/vendor/base/src/lint/nodes/blocks.mjs +6 -2
  61. package/vendor/base/src/lint/nodes/index.mjs +5 -0
  62. package/vendor/base/src/lint/nodes/node.mjs +157 -7
  63. package/vendor/base/src/lint/objects/index.d.mts +32 -0
  64. package/vendor/base/src/lint/objects/index.mjs +324 -0
  65. package/vendor/base/src/lint/objects/rules.d.mts +44 -0
  66. package/vendor/base/src/lint/objects/rules.mjs +446 -0
  67. package/vendor/base/src/lint/prompts/index.d.mts +34 -0
  68. package/vendor/base/src/lint/prompts/index.mjs +198 -0
  69. package/vendor/base/src/manifests/compose.ts +80 -9
  70. package/vendor/base/src/manifests/executor/callback.ts +15 -4
  71. package/vendor/base/src/manifests/executor/context.ts +9 -1
  72. package/vendor/base/src/manifests/executor/memory.ts +83 -3
  73. package/vendor/base/src/manifests/executor/session.ts +50 -0
  74. package/vendor/base/src/manifests/executor/toolBridge.ts +114 -49
  75. package/vendor/base/src/manifests/runtime/auth/aws.ts +14 -1
  76. package/vendor/base/src/manifests/runtime/channels/final.ts +46 -34
  77. package/vendor/base/src/manifests/runtime/channels/service.ts +30 -32
  78. package/vendor/base/src/manifests/runtime/channels/workflow.ts +6 -11
  79. package/vendor/base/src/manifests/runtime/context.ts +18 -6
  80. package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +159 -3
  81. package/vendor/base/src/manifests/runtime/duplex/session.ts +304 -9
  82. package/vendor/base/src/manifests/runtime/events.ts +39 -3
  83. package/vendor/base/src/manifests/runtime/http/concurrency.ts +44 -0
  84. package/vendor/base/src/manifests/runtime/http/request.ts +36 -7
  85. package/vendor/base/src/manifests/runtime/index.ts +3 -2
  86. package/vendor/base/src/manifests/runtime/loops/repeat.ts +86 -0
  87. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +30 -3
  88. package/vendor/base/src/manifests/runtime/usage.ts +10 -0
  89. package/vendor/base/src/paths.ts +16 -8
  90. package/vendor/base/src/platform/audioManager.ts +32 -0
  91. package/vendor/base/src/platform/executionContext.ts +14 -0
  92. package/vendor/base/src/platform/liveInput.ts +52 -0
  93. package/vendor/base/src/plugins/discovery.ts +1 -1
  94. package/vendor/base/src/template/SafeExpression.ts +6 -0
  95. package/vendor/base/src/workflow/GetMemoryService.ts +12 -11
  96. package/vendor/base/src/workflow/PlatformMemoryCore.ts +23 -10
  97. package/vendor/sdk/appEngine.tsx +30 -11
  98. package/vendor/sdk/lib/UnoverseComponent.tsx +8 -4
  99. package/vendor/sdk/lib/app.tsx +70 -95
  100. package/vendor/sdk/lib/connection.tsx +47 -57
  101. package/vendor/sdk/lib/core/actions.ts +6 -0
  102. package/vendor/sdk/lib/core/apps.ts +66 -81
  103. package/vendor/sdk/lib/core/client.ts +5 -14
  104. package/vendor/sdk/lib/core/connection.ts +30 -0
  105. package/vendor/sdk/lib/core/index.ts +2 -2
  106. package/vendor/sdk/lib/core/store.ts +90 -15
  107. package/vendor/sdk/lib/core/types.ts +9 -8
  108. package/vendor/sdk/lib/primitives.tsx +113 -4
  109. package/vendor/sdk/lib/realtime/useAudioPlayback.ts +8 -1
  110. package/vendor/sdk/lib/render.tsx +3 -3
  111. package/vendor/sdk/lib/voice.tsx +27 -13
  112. package/local/sample-identity/brand/brand.yaml +0 -88
  113. package/local/sample-identity/brand/manifest.yaml +0 -13
  114. package/local/sample-identity/organisation/manifest.yaml +0 -15
  115. package/local/sample-identity/organisation/organisation.yaml +0 -53
  116. package/local/sample-identity/purpose/manifest.yaml +0 -12
  117. package/local/sample-identity/purpose/purpose.yaml +0 -46
  118. package/local/sample-identity/story/manifest.yaml +0 -12
  119. package/local/sample-identity/story/story.yaml +0 -63
  120. package/vendor/base/src/definitions/registry/identitySeed.ts +0 -17
@@ -41,11 +41,33 @@ const getIssuerCandidates = (): string[] => {
41
41
  };
42
42
  const getAudience = (): string | undefined => process.env.AUTH_AUDIENCE;
43
43
 
44
- let jwks: ReturnType<typeof createRemoteJWKSet> | null = null;
45
- function getJWKS() {
44
+ // WHERE THE SIGNING KEYS ARE IS THE PROVIDER'S TO SAY, in its discovery document.
45
+ //
46
+ // This read `${issuer}/.well-known/jwks.json`, which is where Auth0 and Cognito happen to keep
47
+ // them. Microsoft Entra keeps them under /discovery/v2.0/keys, Keycloak under
48
+ // /protocol/openid-connect/certs, WorkOS AuthKit under /oauth2/jwks, so a bank's Entra or a
49
+ // WorkOS login was refused on every request with a key that could not be found. OIDC
50
+ // discovery names the place (`jwks_uri`). With no discovery document the old location is
51
+ // used, and not remembered, so a transient failure is retried on the next request.
52
+ type KeySet = ReturnType<typeof createRemoteJWKSet>;
53
+ let jwks: Promise<KeySet> | null = null;
54
+ async function loadJWKS(base: string): Promise<{ set: KeySet; discovered: boolean }> {
55
+ try {
56
+ const res = await fetch(`${base}/.well-known/openid-configuration`, { signal: AbortSignal.timeout(5000) });
57
+ const doc: any = res.ok ? await res.json() : null;
58
+ if (typeof doc?.jwks_uri === "string") return { set: createRemoteJWKSet(new URL(doc.jwks_uri)), discovered: true };
59
+ } catch {
60
+ // fall through to the conventional location
61
+ }
62
+ return { set: createRemoteJWKSet(new URL(`${base}/.well-known/jwks.json`)), discovered: false };
63
+ }
64
+ function getJWKS(): Promise<KeySet> | null {
46
65
  const base = getIssuerBase();
47
66
  if (!jwks && base) {
48
- jwks = createRemoteJWKSet(new URL(`${base}/.well-known/jwks.json`));
67
+ jwks = loadJWKS(base).then(({ set, discovered }) => {
68
+ if (!discovered) jwks = null;
69
+ return set;
70
+ });
49
71
  }
50
72
  return jwks;
51
73
  }
@@ -69,6 +91,14 @@ function parseArrayClaim(payload: JWTPayload, claimName: string): string[] {
69
91
  return [];
70
92
  }
71
93
 
94
+ /** Roles as a list. Most providers send a `roles` list (Auth0 namespaced, Entra app roles);
95
+ * WorkOS sends one `role`, the role of the organization membership signed in with. */
96
+ export function parseRoles(payload: JWTPayload): string[] {
97
+ const roles = parseArrayClaim(payload, "roles");
98
+ const one = payload.role;
99
+ return typeof one === "string" && one && !roles.includes(one) ? [...roles, one] : roles;
100
+ }
101
+
72
102
  /** Parse a string claim, handling Auth0-style namespaced claims (e.g. https://…/email).
73
103
  * Auth0 emits custom claims (email on the access token) ONLY namespaced, so a plain
74
104
  * `payload.email` read returns undefined and silently no-ops every email-keyed feature
@@ -114,7 +144,7 @@ export async function validateJWT(token: string): Promise<AuthUser | null> {
114
144
  }
115
145
 
116
146
  async function verifyJWT(token: string): Promise<AuthUser | null> {
117
- const jwksSet = getJWKS();
147
+ const jwksSet = await getJWKS();
118
148
  const issuers = getIssuerCandidates();
119
149
  if (!jwksSet || issuers.length === 0) {
120
150
  console.warn("[auth] OIDC not configured (AUTH_ISSUER missing)");
@@ -148,7 +178,7 @@ async function verifyJWT(token: string): Promise<AuthUser | null> {
148
178
  return {
149
179
  id: payload.sub || "unknown",
150
180
  email: parseStringClaim(payload, "email"),
151
- roles: parseArrayClaim(payload, "roles"),
181
+ roles: parseRoles(payload),
152
182
  permissions: parseArrayClaim(payload, "permissions"),
153
183
  raw: payload,
154
184
  };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * AN ORG IS A WALL (docs/unoverse/UNOVERSE_ORGANISATION.md §11, part 1).
3
+ *
4
+ * Anything built from many orgs' definitions (an MCP session's tools, the spatial
5
+ * registry, the Studio lists) builds each item through here. One that throws is left
6
+ * out and named, `org/item: reason`, and every other org is still served. Observed live
7
+ * 2026-09-23: one org's stale template threw inside MCP session construction and every
8
+ * signed-in session, for every org, answered -32603.
9
+ *
10
+ * The one door, so no builder writes its own try and no builder forgets one.
11
+ */
12
+
13
+ /** Name what was left out and why. The label is `org/item` (or `kind org/item`). */
14
+ export function reportLeftOut(label: string, err: unknown): void {
15
+ const reason = err instanceof Error ? err.message : String(err);
16
+ console.warn(`[unoverse] left out ${label}: ${reason}`);
17
+ }
18
+
19
+ /** Build one item on its own. Undefined when it failed; the failure is reported. */
20
+ export function isolated<T>(label: string, build: () => T): T | undefined {
21
+ try {
22
+ return build();
23
+ } catch (err) {
24
+ reportLeftOut(label, err);
25
+ return undefined;
26
+ }
27
+ }
28
+
29
+ /** The same, for a build that awaits. */
30
+ export async function isolatedAsync<T>(label: string, build: () => Promise<T>): Promise<T | undefined> {
31
+ try {
32
+ return await build();
33
+ } catch (err) {
34
+ reportLeftOut(label, err);
35
+ return undefined;
36
+ }
37
+ }
@@ -28,6 +28,8 @@ export function compileComponentTree(def: UnoverseDefinition): void {
28
28
  const initial = typeof tree.initial === "string" ? tree.initial : names[0];
29
29
  def.publicStates = names;
30
30
  def.initialView = initial;
31
+ const statePlaces = placesOfStates(tree.states);
32
+ if (statePlaces) def.statePlaces = statePlaces;
31
33
  // The nested projection viewers render from (never scan layouts for it).
32
34
  def.stateTree = names.map((n) => {
33
35
  const s = (tree.states as Record<string, { layout?: unknown; on?: unknown; initial?: unknown; states?: Record<string, { layout?: unknown } | null> } | null>)[n];
@@ -35,6 +37,7 @@ export function compileComponentTree(def: UnoverseDefinition): void {
35
37
  if (s && typeof s === "object") {
36
38
  if (typeof s.layout === "string") entry.layout = s.layout;
37
39
  if (typeof s.on === "string") entry.on = s.on;
40
+ else if (s.states && typeof s.states === "object" && Object.keys(s.states).length) entry.on = STEP;
38
41
  if (typeof s.initial === "string") entry.initial = s.initial;
39
42
  if (s.states && typeof s.states === "object")
40
43
  entry.states = Object.entries(s.states).map(([k, v]) => ({
@@ -70,11 +73,63 @@ export function compileComponentTree(def: UnoverseDefinition): void {
70
73
  // READ where a def has not been swept, and lint-forbidden in rx.
71
74
  const flat: Record<string, unknown> = { ...authored, state: initial };
72
75
  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];
76
+ const steps = new Set<string>();
77
+ for (const s of Object.values(tree.states)) {
78
+ if (!s || typeof s !== "object") continue;
79
+ const subs = s.states && typeof s.states === "object" ? Object.keys(s.states) : [];
80
+ const step = typeof s.initial === "string" ? s.initial : subs[0];
81
+ if (typeof s.on === "string") {
77
82
  if (step) flat[s.on] = step;
83
+ } else if (subs.length) {
84
+ // ONE WORD (UNOVERSE_STATE_MODEL §5, "One word: `state`"): a face's steps carry no second
85
+ // name. They start on the first declared and live in the hidden step value.
86
+ subs.forEach((n) => steps.add(n));
87
+ if (step && !(STEP in flat)) flat[STEP] = step;
78
88
  }
89
+ }
79
90
  def.state = flat;
91
+ if (steps.size) {
92
+ if (def.root) def.root = pointStepsAtStep(def.root, steps);
93
+ if (def.layouts) for (const n of Object.keys(def.layouts)) def.layouts[n] = pointStepsAtStep(def.layouts[n], steps);
94
+ }
95
+ }
96
+
97
+ /** The hidden value a face's steps live in. The author writes `state`; only the compiler says this. */
98
+ export const STEP = "step";
99
+
100
+ /**
101
+ * ONE WORD (UNOVERSE_STATE_MODEL §5): the author writes `state` for a face and for its steps alike.
102
+ * Wherever `state` names one of this component's STEPS, it means the step value: a `Switch on:
103
+ * state` whose cases are steps, a condition on `field: state` against steps, and a write of
104
+ * `key: state` to a step. Everything else about `state` (the face) is left untouched.
105
+ */
106
+ export function pointStepsAtStep<T>(node: T, steps: Set<string>): T {
107
+ const isStep = (v: unknown): boolean => typeof v === "string" && steps.has(v);
108
+ const walk = (n: unknown): unknown => {
109
+ if (Array.isArray(n)) return n.map(walk);
110
+ if (!n || typeof n !== "object") return n;
111
+ const o: Record<string, unknown> = {};
112
+ for (const [k, v] of Object.entries(n as Record<string, unknown>)) o[k] = walk(v);
113
+ if (o.type === "Switch" && o.on === "state" && o.cases && typeof o.cases === "object") {
114
+ const keys = Object.keys(o.cases as object).filter((k) => k !== "default");
115
+ if (keys.length && keys.every(isStep)) o.on = STEP;
116
+ }
117
+ if (o.field === "state") {
118
+ const vals = [o.eq, o.ne, ...(Array.isArray(o.in) ? o.in : [])].filter((v) => v !== undefined);
119
+ if (vals.length && vals.every(isStep)) o.field = STEP;
120
+ }
121
+ if (o.key === "state" && isStep(o.value)) o.key = STEP;
122
+ return o;
123
+ };
124
+ return walk(node) as T;
125
+ }
126
+
127
+ /** The place each state names, or undefined when none names one (UNOVERSE_SCREEN.md §2.3). */
128
+ export function placesOfStates(states: Record<string, unknown>): Record<string, string> | undefined {
129
+ const out: Record<string, string> = {};
130
+ for (const [name, s] of Object.entries(states)) {
131
+ const place = s && typeof s === "object" ? (s as { place?: unknown }).place : undefined;
132
+ if (typeof place === "string" && place) out[name] = place;
133
+ }
134
+ return Object.keys(out).length ? out : undefined;
80
135
  }
@@ -80,6 +80,13 @@ export function expandTemplateSections(
80
80
  `template "${templateName}": an items place nests its search words INSIDE items: (each ITEM searches) — items: { min, max, type, include, pick }`,
81
81
  );
82
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)" : ""}`);
83
90
  out.push({
84
91
  kind: "director",
85
92
  face,
@@ -116,7 +123,6 @@ export function expandTemplateSections(
116
123
  face,
117
124
  ...(typeof d.state === "string" ? { state: d.state } : {}),
118
125
  ...(typeof d.pick === "number" ? { pick: d.pick } : {}),
119
- ...(typeof d.rules === "string" ? { rules: d.rules } : {}),
120
126
  ...(Array.isArray(d.preview) ? { preview: d.preview.map(String) } : {}),
121
127
  });
122
128
  const { director: _d, ...rest } = n;
@@ -9,6 +9,7 @@ import type { UnoverseDefinition } from "../../model.js";
9
9
  import { loadDefinition } from "../load.js";
10
10
  import { composeIncludes } from "../expand/index.js";
11
11
  import { expandTemplateSections } from "./sections.js";
12
+ import { placesOfStates } from "./component.js";
12
13
 
13
14
  /**
14
15
  * ONE PREVIEWED INSTANCE'S SEED — `preview ?? default` per prop, the same values the
@@ -108,19 +109,8 @@ export function synthesizeTemplateRoot(def: UnoverseDefinition, folderDir?: stri
108
109
  const layouts: Record<string, unknown> = {};
109
110
  const sections: NonNullable<UnoverseDefinition["sections"]> = {};
110
111
  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
112
  for (const name of shapeNames) {
117
113
  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
114
  if (typeof st.layout !== "string") continue;
125
115
  const secs: NonNullable<UnoverseDefinition["sections"]>[string] = [];
126
116
  layouts[name] = expandTemplateSections(composeIncludes({ $include: st.layout }, folderDir), name, def.name, secs);
@@ -143,11 +133,25 @@ export function synthesizeTemplateRoot(def: UnoverseDefinition, folderDir?: stri
143
133
  // same contract: states are the interface, the first declared one is arrival.
144
134
  def.publicStates = shapeNames;
145
135
  def.initialView = shapeNames[0];
146
- if (Object.keys(latches).length) def.latches = latches;
136
+ const statePlaces = placesOfStates(states);
137
+ if (statePlaces) def.statePlaces = statePlaces;
147
138
  if (Object.keys(sections).length) def.sections = sections;
148
139
  if (Object.keys(compiledPreview).length) def.preview = { ...(def.preview ?? {}), ...compiledPreview };
149
140
  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]]] };
141
+ // THE TREE IS THE MACHINE, for templates too (ruled 2026-09-06, §5d). A template that
142
+ // STREAMS IN is an INSTANCE that draws itself, and the renderer draws `root`; served as
143
+ // the base layout alone, the instance's own face was read by nobody, so a task could
144
+ // never retract to its inline face (the ✕ wrote `inline` and the flow drew the whole
145
+ // page). A component's root is synthesized as a Switch on `state` over its declared
146
+ // layouts (load.ts, 2026-08-29); the same root here, one case per state, default = the
147
+ // base. `layouts` / `defaultLayout` are unchanged, so a MOUNT — which reads `layouts`
148
+ // by the faces of what it holds — is untouched. Wrapped in a container so responsive
149
+ // columns can query their surface width in any host.
150
+ const cases: Record<string, unknown> = { ...layouts };
151
+ if (!("default" in cases)) cases.default = layouts[shapeNames[0]];
152
+ def.root = {
153
+ type: "Box",
154
+ style: { width: "full", container: "inline-size" },
155
+ children: [shapeNames.length > 1 ? { type: "Switch", on: "state", cases } : layouts[shapeNames[0]]],
156
+ } as UnoverseDefinition["root"];
153
157
  }
@@ -110,7 +110,11 @@ export function inheritProps(
110
110
  // Day's moments the day `chapter` stopped restating the tile's contract: `required` was
111
111
  // still `["ref"]`, but `title` and `description` were back among the properties a model
112
112
  // could send, which is exactly the venue-renaming this flag exists to prevent.
113
- const CONTRACT = ["type", "description", "maxLength", "minItems", "maxItems", "items", "hydrate", "optional", "override", "preview"] as const;
113
+ // `input` TRAVELS TOO (2026-09-15). An atom that marks a field `input: false` (the button's
114
+ // `icon` and `link`) says nobody outside fills it; without the word here the host inherited
115
+ // the field as fillable and the tool asked the model for it (SAB transfer: four fields it
116
+ // could not honestly write). Same failure as `override` above, one word later.
117
+ const CONTRACT = ["type", "description", "maxLength", "minItems", "maxItems", "items", "hydrate", "optional", "override", "preview", "input"] as const;
114
118
  // AN ATOM INSIDE AN `Each` DESCRIBES THE ITEM, not the page. The scope is the chain of
115
119
  // array props the Ref sits inside, so a `prose` atom drawn once per section lands on
116
120
  // `sections.items.body` — where a brief on its node used to land by being there.
@@ -11,6 +11,7 @@ import type { UnoverseDefinition } from "../../model.js";
11
11
  import { loadDefinition } from "../load.js";
12
12
  import { inheritState } from "./inherit.js";
13
13
  import type { AnyNode } from "./refs.js";
14
+ import { composeRef, composesAxis } from "./walk.js";
14
15
 
15
16
  export function referencedDefinition(def: UnoverseDefinition, contextOrg?: string): UnoverseDefinition {
16
17
  const defs: Record<string, AnyNode> = {};
@@ -21,6 +22,22 @@ export function referencedDefinition(def: UnoverseDefinition, contextOrg?: strin
21
22
  const collect = (node: AnyNode | undefined): void => {
22
23
  if (!node || typeof node !== "object") return;
23
24
  if (Array.isArray(node)) return node.forEach(collect);
25
+ // A REF THAT CHOOSES A KIND IS COMPOSED HERE, NOT SHIPPED BY NAME. An org's component
26
+ // with a private axis (`sab/button`, `with: {kind: link}`) is specialised per use: the
27
+ // host picks the kind, the Ref's action/style land on the real node. Shipping its
28
+ // whole root once and letting the client resolve it drew the arrival kind with no
29
+ // words (the studio's comparison template, 2026-09-07). The composed node keeps its
30
+ // own inner atom references, which the client resolves from `defs` as ever.
31
+ if (node.type === "Ref" && typeof node.ref === "string" && composesAxis(node, contextOrg ?? def.org)) {
32
+ const composed = composeRef(node, contextOrg ?? def.org, "referenced");
33
+ if (composed) {
34
+ inheritState(inherit, composed.piece);
35
+ for (const [k, v] of Object.entries((composed.piece.defs ?? {}) as Record<string, AnyNode>)) if (!(k in defs)) defs[k] = v;
36
+ for (const k of Object.keys(node)) delete node[k];
37
+ Object.assign(node, composed.root);
38
+ return collect(node);
39
+ }
40
+ }
24
41
  if (node.type === "Ref" && typeof node.ref === "string") {
25
42
  const name = node.ref;
26
43
  if (!(name in defs)) {
@@ -30,8 +47,10 @@ export function referencedDefinition(def: UnoverseDefinition, contextOrg?: strin
30
47
  // EXPANDED instead was the difference between product-card costing 17 KB + 173 KB
31
48
  // and 17 KB + a handful: it references `document`, and an expanded document drags
32
49
  // in every copy this exists to remove.
33
- const piece =
34
- loadDefinition(name, "atom", "referenced") ?? loadDefinition(name, "component", "referenced", contextOrg ?? def.org);
50
+ // Qualified names are an org's piece, never a marketplace atom (see composeRef).
51
+ const piece = name.includes("/")
52
+ ? (loadDefinition(name, "component", "referenced", contextOrg ?? def.org) ?? loadDefinition(name, "atom", "referenced"))
53
+ : (loadDefinition(name, "atom", "referenced") ?? loadDefinition(name, "component", "referenced", contextOrg ?? def.org));
35
54
  if (piece?.root) {
36
55
  defs[name] = piece.root as AnyNode;
37
56
  // Same inheritance as the expanded form. The client resolves the Ref itself here,
@@ -82,6 +82,20 @@ function applyStyleLiterals(style: unknown, lits: Record<string, unknown>): void
82
82
  }
83
83
 
84
84
  export function applyLiterals(node: AnyNode, lits: Record<string, unknown>): void {
85
+ // A NESTED REF FORWARDS THE LITERAL. A piece whose kind is itself a Ref to an atom
86
+ // (SAB's button: primary = the marketplace `button` atom with `props: {label: label}`)
87
+ // hands its host's literal on as that Ref's `with`, so the atom draws the words whether
88
+ // the tree is expanded here or resolved by name on the client (referenced form).
89
+ if (node.type === "Ref" && node.props && typeof node.props === "object") {
90
+ for (const k of Object.keys(node.props as AnyNode)) {
91
+ const field = (node.props as AnyNode)[k];
92
+ if (typeof field === "string" && field in lits) {
93
+ (node.with ??= {})[k] = lits[field];
94
+ delete (node.props as AnyNode)[k];
95
+ }
96
+ }
97
+ if (Object.keys(node.props as AnyNode).length === 0) delete node.props;
98
+ }
85
99
  if (node.bind) {
86
100
  for (const attr of Object.keys(node.bind)) {
87
101
  const field = node.bind[attr];
@@ -13,44 +13,11 @@ import { inheritState, inheritProps, mergeInheritedProps } from "./inherit.js";
13
13
 
14
14
  export function expandNode(node: AnyNode, contextOrg?: string, inherit?: Record<string, unknown>, props?: Record<string, Record<string, unknown>>, scope: string[] = []): AnyNode {
15
15
  if (node?.type === "Ref") {
16
- // A Ref inlines an ATOM (the usual case, rx/atoms). It also resolves a COMPONENT
17
- // as a fallback, so a template can embed a shared flat component (e.g. the
18
- // ComposerBar chrome) the same way it embeds an atom — the one mechanism for fixed,
19
- // always-present chrome. (A component's root is inlined as-is; flat components only.)
20
- // The parent definition's org rides along so a bare ref inside an org's own tree
21
- // resolves to that org's component when two orgs share the name.
22
- //
23
- // AND A BLOCK (2026-08-31) — a flat TEMPLATE with no manifest: `pair`, `feature`,
24
- // `single`, `rail`, `stack`. §2 always said "a rich template may compose basic ones
25
- // inside itself", and this is that sentence, by the mechanism already here rather than
26
- // a new one. Inlining is exactly right for it: the block's `director:` places arrive in
27
- // the composing page's tree BEFORE its sections compile, so they become real slots and
28
- // the page addresses them by position along with its own. Nothing is mounted, nothing
29
- // is nested at runtime — the page ends up with the tree it would have had if someone
30
- // had drawn the block by hand, which is the point.
31
- //
32
- // Last in the chain deliberately: atoms and components are the common case, and a
33
- // template sharing a name with either keeps the old resolution.
34
- const atom =
35
- loadDefinition(String(node.ref), "atom") ??
36
- loadDefinition(String(node.ref), "component", "expanded", contextOrg) ??
37
- loadDefinition(String(node.ref), "template", "expanded", contextOrg);
38
- if (!atom?.root) return node; // unknown ref — leave the Ref (renders as a no-op)
39
- inheritState(inherit ?? {}, atom);
40
- if (props) inheritProps(props, atom, (node.props ?? {}) as Record<string, string>, (node.with ?? {}) as Record<string, unknown>, scope);
41
- const root = JSON.parse(JSON.stringify(atom.root)) as AnyNode;
42
- remapFields(root, (node.props ?? {}) as Record<string, string>);
43
- if (node.with && typeof node.with === "object") applyLiterals(root, node.with as Record<string, unknown>);
44
- if (node.visibleWhen) root.visibleWhen = node.visibleWhen;
45
- // A Ref may override the atom's `action` — the per-host behaviour the atom can't carry
46
- // (e.g. each wizard step's option sets different fields). Parallel to style/visibleWhen.
47
- if (node.action) root.action = node.action;
48
- if (node.style) root.style = { ...(root.style ?? {}), ...node.style };
49
- // THE BLOCK'S OPENING (2026-09-02, blocks returned): a Ref carrying `children:` fills
50
- // the inlined root with the CALLER's children — a `band` or `two-column` block styles
51
- // the frame, the composing layout says what sits inside. Same override family as
52
- // style/action above; a Ref without children keeps the block's own (a complete block).
53
- if (Array.isArray(node.children) && node.children.length) root.children = node.children;
16
+ const composed = composeRef(node, contextOrg, "expanded");
17
+ if (!composed) return node; // unknown ref — leave the Ref (renders as a no-op)
18
+ const { piece, root } = composed;
19
+ inheritState(inherit ?? {}, piece);
20
+ if (props) inheritProps(props, piece, (node.props ?? {}) as Record<string, string>, (node.with ?? {}) as Record<string, unknown>, scope);
54
21
  return expandNode(root, contextOrg, inherit, props, scope); // expand nested refs too
55
22
  }
56
23
  if (Array.isArray(node?.children)) node.children = node.children.map((c: AnyNode) => expandNode(c, contextOrg, inherit, props, scope));
@@ -75,6 +42,89 @@ export function expandNode(node: AnyNode, contextOrg?: string, inherit?: Record<
75
42
  }
76
43
 
77
44
 
45
+ /**
46
+ * RESOLVE ONE REF INTO THE NODE IT COMPOSES — shared by the expanded form (which then
47
+ * recurses) and the referenced form (which keeps the inner atom references for the client).
48
+ *
49
+ * A Ref inlines an ATOM (the usual case, rx/atoms). It also resolves a COMPONENT as a
50
+ * fallback, so a template can embed a shared flat component (e.g. the ComposerBar chrome)
51
+ * the same way it embeds an atom — the one mechanism for fixed, always-present chrome.
52
+ * AND A BLOCK (2026-08-31) — a flat TEMPLATE with no manifest: `pair`, `feature`, `single`,
53
+ * `rail`, `stack`. Last in the chain deliberately: atoms and components are the common
54
+ * case, and a template sharing a name with either keeps the old resolution.
55
+ *
56
+ * AN ORG-QUALIFIED REF (`sab/button`) NAMES THAT ORG'S PIECE. Atoms are the marketplace's
57
+ * (no org), and the atom lookup strips the org from a qualified name — so `sab/button`
58
+ * silently resolved to the marketplace `button` atom and the org's own button component
59
+ * was never reached (2026-09-07). A bare name keeps the old order: atom first.
60
+ *
61
+ * AN EMBEDDED PIECE'S PRIVATE AXIS IS CHOSEN WHERE IT IS COMPOSED (2026-09-07, the SAB
62
+ * button: kinds primary/secondary/outline/link on a private `kind` axis). Its root is the
63
+ * compiled surface shell around the switches on that axis. Inlined, the piece has no slice
64
+ * of its own, so the host's `with` names the kind, and a host that names none gets the
65
+ * arrival kind (the first declared, def.state). Resolved HERE, statically, because the
66
+ * Ref's action/style/visibleWhen overrides must land on the real node, not on a Switch —
67
+ * and two buttons of different kinds in one host cannot share one runtime key.
68
+ */
69
+ export function composeRef(node: AnyNode, contextOrg: string | undefined, mode: "expanded" | "referenced"): { piece: UnoverseDefinition; root: AnyNode } | null {
70
+ const ref = String(node.ref);
71
+ const qualified = ref.includes("/");
72
+ const piece = qualified
73
+ ? (loadDefinition(ref, "component", mode, contextOrg) ?? loadDefinition(ref, "template", mode, contextOrg) ?? loadDefinition(ref, "atom", mode))
74
+ : (loadDefinition(ref, "atom", mode) ?? loadDefinition(ref, "component", mode, contextOrg) ?? loadDefinition(ref, "template", mode, contextOrg));
75
+ if (!piece?.root) return null;
76
+ let root = JSON.parse(JSON.stringify(piece.root)) as AnyNode;
77
+ const lits = (node.with && typeof node.with === "object" ? node.with : {}) as Record<string, unknown>;
78
+ const axes = (piece.state ?? {}) as Record<string, unknown>;
79
+ const isAxisSwitch = (n: AnyNode | undefined): boolean => n?.type === "Switch" && typeof n.on === "string" && !!n.cases && (n.on in lits || n.on in axes);
80
+ const shared = (piece.defs ?? {}) as Record<string, AnyNode>;
81
+ for (let guard = 0; guard < 8; guard++) {
82
+ // The referenced form hoists a repeated subtree into a `shared:` chunk and leaves a
83
+ // parameterless reference to it (the kind switch sits under two cases, so it is one).
84
+ // Resolving the axis has to read through that reference.
85
+ if (root?.type === "Ref" && typeof root.ref === "string" && root.ref.startsWith("shared:") && shared[root.ref]) {
86
+ root = JSON.parse(JSON.stringify(shared[root.ref])) as AnyNode;
87
+ continue;
88
+ }
89
+ if (isAxisSwitch(root)) {
90
+ const pick = String(root.on in lits ? lits[root.on] : axes[root.on]);
91
+ const chosen = (root.cases as AnyNode)[pick] ?? (root.cases as AnyNode).default;
92
+ if (!chosen) break;
93
+ root = JSON.parse(JSON.stringify(chosen)) as AnyNode;
94
+ continue;
95
+ }
96
+ // The compiled SURFACE SHELL (a Box around the state switch) belongs to a piece standing
97
+ // on its own; a piece inlined into another is the node its kind resolves to.
98
+ if (root?.type === "Box" && Array.isArray(root.children) && root.children.length === 1 && isAxisSwitch(root.children[0] as AnyNode)) {
99
+ root = root.children[0] as AnyNode;
100
+ continue;
101
+ }
102
+ break;
103
+ }
104
+ remapFields(root, (node.props ?? {}) as Record<string, string>);
105
+ if (node.with && typeof node.with === "object") applyLiterals(root, node.with as Record<string, unknown>);
106
+ if (node.visibleWhen) root.visibleWhen = node.visibleWhen;
107
+ // A Ref may override the atom's `action` — the per-host behaviour the atom can't carry
108
+ // (e.g. each wizard step's option sets different fields). Parallel to style/visibleWhen.
109
+ if (node.action) root.action = node.action;
110
+ if (node.style) root.style = { ...(root.style ?? {}), ...node.style };
111
+ // THE BLOCK'S OPENING (2026-09-02, blocks returned): a Ref carrying `children:` fills
112
+ // the inlined root with the CALLER's children — a `band` or `two-column` block styles
113
+ // the frame, the composing layout says what sits inside. Same override family as
114
+ // style/action above; a Ref without children keeps the block's own (a complete block).
115
+ if (Array.isArray(node.children) && node.children.length) root.children = node.children;
116
+ return { piece, root };
117
+ }
118
+
119
+ /** Does this Ref compose a piece whose kind the host chooses (a component with a private
120
+ * axis), rather than a shared atom the client can resolve by name? */
121
+ export function composesAxis(node: AnyNode, contextOrg: string | undefined): boolean {
122
+ const ref = String(node.ref ?? "");
123
+ if (!ref.includes("/")) return false; // bare names are atoms first — the shared case
124
+ const piece = loadDefinition(ref, "component", "referenced", contextOrg) ?? loadDefinition(ref, "template", "referenced", contextOrg);
125
+ return !!piece?.root && Object.keys((piece.state ?? {}) as Record<string, unknown>).length > 0;
126
+ }
127
+
78
128
  export function expandRefs(def: UnoverseDefinition, contextOrg?: string): UnoverseDefinition {
79
129
  // Whatever the tree embeds declares its own starting values; this is where they land on
80
130
  // the slice those inlined nodes actually read (see `inheritState`). Merged AFTER the
@@ -82,7 +82,14 @@ export function loadDefinition(
82
82
  // expanding, because the parsed object is shared and expansion mutates in place.
83
83
  if (k === "atom") {
84
84
  const sig = `${path}:${statSync(path).mtimeMs};${dirSignature([SHARED_DIR.atom])}`;
85
- return cachedBySignature(`def:atom:${lower}${suffix}`, sig, () => form(structuredClone(raw)));
85
+ // `type: atom` → `kind`, exactly as every other kind below. The 2026-08-30 refactor
86
+ // skipped it here, and from 2026-09-01 the atom resource refused a def with no
87
+ // `kind`, so every Studio atom preview said "Unknown Unoverse atom" (seen 2026-09-07).
88
+ return cachedBySignature(`def:atom:${lower}${suffix}`, sig, () => {
89
+ const d = structuredClone(raw) as UnoverseDefinition;
90
+ normalizeDeclaredKind(d);
91
+ return form(d);
92
+ });
86
93
  }
87
94
  const sig = `${path}:${statSync(path).mtimeMs};${dirSignature([SHARED_DIR.atom, SHARED_DIR.component])}`;
88
95
  // The org is stamped BEFORE form so expansion knows whose tree it is walking
@@ -181,11 +188,13 @@ export function loadDefinition(
181
188
  // nothing and the registry row is the whole opt-in (default OFF).
182
189
  def.spatial = true;
183
190
  }
184
- // COMPONENT manifest (OPTIONAL — its PRESENCE means "spatial-capable"; a basic
185
- // component has none and is never discovered). It is the single home for the
186
- // discovery meta (description/whenToUse/title): merged over the def so every
187
- // consumer (nodegen, workbench, catalog) reads one merged shape. `spatial: true`
188
- // is the projected capability flag — derived from presence, never authored.
191
+ // COMPONENT manifest (OPTIONAL — its PRESENCE means "offerable to the map"; a basic
192
+ // component has none and is never a task). It is the single home for the discovery
193
+ // meta (description/whenToUse/title): merged over the def so every consumer reads
194
+ // one merged shape. `spatial: true` is the projected capability flag — derived from
195
+ // presence, never authored. The envelope's own `whenToUse` is palette text and
196
+ // does NOT make a component a task (observed 2026-09-06: every marketplace card
197
+ // listed in the Tasks lane).
189
198
  if (k === "component" && manifestPath) {
190
199
  const m = readDefCached<ComponentManifest>(manifestPath);
191
200
  keepComposition(def, m.description);
@@ -220,7 +229,7 @@ export function loadDefinition(
220
229
  // folders, exactly like `kind:`/`defaultState`.
221
230
  type AppStatesRecord = Record<
222
231
  string,
223
- { layout?: unknown; preview?: unknown; latch?: unknown; states?: Record<string, unknown> } | null
232
+ { layout?: unknown; preview?: unknown; states?: Record<string, unknown> } | null
224
233
  >;
225
234
  const appStates: AppStatesRecord | undefined =
226
235
  k === "app"
@@ -240,11 +249,7 @@ export function loadDefinition(
240
249
  // Sources (`workflow`/`query`/`autoTrigger`) ride along as data until
241
250
  // per-container routing lands.
242
251
  if (k === "app" && manifestPath) {
243
- const man = readDefCached<{ templates?: unknown; states?: Record<string, unknown> }>(manifestPath);
244
252
  const merged: Record<string, unknown> = {};
245
- // The legacy container form (deal-workspace): a top-level `templates:` block.
246
- if (man.templates && typeof man.templates === "object" && !Array.isArray(man.templates))
247
- Object.assign(merged, man.templates as Record<string, unknown>);
248
253
  // A STATE ENTRY CARRIES ONLY WHAT SEEDS IT (owner ruling 2026-08-28
249
254
  // evening): its `preview`. What a place holds and the judgment granted to
250
255
  // it live on the PLACED NODE in the state's layout — projected below,
@@ -412,23 +417,6 @@ export function loadDefinition(
412
417
  // by compilation. (Identified by structural equality: the composed
413
418
  // states/<name> file IS the inlined node.)
414
419
  if (k === "app" && appStates) {
415
- // THE LATCH IS A FACT ABOUT A STATE (MCP guide §The Component Latch, moved off
416
- // the manifest 2026-08-31). Projected here for the same reason `preview` is:
417
- // the raw states block never rides the served def, so anything a state declares
418
- // has to become a projection or it is lost. `def.latches` is what the SDK's
419
- // `resolveLatch` looks the wearer's face up in — a state that says nothing here
420
- // can never be addressed, and an instance retracting out of an addressable
421
- // state drops the pill with nothing to unregister.
422
- const latches: NonNullable<UnoverseDefinition["latches"]> = {};
423
- for (const [sname, sentry] of Object.entries(appStates)) {
424
- const l = (sentry as { latch?: { title?: unknown; icon?: unknown } } | null)?.latch;
425
- // TITLE OR NOTHING: the pill's whole content is its title, so a latch without
426
- // one draws a nameless pill the guest cannot read. Taking a declared-but-empty
427
- // block literally is what makes the authoring slip visible.
428
- if (l && typeof l === "object" && typeof l.title === "string" && l.title.trim())
429
- latches[sname] = { title: l.title, ...(typeof l.icon === "string" ? { icon: l.icon } : {}) };
430
- }
431
- if (Object.keys(latches).length) def.latches = latches;
432
420
  // The tree COMPILES into projections; the raw block never rides the
433
421
  // served def (stateTree is the viewers' shape).
434
422
  delete (def as unknown as Record<string, unknown>).states;
@@ -475,6 +463,10 @@ export function loadDefinition(
475
463
  // - def.initialView = the declared initial (the retract/fallback base)
476
464
  // - def.state.view → the scalar initial (the render scope reads a value)
477
465
  // - a nested `on` + `initial` pair → its scalar initial (the substep key)
466
+ if (k === "app") {
467
+ const places = placesIn(def.layouts ?? { base: def.root });
468
+ if (places.length) def.places = places;
469
+ }
478
470
  if (k === "component") compileComponentTree(def);
479
471
  if (k === "template") synthesizeTemplateRoot(def, folderDir);
480
472
  return k === "atom" ? def : form(def);
@@ -485,3 +477,20 @@ export function loadDefinition(
485
477
  return null;
486
478
  }
487
479
 
480
+
481
+ /** Every `Place` an app's layouts declare, first drawing first, each name once
482
+ * (docs/unoverse/design/screen/UNOVERSE_SCREEN.md §2.2). */
483
+ function placesIn(trees: Record<string, unknown>): { name: string; holds: "one" | "many" }[] {
484
+ const out = new Map<string, "one" | "many">();
485
+ const walk = (n: unknown): void => {
486
+ if (Array.isArray(n)) return n.forEach(walk);
487
+ if (!n || typeof n !== "object") return;
488
+ const node = n as Record<string, unknown>;
489
+ if (node.type === "Place" && typeof node.name === "string" && !out.has(node.name))
490
+ out.set(node.name, node.holds === "one" ? "one" : "many");
491
+ for (const key of ["children", "frame", "fallback", "item"]) walk(node[key]);
492
+ if (node.cases && typeof node.cases === "object") Object.values(node.cases).forEach(walk);
493
+ };
494
+ Object.values(trees).forEach(walk);
495
+ return [...out].map(([name, holds]) => ({ name, holds }));
496
+ }