@unoverse-platform/studio 0.1.90 → 0.1.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/local/plugin.ts +20 -12
  2. package/package.json +13 -3
  3. package/vendor/base/src/agent-mcp/discovery.ts +203 -0
  4. package/vendor/base/src/agent-mcp/handoff.ts +56 -0
  5. package/vendor/base/src/agent-mcp/index.ts +44 -0
  6. package/vendor/base/src/agent-mcp/invoke.ts +140 -0
  7. package/vendor/base/src/agent-mcp/lean.ts +111 -0
  8. package/vendor/base/src/agent-mcp/memoryContext.ts +104 -0
  9. package/vendor/base/src/agent-mcp/memoryIngest.ts +88 -0
  10. package/vendor/base/src/agent-mcp/rowComponents.ts +172 -0
  11. package/vendor/base/src/agent-mcp/tools.ts +134 -0
  12. package/vendor/base/src/agent-mcp/types.ts +130 -0
  13. package/vendor/base/src/auth/auth.ts +278 -0
  14. package/vendor/base/src/auth/authConfig.ts +73 -0
  15. package/vendor/base/src/boot.ts +245 -0
  16. package/vendor/base/src/definitions/definitions.ts +18 -0
  17. package/vendor/base/src/definitions/dsPackage.ts +45 -0
  18. package/vendor/base/src/definitions/fsCache.ts +128 -0
  19. package/vendor/base/src/definitions/index.ts +19 -0
  20. package/vendor/base/src/definitions/inputs.ts +30 -0
  21. package/vendor/base/src/definitions/load/compile/component.ts +80 -0
  22. package/vendor/base/src/definitions/load/compile/index.ts +12 -0
  23. package/vendor/base/src/definitions/load/compile/sections.ts +132 -0
  24. package/vendor/base/src/definitions/load/compile/template.ts +153 -0
  25. package/vendor/base/src/definitions/load/expand/includes.ts +27 -0
  26. package/vendor/base/src/definitions/load/expand/index.ts +17 -0
  27. package/vendor/base/src/definitions/load/expand/inherit.ts +169 -0
  28. package/vendor/base/src/definitions/load/expand/referenced.ts +164 -0
  29. package/vendor/base/src/definitions/load/expand/refs.ts +106 -0
  30. package/vendor/base/src/definitions/load/expand/walk.ts +91 -0
  31. package/vendor/base/src/definitions/load/load.ts +487 -0
  32. package/vendor/base/src/definitions/model.ts +238 -0
  33. package/vendor/base/src/definitions/registry/catalog.ts +643 -0
  34. package/vendor/base/src/definitions/registry/homes.ts +252 -0
  35. package/vendor/base/src/definitions/theme.ts +383 -0
  36. package/vendor/base/src/executor.ts +284 -0
  37. package/vendor/base/src/http/uwsFetchAdapter.ts +260 -0
  38. package/vendor/base/src/items/baseVersion.ts +96 -0
  39. package/vendor/base/src/items/collect.ts +199 -0
  40. package/vendor/base/src/items/fingerprint.ts +34 -0
  41. package/vendor/base/src/items/publish.ts +198 -0
  42. package/vendor/base/src/lint/context.mjs +56 -0
  43. package/vendor/base/src/lint/design/defs.mjs +46 -0
  44. package/vendor/base/src/lint/design/file.mjs +737 -0
  45. package/vendor/base/src/lint/design/index.d.mts +30 -0
  46. package/vendor/base/src/lint/design/index.mjs +996 -0
  47. package/vendor/base/src/lint/design/tokens.mjs +128 -0
  48. package/vendor/base/src/lint/design/vocabulary.mjs +181 -0
  49. package/vendor/base/src/lint/design/walk.mjs +444 -0
  50. package/vendor/base/src/lint/index.mjs +28 -0
  51. package/vendor/base/src/lint/nodes/blocks.mjs +63 -0
  52. package/vendor/base/src/lint/nodes/index.mjs +64 -0
  53. package/vendor/base/src/lint/nodes/load.mjs +126 -0
  54. package/vendor/base/src/lint/nodes/node.mjs +764 -0
  55. package/vendor/base/src/lint/nodes/package.mjs +112 -0
  56. package/vendor/base/src/lint/nodes/schema.mjs +75 -0
  57. package/vendor/base/src/logger.ts +33 -0
  58. package/vendor/base/src/manifests/compose.ts +539 -0
  59. package/vendor/base/src/manifests/executor/callback.ts +107 -0
  60. package/vendor/base/src/manifests/executor/context.ts +107 -0
  61. package/vendor/base/src/manifests/executor/index.ts +47 -0
  62. package/vendor/base/src/manifests/executor/lookup.ts +21 -0
  63. package/vendor/base/src/manifests/executor/memory.ts +62 -0
  64. package/vendor/base/src/manifests/executor/promise.ts +71 -0
  65. package/vendor/base/src/manifests/executor/refire.ts +34 -0
  66. package/vendor/base/src/manifests/executor/send.ts +65 -0
  67. package/vendor/base/src/manifests/executor/session.ts +46 -0
  68. package/vendor/base/src/manifests/executor/toolBridge.ts +266 -0
  69. package/vendor/base/src/manifests/index.ts +280 -0
  70. package/vendor/base/src/manifests/integrity.ts +93 -0
  71. package/vendor/base/src/manifests/rows.ts +33 -0
  72. package/vendor/base/src/manifests/runtime/auth/aws.ts +240 -0
  73. package/vendor/base/src/manifests/runtime/auth/oauth.ts +76 -0
  74. package/vendor/base/src/manifests/runtime/authorize.ts +159 -0
  75. package/vendor/base/src/manifests/runtime/channels/final.ts +171 -0
  76. package/vendor/base/src/manifests/runtime/channels/service.ts +125 -0
  77. package/vendor/base/src/manifests/runtime/channels/workflow.ts +310 -0
  78. package/vendor/base/src/manifests/runtime/context.ts +150 -0
  79. package/vendor/base/src/manifests/runtime/docstore/handlers.ts +547 -0
  80. package/vendor/base/src/manifests/runtime/docstore/index.ts +135 -0
  81. package/vendor/base/src/manifests/runtime/docstore/sectionizer.ts +184 -0
  82. package/vendor/base/src/manifests/runtime/docstore/store.ts +172 -0
  83. package/vendor/base/src/manifests/runtime/docstore/types.ts +60 -0
  84. package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +165 -0
  85. package/vendor/base/src/manifests/runtime/duplex/session.ts +511 -0
  86. package/vendor/base/src/manifests/runtime/events.ts +223 -0
  87. package/vendor/base/src/manifests/runtime/helpers.ts +53 -0
  88. package/vendor/base/src/manifests/runtime/http/allowedHosts.ts +118 -0
  89. package/vendor/base/src/manifests/runtime/http/request.ts +637 -0
  90. package/vendor/base/src/manifests/runtime/http/response.ts +162 -0
  91. package/vendor/base/src/manifests/runtime/index.ts +57 -0
  92. package/vendor/base/src/manifests/runtime/loops/chunk.ts +98 -0
  93. package/vendor/base/src/manifests/runtime/loops/loop.ts +191 -0
  94. package/vendor/base/src/manifests/runtime/loops/paginate.ts +143 -0
  95. package/vendor/base/src/manifests/runtime/loops/poll.ts +169 -0
  96. package/vendor/base/src/manifests/runtime/narrate.ts +53 -0
  97. package/vendor/base/src/manifests/runtime/state.ts +171 -0
  98. package/vendor/base/src/manifests/runtime/templating.ts +110 -0
  99. package/vendor/base/src/manifests/runtime/tools/regionSkills.ts +125 -0
  100. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +542 -0
  101. package/vendor/base/src/manifests/runtime/usage.ts +87 -0
  102. package/vendor/base/src/manifests/source.ts +187 -0
  103. package/vendor/base/src/manifests/watch.ts +67 -0
  104. package/vendor/base/src/markdown/document.ts +141 -0
  105. package/vendor/base/src/paths.ts +180 -0
  106. package/vendor/base/src/platform/audioManager.ts +100 -0
  107. package/vendor/base/src/platform/clientTransport.ts +65 -0
  108. package/vendor/base/src/platform/executionContext.ts +154 -0
  109. package/vendor/base/src/platform/index.ts +10 -0
  110. package/vendor/base/src/platform/pluginAPI.ts +146 -0
  111. package/vendor/base/src/platform/redis.ts +54 -0
  112. package/vendor/base/src/platform/serviceCalls.ts +222 -0
  113. package/vendor/base/src/pluginBase.ts +248 -0
  114. package/vendor/base/src/plugins/README.md +57 -0
  115. package/vendor/base/src/plugins/discovery.ts +87 -0
  116. package/vendor/base/src/plugins/index.ts +16 -0
  117. package/vendor/base/src/plugins/loader.ts +179 -0
  118. package/vendor/base/src/plugins/redis.ts +36 -0
  119. package/vendor/base/src/plugins/state.ts +53 -0
  120. package/vendor/base/src/registry.ts +95 -0
  121. package/vendor/base/src/security/permissions.ts +93 -0
  122. package/vendor/base/src/security/rules.ts +73 -0
  123. package/vendor/base/src/template/SafeExpression.ts +556 -0
  124. package/vendor/base/src/template/StringTemplateResolver.ts +75 -0
  125. package/vendor/base/src/template/index.ts +22 -0
  126. package/vendor/base/src/types.ts +50 -0
  127. package/vendor/base/src/workflow/GetMemoryService.ts +158 -0
  128. package/vendor/base/src/workflow/PlatformMemoryCore.ts +438 -0
  129. package/vendor/base/src/workflow/routing.ts +122 -0
  130. package/vendor/sdk/lib/core/store.ts +26 -6
@@ -0,0 +1,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
+ }