@unoverse-platform/studio 0.1.90 → 0.1.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/local/plugin.ts +20 -12
- package/package.json +13 -3
- package/vendor/base/src/agent-mcp/discovery.ts +203 -0
- package/vendor/base/src/agent-mcp/handoff.ts +56 -0
- package/vendor/base/src/agent-mcp/index.ts +44 -0
- package/vendor/base/src/agent-mcp/invoke.ts +140 -0
- package/vendor/base/src/agent-mcp/lean.ts +111 -0
- package/vendor/base/src/agent-mcp/memoryContext.ts +104 -0
- package/vendor/base/src/agent-mcp/memoryIngest.ts +88 -0
- package/vendor/base/src/agent-mcp/rowComponents.ts +172 -0
- package/vendor/base/src/agent-mcp/tools.ts +134 -0
- package/vendor/base/src/agent-mcp/types.ts +130 -0
- package/vendor/base/src/auth/auth.ts +278 -0
- package/vendor/base/src/auth/authConfig.ts +73 -0
- package/vendor/base/src/boot.ts +245 -0
- package/vendor/base/src/definitions/definitions.ts +18 -0
- package/vendor/base/src/definitions/dsPackage.ts +45 -0
- package/vendor/base/src/definitions/fsCache.ts +128 -0
- package/vendor/base/src/definitions/index.ts +19 -0
- package/vendor/base/src/definitions/inputs.ts +30 -0
- package/vendor/base/src/definitions/load/compile/component.ts +80 -0
- package/vendor/base/src/definitions/load/compile/index.ts +12 -0
- package/vendor/base/src/definitions/load/compile/sections.ts +132 -0
- package/vendor/base/src/definitions/load/compile/template.ts +153 -0
- package/vendor/base/src/definitions/load/expand/includes.ts +27 -0
- package/vendor/base/src/definitions/load/expand/index.ts +17 -0
- package/vendor/base/src/definitions/load/expand/inherit.ts +169 -0
- package/vendor/base/src/definitions/load/expand/referenced.ts +164 -0
- package/vendor/base/src/definitions/load/expand/refs.ts +106 -0
- package/vendor/base/src/definitions/load/expand/walk.ts +91 -0
- package/vendor/base/src/definitions/load/load.ts +487 -0
- package/vendor/base/src/definitions/model.ts +238 -0
- package/vendor/base/src/definitions/registry/catalog.ts +643 -0
- package/vendor/base/src/definitions/registry/homes.ts +252 -0
- package/vendor/base/src/definitions/theme.ts +383 -0
- package/vendor/base/src/executor.ts +284 -0
- package/vendor/base/src/http/uwsFetchAdapter.ts +260 -0
- package/vendor/base/src/items/baseVersion.ts +96 -0
- package/vendor/base/src/items/collect.ts +199 -0
- package/vendor/base/src/items/fingerprint.ts +34 -0
- package/vendor/base/src/items/publish.ts +198 -0
- package/vendor/base/src/lint/context.mjs +56 -0
- package/vendor/base/src/lint/design/defs.mjs +46 -0
- package/vendor/base/src/lint/design/file.mjs +737 -0
- package/vendor/base/src/lint/design/index.d.mts +30 -0
- package/vendor/base/src/lint/design/index.mjs +996 -0
- package/vendor/base/src/lint/design/tokens.mjs +128 -0
- package/vendor/base/src/lint/design/vocabulary.mjs +181 -0
- package/vendor/base/src/lint/design/walk.mjs +444 -0
- package/vendor/base/src/lint/index.mjs +28 -0
- package/vendor/base/src/lint/nodes/blocks.mjs +63 -0
- package/vendor/base/src/lint/nodes/index.mjs +64 -0
- package/vendor/base/src/lint/nodes/load.mjs +126 -0
- package/vendor/base/src/lint/nodes/node.mjs +764 -0
- package/vendor/base/src/lint/nodes/package.mjs +112 -0
- package/vendor/base/src/lint/nodes/schema.mjs +75 -0
- package/vendor/base/src/logger.ts +33 -0
- package/vendor/base/src/manifests/compose.ts +539 -0
- package/vendor/base/src/manifests/executor/callback.ts +107 -0
- package/vendor/base/src/manifests/executor/context.ts +107 -0
- package/vendor/base/src/manifests/executor/index.ts +47 -0
- package/vendor/base/src/manifests/executor/lookup.ts +21 -0
- package/vendor/base/src/manifests/executor/memory.ts +62 -0
- package/vendor/base/src/manifests/executor/promise.ts +71 -0
- package/vendor/base/src/manifests/executor/refire.ts +34 -0
- package/vendor/base/src/manifests/executor/send.ts +65 -0
- package/vendor/base/src/manifests/executor/session.ts +46 -0
- package/vendor/base/src/manifests/executor/toolBridge.ts +266 -0
- package/vendor/base/src/manifests/index.ts +280 -0
- package/vendor/base/src/manifests/integrity.ts +93 -0
- package/vendor/base/src/manifests/rows.ts +33 -0
- package/vendor/base/src/manifests/runtime/auth/aws.ts +240 -0
- package/vendor/base/src/manifests/runtime/auth/oauth.ts +76 -0
- package/vendor/base/src/manifests/runtime/authorize.ts +159 -0
- package/vendor/base/src/manifests/runtime/channels/final.ts +171 -0
- package/vendor/base/src/manifests/runtime/channels/service.ts +125 -0
- package/vendor/base/src/manifests/runtime/channels/workflow.ts +310 -0
- package/vendor/base/src/manifests/runtime/context.ts +150 -0
- package/vendor/base/src/manifests/runtime/docstore/handlers.ts +547 -0
- package/vendor/base/src/manifests/runtime/docstore/index.ts +135 -0
- package/vendor/base/src/manifests/runtime/docstore/sectionizer.ts +184 -0
- package/vendor/base/src/manifests/runtime/docstore/store.ts +172 -0
- package/vendor/base/src/manifests/runtime/docstore/types.ts +60 -0
- package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +165 -0
- package/vendor/base/src/manifests/runtime/duplex/session.ts +511 -0
- package/vendor/base/src/manifests/runtime/events.ts +223 -0
- package/vendor/base/src/manifests/runtime/helpers.ts +53 -0
- package/vendor/base/src/manifests/runtime/http/allowedHosts.ts +118 -0
- package/vendor/base/src/manifests/runtime/http/request.ts +637 -0
- package/vendor/base/src/manifests/runtime/http/response.ts +162 -0
- package/vendor/base/src/manifests/runtime/index.ts +57 -0
- package/vendor/base/src/manifests/runtime/loops/chunk.ts +98 -0
- package/vendor/base/src/manifests/runtime/loops/loop.ts +191 -0
- package/vendor/base/src/manifests/runtime/loops/paginate.ts +143 -0
- package/vendor/base/src/manifests/runtime/loops/poll.ts +169 -0
- package/vendor/base/src/manifests/runtime/narrate.ts +53 -0
- package/vendor/base/src/manifests/runtime/state.ts +171 -0
- package/vendor/base/src/manifests/runtime/templating.ts +110 -0
- package/vendor/base/src/manifests/runtime/tools/regionSkills.ts +125 -0
- package/vendor/base/src/manifests/runtime/tools/toolloop.ts +542 -0
- package/vendor/base/src/manifests/runtime/usage.ts +87 -0
- package/vendor/base/src/manifests/source.ts +187 -0
- package/vendor/base/src/manifests/watch.ts +67 -0
- package/vendor/base/src/markdown/document.ts +141 -0
- package/vendor/base/src/paths.ts +180 -0
- package/vendor/base/src/platform/audioManager.ts +100 -0
- package/vendor/base/src/platform/clientTransport.ts +65 -0
- package/vendor/base/src/platform/executionContext.ts +154 -0
- package/vendor/base/src/platform/index.ts +10 -0
- package/vendor/base/src/platform/pluginAPI.ts +146 -0
- package/vendor/base/src/platform/redis.ts +54 -0
- package/vendor/base/src/platform/serviceCalls.ts +222 -0
- package/vendor/base/src/pluginBase.ts +248 -0
- package/vendor/base/src/plugins/README.md +57 -0
- package/vendor/base/src/plugins/discovery.ts +87 -0
- package/vendor/base/src/plugins/index.ts +16 -0
- package/vendor/base/src/plugins/loader.ts +179 -0
- package/vendor/base/src/plugins/redis.ts +36 -0
- package/vendor/base/src/plugins/state.ts +53 -0
- package/vendor/base/src/registry.ts +95 -0
- package/vendor/base/src/security/permissions.ts +93 -0
- package/vendor/base/src/security/rules.ts +73 -0
- package/vendor/base/src/template/SafeExpression.ts +556 -0
- package/vendor/base/src/template/StringTemplateResolver.ts +75 -0
- package/vendor/base/src/template/index.ts +22 -0
- package/vendor/base/src/types.ts +50 -0
- package/vendor/base/src/workflow/GetMemoryService.ts +158 -0
- package/vendor/base/src/workflow/PlatformMemoryCore.ts +438 -0
- package/vendor/base/src/workflow/routing.ts +122 -0
- package/vendor/sdk/lib/core/store.ts +26 -6
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT AN EMBEDDED PIECE CARRIES UP TO ITS HOST.
|
|
3
|
+
*
|
|
4
|
+
* A Ref inlines a root into the host's tree, and the inlined nodes read the HOST's slice —
|
|
5
|
+
* an embedded piece has no slice of its own. So two things have to travel with it: the
|
|
6
|
+
* starting VALUES it declares (`inheritState`) and the field CONTRACTS its props describe
|
|
7
|
+
* (`inheritProps`). Both are DEFAULTS ONLY, and the host wins on anything it declares.
|
|
8
|
+
*
|
|
9
|
+
* Split from expand.ts 2026-08-30; the rules are unchanged.
|
|
10
|
+
*/
|
|
11
|
+
import type { UnoverseDefinition } from "../../model.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A Ref'd COMPONENT's OWN state initials, carried up to the host that embeds it.
|
|
15
|
+
*
|
|
16
|
+
* A Ref inlines a root into the host's tree, and the inlined nodes read the HOST's slice —
|
|
17
|
+
* an embedded component has no slice of its own. So a component that declares where it
|
|
18
|
+
* starts (`markdown-renderer`: `state: { tab: "0" }`, meaning "the first tab") had that
|
|
19
|
+
* declaration dropped the moment it was embedded: its `Switch on: tab` tested a key nobody
|
|
20
|
+
* had ever set and drew nothing, and a `style.when` comparing that key highlighted nothing.
|
|
21
|
+
* Observed live 2026-08-23 on bpp/lyceum course-card — the tab strip rendered, the body under
|
|
22
|
+
* it was blank until a tab was clicked, and every host would have had to re-declare a default
|
|
23
|
+
* belonging to a component it merely embeds.
|
|
24
|
+
*
|
|
25
|
+
* DEFAULTS ONLY, HOST WINS: an initial is what the piece starts at, never an override of what
|
|
26
|
+
* the host says it starts at.
|
|
27
|
+
*
|
|
28
|
+
* VALUES TRAVEL, THE STATE MACHINE DOES NOT. A component's served `state` holds two unlike
|
|
29
|
+
* things: scalars its author wrote (`tab: "0"`) and the keys the v2 tree COMPILE derived from
|
|
30
|
+
* its state tree — `view`, and one key per `on:` axis, each seeded with its landing substate.
|
|
31
|
+
* Only the authored scalars are the piece's; the axis keys are its machine, and an embedded
|
|
32
|
+
* piece has no machine, because it has no slice to run one on.
|
|
33
|
+
*
|
|
34
|
+
* Landing this with only `view` excluded broke every chat app in the estate within a day
|
|
35
|
+
* (2026-08-24). `text-chat` declares one public state with two substates on a `chatState`
|
|
36
|
+
* axis, which compiles to `chatState: "welcome"`; the apps embed it, inherited that, and their
|
|
37
|
+
* `Switch on: chatState` matched `welcome` forever instead of falling to the default branch
|
|
38
|
+
* where the projected conversation facts pick the substate. The timeline could not appear. The
|
|
39
|
+
* component's own props comment had named the failure in advance: "a non-empty default would
|
|
40
|
+
* pin the mood forever". `view` was never the special case — the whole compiled machine is.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
export function machineryKeys(def: UnoverseDefinition): Set<string> {
|
|
44
|
+
// Read off the DEFINITION's own tree rather than a list kept in step by hand: an axis is
|
|
45
|
+
// machinery because the compile derived it, so the compile's own output names them.
|
|
46
|
+
const keys = new Set<string>(["state", "view", "defaultState"]);
|
|
47
|
+
const walk = (entries: NonNullable<UnoverseDefinition["stateTree"]> | undefined): void => {
|
|
48
|
+
for (const e of entries ?? []) {
|
|
49
|
+
if (typeof e.on === "string") keys.add(e.on);
|
|
50
|
+
walk(e.states as NonNullable<UnoverseDefinition["stateTree"]> | undefined);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
walk(def.stateTree);
|
|
54
|
+
return keys;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function inheritState(into: Record<string, unknown>, from: UnoverseDefinition | null | undefined): void {
|
|
58
|
+
const state = from?.state;
|
|
59
|
+
if (!state || typeof state !== "object" || Array.isArray(state)) return;
|
|
60
|
+
const machinery = machineryKeys(from!);
|
|
61
|
+
for (const [k, v] of Object.entries(state as Record<string, unknown>)) {
|
|
62
|
+
if (machinery.has(k)) continue;
|
|
63
|
+
if (!(k in into)) into[k] = v;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A Ref'd atom's PROP CONTRACTS, carried up to the host that embeds it — the twin of
|
|
69
|
+
* `inheritState`, and for the same reason.
|
|
70
|
+
*
|
|
71
|
+
* THE PROP IS THE BRIEF (UNOVERSE_AUTHORING §3b, 2026-08-30). A field's description and
|
|
72
|
+
* limits used to ride the atom's NODES, so they travelled into the host's tree with the
|
|
73
|
+
* inlined copy and were remapped to the host's field names for free. Now they live on the
|
|
74
|
+
* atom's props, and an inlined atom reads the HOST's slice — so the contract has to be
|
|
75
|
+
* remapped the same way the binds are, or a card composed of atoms compiles to a schema
|
|
76
|
+
* with nothing in it.
|
|
77
|
+
*
|
|
78
|
+
* The atom is the LAW OF A FIELD KIND ("the passage, in Markdown, taken from the source");
|
|
79
|
+
* the host says what THIS field is for. So: DEFAULTS ONLY, HOST WINS, key by key. A field
|
|
80
|
+
* pinned to a literal through `with` never arrives at all — it is not a field anyone fills.
|
|
81
|
+
*/
|
|
82
|
+
/** A prop an Agent may write: described, server-hydrated, or a list of such. */
|
|
83
|
+
function writable(spec: Record<string, unknown>): boolean {
|
|
84
|
+
if (typeof spec.description === "string" && spec.description.trim()) return true;
|
|
85
|
+
if (spec.hydrate !== undefined) return true;
|
|
86
|
+
const items = spec.items;
|
|
87
|
+
if (items && typeof items === "object")
|
|
88
|
+
return Object.values(items as Record<string, unknown>).some(
|
|
89
|
+
(v) => v && typeof v === "object" && writable(v as Record<string, unknown>),
|
|
90
|
+
);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function inheritProps(
|
|
95
|
+
into: Record<string, Record<string, unknown>>,
|
|
96
|
+
atom: UnoverseDefinition | null | undefined,
|
|
97
|
+
propMap: Record<string, string>,
|
|
98
|
+
literals: Record<string, unknown>,
|
|
99
|
+
scope: string[],
|
|
100
|
+
): void {
|
|
101
|
+
// `preview` rides along because it is DESIGN-TIME ONLY — the mock a piece shows before
|
|
102
|
+
// any run. A part that composes another and inherits its contract but not its mock
|
|
103
|
+
// previews as a page of skeletons, which reads as broken rather than unwritten.
|
|
104
|
+
// `default` deliberately does NOT: that is a real value at render time, and it is the
|
|
105
|
+
// host's business whether a field has one.
|
|
106
|
+
// `override` TRAVELS TOO (added 2026-09-01). A field declared CLOSED — hydrated from the
|
|
107
|
+
// row and never the writer's to improve — was reopened the moment its contract was
|
|
108
|
+
// inherited: the word was simply missing from this list, so a piece that said "this name
|
|
109
|
+
// is the venue's" lost that the instant a host drew a list of it. Observed on Perfect
|
|
110
|
+
// Day's moments the day `chapter` stopped restating the tile's contract: `required` was
|
|
111
|
+
// still `["ref"]`, but `title` and `description` were back among the properties a model
|
|
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;
|
|
114
|
+
// AN ATOM INSIDE AN `Each` DESCRIBES THE ITEM, not the page. The scope is the chain of
|
|
115
|
+
// array props the Ref sits inside, so a `prose` atom drawn once per section lands on
|
|
116
|
+
// `sections.items.body` — where a brief on its node used to land by being there.
|
|
117
|
+
let target = into;
|
|
118
|
+
for (const arr of scope) {
|
|
119
|
+
const entry = (target[arr] ??= {}) as Record<string, unknown>;
|
|
120
|
+
entry.type ??= "array";
|
|
121
|
+
target = (entry.items ??= {}) as Record<string, Record<string, unknown>>;
|
|
122
|
+
}
|
|
123
|
+
for (const [name, spec] of Object.entries((atom?.props ?? {}) as Record<string, Record<string, unknown>>)) {
|
|
124
|
+
if (!spec || typeof spec !== "object" || name in literals) continue;
|
|
125
|
+
// Only what the atom says is WRITABLE travels: a described field, a hydrated one
|
|
126
|
+
// (the server fills it), or an array whose ITEMS are either. An atom's plain data
|
|
127
|
+
// props are its own business — not a contract the host inherits, any more than an
|
|
128
|
+
// unbriefed node used to be.
|
|
129
|
+
if (!writable(spec)) continue;
|
|
130
|
+
const hostName = propMap[name] ?? name;
|
|
131
|
+
// Several atoms may draw the same field name in different branches of a Switch (a
|
|
132
|
+
// document's `kind`): the LAST one encountered wins, as it did when the contract rode
|
|
133
|
+
// the nodes. The host's own declaration still wins over all of them, at the merge.
|
|
134
|
+
const slot = (target[hostName] ??= {});
|
|
135
|
+
for (const k of CONTRACT) if (spec[k] !== undefined) slot[k] = spec[k];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Fold the atoms' inherited field contracts (see `inheritProps`) UNDER the definition's
|
|
142
|
+
* own props. Exported because a component's layouts are expanded at load time, before
|
|
143
|
+
* `expandRefs` sees the root — an atom drawn inside a layout must reach the props too,
|
|
144
|
+
* or a page composed of atoms compiles to a schema that knows nothing about its fields.
|
|
145
|
+
*/
|
|
146
|
+
export function mergeInheritedProps(def: UnoverseDefinition, fromAtoms: Record<string, Record<string, unknown>>): void {
|
|
147
|
+
if (!Object.keys(fromAtoms).length) return;
|
|
148
|
+
const own = (def.props ?? {}) as Record<string, Record<string, unknown>>;
|
|
149
|
+
const merged: Record<string, unknown> = {};
|
|
150
|
+
// The HOST's declaration order first (the page's reading order, which the compiled
|
|
151
|
+
// schema's property order carries), then anything only an atom knew about. Item shapes
|
|
152
|
+
// merge a level deeper, so a host that describes one field of a repeated entry does not
|
|
153
|
+
// erase the atom's law for the rest of it.
|
|
154
|
+
const fold = (mine: Record<string, unknown>, theirs: Record<string, unknown>): Record<string, unknown> => {
|
|
155
|
+
const out = { ...theirs, ...mine };
|
|
156
|
+
const mineItems = mine.items as Record<string, Record<string, unknown>> | undefined;
|
|
157
|
+
const theirItems = theirs.items as Record<string, Record<string, unknown>> | undefined;
|
|
158
|
+
if (mineItems && theirItems) {
|
|
159
|
+
const items: Record<string, unknown> = {};
|
|
160
|
+
for (const [k, v] of Object.entries(mineItems)) items[k] = theirItems[k] ? fold(v, theirItems[k]) : v;
|
|
161
|
+
for (const [k, v] of Object.entries(theirItems)) if (!(k in items)) items[k] = v;
|
|
162
|
+
out.items = items;
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
};
|
|
166
|
+
for (const [k, v] of Object.entries(own)) merged[k] = v && typeof v === "object" && fromAtoms[k] ? fold(v, fromAtoms[k]) : v;
|
|
167
|
+
for (const [k, v] of Object.entries(fromAtoms)) if (!(k in merged)) merged[k] = v;
|
|
168
|
+
def.props = merged as UnoverseDefinition["props"];
|
|
169
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REFERENCE, DON'T COPY — the served form that ships each atom once.
|
|
3
|
+
*
|
|
4
|
+
* `expandRefs` specialises every Ref into an inline copy, so a shared button travels
|
|
5
|
+
* inside every definition that uses it, on every page load. This is the other form: the
|
|
6
|
+
* definition with its `Ref` nodes LEFT ALONE, plus one copy of each atom they name.
|
|
7
|
+
*
|
|
8
|
+
* Split from expand.ts 2026-08-30; the rules are unchanged.
|
|
9
|
+
*/
|
|
10
|
+
import type { UnoverseDefinition } from "../../model.js";
|
|
11
|
+
import { loadDefinition } from "../load.js";
|
|
12
|
+
import { inheritState } from "./inherit.js";
|
|
13
|
+
import type { AnyNode } from "./refs.js";
|
|
14
|
+
|
|
15
|
+
export function referencedDefinition(def: UnoverseDefinition, contextOrg?: string): UnoverseDefinition {
|
|
16
|
+
const defs: Record<string, AnyNode> = {};
|
|
17
|
+
const inherit: Record<string, unknown> = {};
|
|
18
|
+
// Depth-first over the tree, collecting each named atom ONCE. An atom may itself Ref
|
|
19
|
+
// another, so collected atoms are walked too — otherwise the client resolves an atom
|
|
20
|
+
// and finds a reference it was never sent.
|
|
21
|
+
const collect = (node: AnyNode | undefined): void => {
|
|
22
|
+
if (!node || typeof node !== "object") return;
|
|
23
|
+
if (Array.isArray(node)) return node.forEach(collect);
|
|
24
|
+
if (node.type === "Ref" && typeof node.ref === "string") {
|
|
25
|
+
const name = node.ref;
|
|
26
|
+
if (!(name in defs)) {
|
|
27
|
+
// The referenced piece in its OWN referenced form, with its references merged up
|
|
28
|
+
// into the same flat map. The client threads `defs` through every recursion, so a
|
|
29
|
+
// Ref inside a referenced piece resolves against the same map. Storing these
|
|
30
|
+
// EXPANDED instead was the difference between product-card costing 17 KB + 173 KB
|
|
31
|
+
// and 17 KB + a handful: it references `document`, and an expanded document drags
|
|
32
|
+
// in every copy this exists to remove.
|
|
33
|
+
const piece =
|
|
34
|
+
loadDefinition(name, "atom", "referenced") ?? loadDefinition(name, "component", "referenced", contextOrg ?? def.org);
|
|
35
|
+
if (piece?.root) {
|
|
36
|
+
defs[name] = piece.root as AnyNode;
|
|
37
|
+
// Same inheritance as the expanded form. The client resolves the Ref itself here,
|
|
38
|
+
// but `defs` carries roots only — state never travels node-side, so it has to be
|
|
39
|
+
// merged onto the definition server-side or a referenced-mode host keeps the bug
|
|
40
|
+
// the expanded form no longer has.
|
|
41
|
+
inheritState(inherit, piece);
|
|
42
|
+
for (const [k, v] of Object.entries((piece.defs ?? {}) as Record<string, AnyNode>)) if (!(k in defs)) defs[k] = v;
|
|
43
|
+
collect(piece.root as AnyNode);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (Array.isArray(node.children)) node.children.forEach(collect);
|
|
48
|
+
if (node.item) collect(node.item as AnyNode);
|
|
49
|
+
if (node.cases && typeof node.cases === "object") for (const k of Object.keys(node.cases)) collect(node.cases[k] as AnyNode);
|
|
50
|
+
if (node.frame) collect(node.frame as AnyNode);
|
|
51
|
+
if (node.fallback) collect(node.fallback as AnyNode);
|
|
52
|
+
};
|
|
53
|
+
// EVERY TREE THE DEFINITION SHIPS, not just `root`. A template's arrangements live in
|
|
54
|
+
// `layouts`, and they are the whole payload — a chat template's `root` is a stub. Walking
|
|
55
|
+
// `root` alone deduplicated nothing at all for templates: measured on bpp-chat,
|
|
56
|
+
// whose four layouts include the SAME 7 KB chrome file, the referenced form came back 11%
|
|
57
|
+
// smaller instead of the ~70% every component saw.
|
|
58
|
+
const trees = () => [def.root, ...Object.values(def.layouts ?? {})] as AnyNode[];
|
|
59
|
+
trees().forEach(collect);
|
|
60
|
+
if (Object.keys(inherit).length) def = { ...def, state: { ...inherit, ...((def.state as Record<string, unknown>) ?? {}) } };
|
|
61
|
+
|
|
62
|
+
// REPEATS ARE REFERENCES TOO, wherever they came from.
|
|
63
|
+
//
|
|
64
|
+
// Hoisting named atoms only fixes duplication that went through a `Ref`. A template
|
|
65
|
+
// composes its arrangements with `$include`, which is copied just as literally: a chat
|
|
66
|
+
// layout shipping four arrangements carried four copies of the same chrome, 35 KB of a
|
|
67
|
+
// 44 KB payload, and not one byte of it was an atom.
|
|
68
|
+
//
|
|
69
|
+
// So this is structural: any subtree appearing more than once becomes ONE entry plus
|
|
70
|
+
// references to it. Identical trees render identically, and the client resolves a
|
|
71
|
+
// parameterless reference by cloning, so nothing about meaning changes. Counted ACROSS
|
|
72
|
+
// the trees, because the repetition that matters most is one arrangement against another.
|
|
73
|
+
// SCOPED TO THIS DEFINITION, because `defs` is not this definition's map alone: every
|
|
74
|
+
// referenced piece merges ITS names up into the same flat namespace above. Generated
|
|
75
|
+
// names used to be a counter restarting at 0 for every definition, so a template's
|
|
76
|
+
// `shared:0` and a component's `shared:0` were the same key. The component's landed
|
|
77
|
+
// first, the template's store was skipped as "already there", and the template's chunk
|
|
78
|
+
// was handed a pointer to the component's — silently, since a collision looks exactly
|
|
79
|
+
// like a chunk already stored. Observed live 2026-08-23: bpp-chat's chrome resolved to
|
|
80
|
+
// bpp/text-chat's, so the `appWidth: chat` it carried never reached the client and the
|
|
81
|
+
// app sized wrong. Nothing errored; a chunk simply became a different chunk.
|
|
82
|
+
const hoisted = hoistRepeats(trees(), defs, `${def.org ? `${def.org}/` : ""}${def.name}`);
|
|
83
|
+
if (hoisted.length) {
|
|
84
|
+
const [root, ...layouts] = hoisted;
|
|
85
|
+
def = { ...def, root };
|
|
86
|
+
if (def.layouts) def.layouts = Object.fromEntries(Object.keys(def.layouts).map((k, i) => [k, layouts[i]]));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return Object.keys(defs).length ? ({ ...def, defs } as UnoverseDefinition) : def;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Replace every repeated subtree, across ALL of a definition's trees, with a reference to
|
|
94
|
+
* one copy. Returns the rewritten trees in the order given.
|
|
95
|
+
*
|
|
96
|
+
* Counting spans the whole set rather than each tree alone, because the duplication that
|
|
97
|
+
* dominates a template is one ARRANGEMENT against another: four layouts that each include
|
|
98
|
+
* the same chrome share nothing within themselves and everything between themselves.
|
|
99
|
+
*
|
|
100
|
+
* Top-down, so the LARGEST repeated tree is hoisted and its children ride inside it rather
|
|
101
|
+
* than being hoisted separately: hoisting a child first would leave the parent's copies
|
|
102
|
+
* differing only in that child's reference, and they would stop matching each other.
|
|
103
|
+
*
|
|
104
|
+
* `MIN` exists because a reference costs about 40 bytes; hoisting a 60-byte node makes the
|
|
105
|
+
* payload bigger and the tree harder to read for nothing.
|
|
106
|
+
*/
|
|
107
|
+
function hoistRepeats(roots: AnyNode[], defs: Record<string, AnyNode>, scope: string): AnyNode[] {
|
|
108
|
+
const MIN = 700; // bytes: comfortably larger than the reference that replaces it
|
|
109
|
+
const counts = new Map<string, number>();
|
|
110
|
+
const count = (node: unknown): void => {
|
|
111
|
+
if (Array.isArray(node)) return node.forEach(count);
|
|
112
|
+
if (!node || typeof node !== "object") return;
|
|
113
|
+
const n = node as AnyNode;
|
|
114
|
+
if (n.type && n.type !== "Ref") {
|
|
115
|
+
const j = JSON.stringify(n);
|
|
116
|
+
if (j.length >= MIN) counts.set(j, (counts.get(j) ?? 0) + 1);
|
|
117
|
+
}
|
|
118
|
+
for (const v of Object.values(n)) if (v && typeof v === "object") count(v);
|
|
119
|
+
};
|
|
120
|
+
roots.forEach(count);
|
|
121
|
+
|
|
122
|
+
const repeated = new Map<string, string>(); // serialized tree → its reference name
|
|
123
|
+
let i = 0;
|
|
124
|
+
// `shared:<definition>:<n>` — the definition segment is what makes a generated name mean
|
|
125
|
+
// ONE chunk in the flat map every referenced piece also writes into.
|
|
126
|
+
for (const [j, n] of counts) if (n > 1) repeated.set(j, `shared:${scope}:${(i++).toString(36)}`);
|
|
127
|
+
if (!repeated.size) return [];
|
|
128
|
+
|
|
129
|
+
const swap = (node: unknown): unknown => {
|
|
130
|
+
if (Array.isArray(node)) return node.map(swap);
|
|
131
|
+
if (!node || typeof node !== "object") return node;
|
|
132
|
+
const n = node as AnyNode;
|
|
133
|
+
if (n.type && n.type !== "Ref") {
|
|
134
|
+
const name = repeated.get(JSON.stringify(n));
|
|
135
|
+
if (name) {
|
|
136
|
+
// The first sighting becomes the stored copy, with ITS OWN repeats hoisted, so a
|
|
137
|
+
// shape repeated inside a repeated shape collapses too.
|
|
138
|
+
if (!(name in defs)) {
|
|
139
|
+
const stored = JSON.parse(JSON.stringify(n)) as AnyNode;
|
|
140
|
+
defs[name] = stored;
|
|
141
|
+
for (const [k, v] of Object.entries(stored)) if (v && typeof v === "object") stored[k] = swap(v) as AnyNode;
|
|
142
|
+
}
|
|
143
|
+
return { type: "Ref", ref: name };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const [k, v] of Object.entries(n)) if (v && typeof v === "object") n[k] = swap(v) as AnyNode;
|
|
147
|
+
return n;
|
|
148
|
+
};
|
|
149
|
+
// A whole tree can itself be a repeat (two arrangements that are the same), so the
|
|
150
|
+
// returned value is used rather than relying on in-place rewriting.
|
|
151
|
+
return roots.map((r) => swap(r) as AnyNode);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
// ---- Folder composition: inline `{ "$include": "name" }` from sibling files ----
|
|
156
|
+
//
|
|
157
|
+
// A template can be a FOLDER (apps/chatlayout/) of multiple data files, so
|
|
158
|
+
// UX pieces live in their own files (chatlayout + user-turn + …). Any
|
|
159
|
+
// `{ "$include": "user-turn" }` node is replaced by the parsed content of
|
|
160
|
+
// `<folder>/user-turn.{yaml,json}` (recursively). All UX stays as served data — the SDK
|
|
161
|
+
// receives one fully-assembled tree.
|
|
162
|
+
//
|
|
163
|
+
// `$include` values are EXTENSION-LESS by design, so a partial can be converted from JSON
|
|
164
|
+
// to YAML without touching any file that references it.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* INLINING A REF: the remaps and the literals.
|
|
3
|
+
*
|
|
4
|
+
* A component composes an atom with `{ type: "Ref", ref: "button", props: {…}, with: {…} }`.
|
|
5
|
+
* `props` REMAPS the atom's field names onto the host's; `with` resolves them to LITERALS.
|
|
6
|
+
* Everything here is that one job — nothing in this file knows what a definition is.
|
|
7
|
+
*
|
|
8
|
+
* Split from expand.ts 2026-08-30 (it had grown five jobs); the rules are unchanged.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type AnyNode = Record<string, any>;
|
|
12
|
+
|
|
13
|
+
// Remap `{{field}}` data-bindings embedded in STYLE values (e.g. a radial gauge's
|
|
14
|
+
// `radial.at: "{{value}}"`, a progress bar's `width: "{{pct}}"`) — the style-side twin
|
|
15
|
+
// of the bind/visibleWhen remap, so an atom whose look is data-driven is parameterizable.
|
|
16
|
+
function remapStyleBindings(style: AnyNode, propMap: Record<string, string>): void {
|
|
17
|
+
for (const k of Object.keys(style)) {
|
|
18
|
+
const v = style[k];
|
|
19
|
+
if (typeof v === "string" && v.includes("{{")) {
|
|
20
|
+
style[k] = v.replace(/\{\{(\w[\w.]*)\}\}/g, (m, f) => (propMap[f] ? `{{${propMap[f]}}}` : m));
|
|
21
|
+
} else if (v && typeof v === "object") {
|
|
22
|
+
remapStyleBindings(v as AnyNode, propMap); // nested: radial.at, hover, etc.
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function remapFields(node: AnyNode, propMap: Record<string, string>): void {
|
|
28
|
+
// A NESTED `Ref` IS REMAPPED THROUGH ITS OWN `props`. The values of an inner reference's
|
|
29
|
+
// `props` are field names in THIS tree's scope, so the host's rename has to reach them:
|
|
30
|
+
// an atom that composes another atom (toggle-row and form-toggle both draw toggle-switch)
|
|
31
|
+
// otherwise hands the inner one the OUTER atom's prop name, which the host has just
|
|
32
|
+
// renamed away. The switch then tests an absent key and never draws its ON state — the
|
|
33
|
+
// control looks right, moves nothing, and says nothing. Same silence as the visibleWhen
|
|
34
|
+
// object form below, one level deeper.
|
|
35
|
+
if (node.type === "Ref" && node.props && typeof node.props === "object") {
|
|
36
|
+
for (const k of Object.keys(node.props as AnyNode)) {
|
|
37
|
+
const v = (node.props as AnyNode)[k];
|
|
38
|
+
if (typeof v === "string" && propMap[v]) (node.props as AnyNode)[k] = propMap[v];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (node.bind) for (const k of Object.keys(node.bind)) if (propMap[node.bind[k]]) node.bind[k] = propMap[node.bind[k]];
|
|
42
|
+
if (typeof node.visibleWhen === "string" && propMap[node.visibleWhen]) node.visibleWhen = propMap[node.visibleWhen];
|
|
43
|
+
// THE OBJECT FORM REMAPS TOO. A guard is a guard whichever way it is written, and only
|
|
44
|
+
// the string form was carried across: an atom guarding on `{ field: "value", ne: "" }`
|
|
45
|
+
// kept testing its OWN prop name after the host remapped `value` to its field, so the
|
|
46
|
+
// guard read an absent key and the branch never showed. Silent, and it bit form-select
|
|
47
|
+
// the day it was written (2026-08-09).
|
|
48
|
+
if (node.visibleWhen && typeof node.visibleWhen === "object" && !Array.isArray(node.visibleWhen)) {
|
|
49
|
+
const g = node.visibleWhen as { field?: string };
|
|
50
|
+
if (typeof g.field === "string" && propMap[g.field]) g.field = propMap[g.field];
|
|
51
|
+
}
|
|
52
|
+
// `when` style variants are the same predicate wearing a different hat: an atom whose
|
|
53
|
+
// LOOK is data-driven (a switch track filling when `on` is true) must follow its host's
|
|
54
|
+
// field for the same reason.
|
|
55
|
+
const when = (node.style as AnyNode | undefined)?.when;
|
|
56
|
+
if (Array.isArray(when)) {
|
|
57
|
+
for (const v of when as { field?: string }[]) {
|
|
58
|
+
if (v && typeof v.field === "string" && propMap[v.field]) v.field = propMap[v.field];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (node.style && typeof node.style === "object") remapStyleBindings(node.style as AnyNode, propMap);
|
|
62
|
+
if (Array.isArray(node.children)) node.children.forEach((c: AnyNode) => remapFields(c, propMap));
|
|
63
|
+
if (node.item) remapFields(node.item as AnyNode, propMap); // Each item subtree
|
|
64
|
+
if (node.cases && typeof node.cases === "object") for (const k of Object.keys(node.cases)) remapFields(node.cases[k] as AnyNode, propMap); // Switch branches
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Resolve an atom's bindings to LITERAL content (the Ref's `with` map). A bind entry
|
|
68
|
+
// whose field is a `with` key becomes a hardcoded attribute; a visibleWhen guard on a
|
|
69
|
+
// provided (truthy) key is satisfied statically and dropped; `{{key}}` style bindings
|
|
70
|
+
// take the value. Keys the atom doesn't bind are ignored (harmless).
|
|
71
|
+
/** Substitute `{{name}}` anywhere inside a style object, at any depth. */
|
|
72
|
+
function applyStyleLiterals(style: unknown, lits: Record<string, unknown>): void {
|
|
73
|
+
if (Array.isArray(style)) return style.forEach((v) => applyStyleLiterals(v, lits));
|
|
74
|
+
if (!style || typeof style !== "object") return;
|
|
75
|
+
const s = style as AnyNode;
|
|
76
|
+
for (const k of Object.keys(s)) {
|
|
77
|
+
const v = s[k];
|
|
78
|
+
if (typeof v === "string" && v.includes("{{"))
|
|
79
|
+
s[k] = v.replace(/\{\{(\w[\w.]*)\}\}/g, (m, f) => (f in lits ? String(lits[f]) : m));
|
|
80
|
+
else if (v && typeof v === "object") applyStyleLiterals(v, lits);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function applyLiterals(node: AnyNode, lits: Record<string, unknown>): void {
|
|
85
|
+
if (node.bind) {
|
|
86
|
+
for (const attr of Object.keys(node.bind)) {
|
|
87
|
+
const field = node.bind[attr];
|
|
88
|
+
if (typeof field === "string" && field in lits) {
|
|
89
|
+
node[attr] = lits[field];
|
|
90
|
+
delete node.bind[attr];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (Object.keys(node.bind).length === 0) delete node.bind;
|
|
94
|
+
}
|
|
95
|
+
if (typeof node.visibleWhen === "string" && node.visibleWhen in lits) {
|
|
96
|
+
if (lits[node.visibleWhen]) delete node.visibleWhen; // statically satisfied
|
|
97
|
+
// falsy literal: keep the (now never-true) guard — the node stays hidden
|
|
98
|
+
}
|
|
99
|
+
// Style values may NEST (a radial's `at`, a `when` clause's `apply`), so this walks the
|
|
100
|
+
// whole style object rather than its top level — a `{{value}}` one level down survived
|
|
101
|
+
// and reached the renderer verbatim.
|
|
102
|
+
if (node.style && typeof node.style === "object") applyStyleLiterals(node.style as AnyNode, lits);
|
|
103
|
+
if (Array.isArray(node.children)) node.children.forEach((c: AnyNode) => applyLiterals(c, lits));
|
|
104
|
+
if (node.item) applyLiterals(node.item as AnyNode, lits);
|
|
105
|
+
if (node.cases && typeof node.cases === "object") for (const k of Object.keys(node.cases)) applyLiterals(node.cases[k] as AnyNode, lits);
|
|
106
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE EXPANSION WALK: inline every `Ref`, gathering what the pieces carry up.
|
|
3
|
+
*
|
|
4
|
+
* One pass over a definition's tree. It calls into `refs.ts` to inline and remap, and into
|
|
5
|
+
* `inherit.ts` for the state and prop contracts an embedded piece hands its host.
|
|
6
|
+
*
|
|
7
|
+
* Split from expand.ts 2026-08-30; the rules are unchanged.
|
|
8
|
+
*/
|
|
9
|
+
import type { UnoverseDefinition } from "../../model.js";
|
|
10
|
+
import { loadDefinition } from "../load.js";
|
|
11
|
+
import { remapFields, applyLiterals, type AnyNode } from "./refs.js";
|
|
12
|
+
import { inheritState, inheritProps, mergeInheritedProps } from "./inherit.js";
|
|
13
|
+
|
|
14
|
+
export function expandNode(node: AnyNode, contextOrg?: string, inherit?: Record<string, unknown>, props?: Record<string, Record<string, unknown>>, scope: string[] = []): AnyNode {
|
|
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;
|
|
54
|
+
return expandNode(root, contextOrg, inherit, props, scope); // expand nested refs too
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(node?.children)) node.children = node.children.map((c: AnyNode) => expandNode(c, contextOrg, inherit, props, scope));
|
|
57
|
+
// `Each` carries a per-item subtree in `item` (not `children`) — expand it too,
|
|
58
|
+
// so an atom `Ref` inside a repeated item is inlined like anywhere else.
|
|
59
|
+
// An `Each`'s `item` draws ONE ITEM, so everything inside it belongs to that
|
|
60
|
+
// array's item shape (see `inheritProps`).
|
|
61
|
+
const itemsField = node?.type === "Each" ? (node.bind as AnyNode | undefined)?.items : undefined;
|
|
62
|
+
if (node?.item) node.item = expandNode(node.item, contextOrg, inherit, props, typeof itemsField === "string" ? [...scope, itemsField] : scope);
|
|
63
|
+
// `Switch` carries its branches in `cases` (a value→subtree map) — expand each, so an
|
|
64
|
+
// atom `Ref` inside a Switch branch is inlined like anywhere else (else the branch
|
|
65
|
+
// renders blank). Parallel to children/item.
|
|
66
|
+
if (node?.cases && typeof node.cases === "object") {
|
|
67
|
+
for (const k of Object.keys(node.cases)) node.cases[k] = expandNode(node.cases[k] as AnyNode, contextOrg, inherit, props, scope);
|
|
68
|
+
}
|
|
69
|
+
// `ComponentSlot` carries chrome in `frame` (born when the slot matches) and `fallback`
|
|
70
|
+
// (shown when it doesn't) — both are real child slots, so an atom `Ref` inside them (e.g. a
|
|
71
|
+
// ✕ CloseButton on a rail/panel frame) must inline like anywhere else, else the Ref survives.
|
|
72
|
+
if (node?.frame) node.frame = expandNode(node.frame as AnyNode, contextOrg, inherit, props, scope);
|
|
73
|
+
if (node?.fallback) node.fallback = expandNode(node.fallback as AnyNode, contextOrg, inherit, props, scope);
|
|
74
|
+
return node;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
export function expandRefs(def: UnoverseDefinition, contextOrg?: string): UnoverseDefinition {
|
|
79
|
+
// Whatever the tree embeds declares its own starting values; this is where they land on
|
|
80
|
+
// the slice those inlined nodes actually read (see `inheritState`). Merged AFTER the
|
|
81
|
+
// whole tree is walked, and beneath the def's own state, so nothing an author wrote is
|
|
82
|
+
// touched — including the v2 `state.view` compile, which runs before this.
|
|
83
|
+
const inherit: Record<string, unknown> = {};
|
|
84
|
+
// The atoms' own field contracts land here (see `inheritProps`), beneath whatever the
|
|
85
|
+
// host declared — merged after the walk, so authored props are never touched.
|
|
86
|
+
const fromAtoms: Record<string, Record<string, unknown>> = {};
|
|
87
|
+
if (def?.root) def.root = expandNode(def.root as AnyNode, contextOrg ?? def.org, inherit, fromAtoms);
|
|
88
|
+
if (Object.keys(inherit).length) def.state = { ...inherit, ...((def.state as Record<string, unknown>) ?? {}) };
|
|
89
|
+
mergeInheritedProps(def, fromAtoms);
|
|
90
|
+
return def;
|
|
91
|
+
}
|