@unoverse-platform/studio 0.1.96 → 0.1.98
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.
- package/bin/studio.mjs +3 -3
- package/local/authoring.ts +1 -1
- package/local/catalog.ts +22 -2
- package/local/keys.ts +1 -1
- package/local/nodes.ts +29 -42
- package/local/plugin.ts +75 -13
- package/local/sample-identity/brand.yaml +24 -0
- package/local/sample-identity/organisation.yaml +14 -0
- package/local/sample-identity/purpose.yaml +14 -0
- package/local/sample-identity/story.yaml +18 -0
- package/local/scaffold.mjs +51 -22
- package/local/source.ts +4 -9
- package/package.json +1 -1
- package/screens/AgentsView.tsx +173 -0
- package/screens/ComponentsView.tsx +2 -2
- package/screens/EmptyLibrary.tsx +1 -1
- package/screens/IdentityHeader.tsx +1 -1
- package/screens/IdentityView.tsx +206 -0
- package/screens/PromptBlocksView.tsx +1 -1
- package/screens/SourceView.tsx +1 -1
- package/screens/TemplatesLibraryView.tsx +91 -6
- package/screens/TemplatesView.tsx +24 -9
- package/screens/index.ts +5 -2
- package/src/App.tsx +14 -4
- package/src/NodeKeys.tsx +1 -1
- package/src/host.ts +3 -16
- package/vendor/base/src/agent-mcp/discovery.ts +76 -18
- package/vendor/base/src/agent-mcp/handoff.ts +11 -31
- package/vendor/base/src/agent-mcp/index.ts +5 -5
- package/vendor/base/src/agent-mcp/invoke.ts +37 -19
- package/vendor/base/src/agent-mcp/lean.ts +19 -0
- package/vendor/base/src/agent-mcp/memoryContext.ts +15 -9
- package/vendor/base/src/agent-mcp/memoryIngest.ts +8 -3
- package/vendor/base/src/agent-mcp/rowComponents.ts +184 -72
- package/vendor/base/src/agent-mcp/tools.ts +136 -7
- package/vendor/base/src/agent-mcp/types.ts +47 -10
- package/vendor/base/src/definitions/load/compile/component.ts +59 -4
- package/vendor/base/src/definitions/load/compile/sections.ts +7 -1
- package/vendor/base/src/definitions/load/compile/template.ts +19 -15
- package/vendor/base/src/definitions/load/expand/inherit.ts +5 -1
- package/vendor/base/src/definitions/load/expand/referenced.ts +21 -2
- package/vendor/base/src/definitions/load/expand/refs.ts +14 -0
- package/vendor/base/src/definitions/load/expand/walk.ts +88 -38
- package/vendor/base/src/definitions/load/load.ts +37 -28
- package/vendor/base/src/definitions/model.ts +8 -12
- package/vendor/base/src/definitions/registry/agents.ts +111 -0
- package/vendor/base/src/definitions/registry/catalog.ts +28 -12
- package/vendor/base/src/definitions/registry/datasetSql.ts +149 -0
- package/vendor/base/src/definitions/registry/homes.ts +69 -5
- package/vendor/base/src/definitions/registry/objects.ts +413 -0
- package/vendor/base/src/definitions/registry/orgDocuments.ts +107 -0
- package/vendor/base/src/definitions/registry/orgPack.ts +50 -0
- package/vendor/base/src/definitions/registry/pipelines.ts +197 -0
- package/vendor/base/src/definitions/theme.ts +1 -2
- package/vendor/base/src/items/collect.ts +33 -25
- package/vendor/base/src/items/publish.ts +31 -7
- package/vendor/base/src/lint/context.mjs +17 -0
- package/vendor/base/src/lint/design/file.mjs +199 -2
- package/vendor/base/src/lint/design/index.mjs +66 -73
- package/vendor/base/src/lint/design/vocabulary.mjs +6 -3
- package/vendor/base/src/lint/design/walk.mjs +14 -4
- package/vendor/base/src/lint/index.mjs +15 -9
- package/vendor/base/src/lint/nodes/blocks.mjs +6 -2
- package/vendor/base/src/lint/nodes/index.mjs +6 -1
- package/vendor/base/src/lint/nodes/node.mjs +157 -7
- package/vendor/base/src/lint/objects/index.d.mts +32 -0
- package/vendor/base/src/lint/objects/index.mjs +324 -0
- package/vendor/base/src/lint/objects/rules.d.mts +44 -0
- package/vendor/base/src/lint/objects/rules.mjs +446 -0
- package/vendor/base/src/lint/prompts/index.d.mts +34 -0
- package/vendor/base/src/lint/prompts/index.mjs +198 -0
- package/vendor/base/src/manifests/compose.ts +81 -10
- package/vendor/base/src/manifests/executor/callback.ts +15 -4
- package/vendor/base/src/manifests/executor/context.ts +12 -1
- package/vendor/base/src/manifests/executor/memory.ts +83 -3
- package/vendor/base/src/manifests/executor/session.ts +50 -0
- package/vendor/base/src/manifests/executor/toolBridge.ts +114 -49
- package/vendor/base/src/manifests/index.ts +1 -1
- package/vendor/base/src/manifests/runtime/auth/aws.ts +14 -1
- package/vendor/base/src/manifests/runtime/channels/final.ts +46 -34
- package/vendor/base/src/manifests/runtime/channels/service.ts +30 -32
- package/vendor/base/src/manifests/runtime/channels/workflow.ts +6 -11
- package/vendor/base/src/manifests/runtime/context.ts +18 -6
- package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +159 -3
- package/vendor/base/src/manifests/runtime/duplex/session.ts +304 -9
- package/vendor/base/src/manifests/runtime/events.ts +39 -3
- package/vendor/base/src/manifests/runtime/http/concurrency.ts +44 -0
- package/vendor/base/src/manifests/runtime/http/request.ts +36 -7
- package/vendor/base/src/manifests/runtime/index.ts +3 -2
- package/vendor/base/src/manifests/runtime/loops/repeat.ts +86 -0
- package/vendor/base/src/manifests/runtime/tools/toolloop.ts +30 -3
- package/vendor/base/src/manifests/runtime/usage.ts +10 -0
- package/vendor/base/src/manifests/source.ts +1 -1
- package/vendor/base/src/paths.ts +16 -8
- package/vendor/base/src/platform/audioManager.ts +32 -0
- package/vendor/base/src/platform/executionContext.ts +14 -0
- package/vendor/base/src/platform/liveInput.ts +52 -0
- package/vendor/base/src/plugins/README.md +1 -1
- package/vendor/base/src/plugins/discovery.ts +1 -1
- package/vendor/base/src/template/SafeExpression.ts +6 -0
- package/vendor/base/src/workflow/GetMemoryService.ts +12 -11
- package/vendor/base/src/workflow/PlatformMemoryCore.ts +23 -10
- package/vendor/sdk/appEngine.tsx +31 -12
- package/vendor/sdk/lib/UnoverseComponent.tsx +8 -4
- package/vendor/sdk/lib/app.tsx +70 -95
- package/vendor/sdk/lib/connection.tsx +47 -57
- package/vendor/sdk/lib/core/actions.ts +6 -0
- package/vendor/sdk/lib/core/apps.ts +66 -81
- package/vendor/sdk/lib/core/client.ts +5 -14
- package/vendor/sdk/lib/core/connection.ts +30 -0
- package/vendor/sdk/lib/core/index.ts +2 -2
- package/vendor/sdk/lib/core/store.ts +90 -15
- package/vendor/sdk/lib/core/types.ts +9 -8
- package/vendor/sdk/lib/primitives.tsx +113 -4
- package/vendor/sdk/lib/realtime/useAudioPlayback.ts +8 -1
- package/vendor/sdk/lib/render.tsx +3 -3
- package/vendor/sdk/lib/voice.tsx +27 -13
- package/vite.config.ts +1 -1
|
@@ -25,11 +25,12 @@
|
|
|
25
25
|
*
|
|
26
26
|
* THE ONE-WIRE-CALL RULE — where our code ends and native MCP begins. Everything here
|
|
27
27
|
* is pure interpretation/routing and never touches the MCP wire, EXCEPT
|
|
28
|
-
* `
|
|
28
|
+
* `invokeTaskNative` — the single function that opens an MCP client and makes a
|
|
29
29
|
* real `tools/call`. From that call onward it is all native MCP (the server renders +
|
|
30
|
-
* elicits; the answers resolve the call).
|
|
31
|
-
* in-process via the caller's `api.callService` because they have a node
|
|
32
|
-
*
|
|
30
|
+
* elicits, or runs an Agent and holds until it completes; the answers resolve the call).
|
|
31
|
+
* Node-MCP rows route in-process via the caller's `api.callService` because they have a node
|
|
32
|
+
* to run; component apps and WORKFLOW TASKS (every task is a native MCP tool, 2026-09-06)
|
|
33
|
+
* ride the wire. Full map:
|
|
33
34
|
* docs/MCP_COMPLETE_GUIDE.md → "where YOUR code ends and native MCP begins".
|
|
34
35
|
*/
|
|
35
36
|
|
|
@@ -54,7 +55,7 @@ export interface DiscoveredMCP {
|
|
|
54
55
|
/**
|
|
55
56
|
* PATH B (UNOVERSE_MCP_APP_PROTOCOL §0.1 / §4b): a self-contained component app —
|
|
56
57
|
* an `app` URI but NO workflow. Holds the app-tool id the adapter hands to
|
|
57
|
-
* `
|
|
58
|
+
* `invokeTaskNative` (the held native `/mcp` call) instead of firing
|
|
58
59
|
* `workflow@trigger`; `workflowId`/`nodeId` are empty. Undefined for node-MCP + path-A apps.
|
|
59
60
|
*/
|
|
60
61
|
component?: string;
|
|
@@ -62,6 +63,33 @@ export interface DiscoveredMCP {
|
|
|
62
63
|
* schema handle. The tool's input schema is read LIVE from `<appUri>/schema` at mint
|
|
63
64
|
* time (`resolveToolDefNative`); spatial rows carry no schema copy. */
|
|
64
65
|
appUri?: string;
|
|
66
|
+
/**
|
|
67
|
+
* A WORKFLOW TASK (2026-09-06): a task-augmented native tool on the platform's /mcp. The
|
|
68
|
+
* value is the tool's identity (`<org>/<name>`), spelled dotted on the wire. Calling it
|
|
69
|
+
* holds until the Agent's run completes and returns the terminal node's output.
|
|
70
|
+
*/
|
|
71
|
+
taskTool?: string;
|
|
72
|
+
/**
|
|
73
|
+
* MINTED FROM A TASK ROW (object_type `task`): an outcome with a door, whichever door it
|
|
74
|
+
* has. The bridge reports a call to it as a `task` step, so the execution timeline draws
|
|
75
|
+
* it as a task and not as one more tool.
|
|
76
|
+
*/
|
|
77
|
+
task?: true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A FUNCTION THE ROWS OF A SEARCH RESULT OFFER (docs/unoverse/data/spatial/objects-in-spatial.md,
|
|
82
|
+
* "Found objects' functions as tools"): the engine names, once per NAME across the result, every
|
|
83
|
+
* public function the found rows' objects declare in api/service.yaml. The harness mints one tool
|
|
84
|
+
* per name, its input the function's own plus the row's universal_id, and a call runs that row's
|
|
85
|
+
* own object's function through the engine's one function door.
|
|
86
|
+
*/
|
|
87
|
+
export interface OfferedFunction {
|
|
88
|
+
name: string;
|
|
89
|
+
description: string;
|
|
90
|
+
input?: Record<string, unknown>;
|
|
91
|
+
/** The objects in the result that offer it, by title. */
|
|
92
|
+
objects: string[];
|
|
65
93
|
}
|
|
66
94
|
|
|
67
95
|
/** A neutral function-tool definition; each family maps this to its wire format. */
|
|
@@ -69,6 +97,8 @@ export interface AgentToolDef {
|
|
|
69
97
|
name: string;
|
|
70
98
|
description: string;
|
|
71
99
|
parameters: Record<string, unknown>;
|
|
100
|
+
/** What the tool returns, from a component's `outputs` block (MCP `outputSchema`). */
|
|
101
|
+
outputSchema?: Record<string, unknown>;
|
|
72
102
|
}
|
|
73
103
|
|
|
74
104
|
/** Mint the model-facing tool definition for a discovered MCP. */
|
|
@@ -91,10 +121,17 @@ export interface AppInvocationContext {
|
|
|
91
121
|
/** Deliver the component's SHELL only: render it in its declared state and return
|
|
92
122
|
* at once — never collect its outputs (§12e "Discovery delivers; it never invokes"). */
|
|
93
123
|
shell?: boolean;
|
|
124
|
+
/** The row's relevance as the search scored it — the director's ordering. */
|
|
125
|
+
rank?: number;
|
|
94
126
|
/** The delivery batch this render belongs to (UNOVERSE_TEMPLATE_MODEL §12e): the
|
|
95
127
|
* server's director judges the WHOLE batch before anything renders. `size` is how
|
|
96
128
|
* many siblings to wait for; `index` is this render's arrival position. */
|
|
97
129
|
batch?: { id: string; size: number; index: number };
|
|
130
|
+
/**
|
|
131
|
+
* A WORKFLOW TASK's own arguments (2026-09-06), as the model wrote them against the task's
|
|
132
|
+
* input schema. Spread into the call beside the identity; the server shapes the run from them.
|
|
133
|
+
*/
|
|
134
|
+
args?: Record<string, unknown>;
|
|
98
135
|
/** Forwarded as a Bearer token so the internal `/mcp` call rides the gated lane too. */
|
|
99
136
|
accessToken?: string;
|
|
100
137
|
/** Internal MCP base; defaults to UNOVERSE_SERVICE_URL or http://localhost:4106. */
|
|
@@ -130,9 +167,9 @@ export interface RowComponent {
|
|
|
130
167
|
component: string;
|
|
131
168
|
/** The row's content AS-IS — the component's state (binds resolve by name). */
|
|
132
169
|
props: Record<string, unknown>;
|
|
133
|
-
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
|
|
170
|
+
/** THE ROW'S RELEVANCE, as the search scored it (`similarity_score`, else `similarity`,
|
|
171
|
+
* else `-distance`, read exactly as `routes/dictionary/search/ranking.ts` ranks). Carried
|
|
172
|
+
* as a NUMBER because arrival position is not rank: renders dispatch in parallel. Rows
|
|
173
|
+
* within one delivery come from one search call, so their numbers compare. */
|
|
174
|
+
rank?: number;
|
|
138
175
|
}
|
|
@@ -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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
151
|
-
//
|
|
152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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 "
|
|
185
|
-
// component has none and is never
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
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;
|
|
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
|
+
}
|