@unoverse-platform/studio 0.1.90 → 0.1.92
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 +21 -65
- 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 +153 -0
- package/vendor/base/src/agent-mcp/lean.ts +114 -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 +234 -0
- package/vendor/base/src/agent-mcp/tools.ts +134 -0
- package/vendor/base/src/agent-mcp/types.ts +138 -0
- package/vendor/base/src/auth/auth.ts +307 -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 +155 -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 +659 -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 +397 -0
- package/vendor/base/src/items/baseVersion.ts +96 -0
- package/vendor/base/src/items/collect.ts +246 -0
- package/vendor/base/src/items/fingerprint.ts +34 -0
- package/vendor/base/src/items/publish.ts +212 -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.d.mts +28 -0
- package/vendor/base/src/lint/nodes/index.mjs +82 -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 +110 -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 +138 -0
- package/vendor/base/src/manifests/runtime/channels/workflow.ts +310 -0
- package/vendor/base/src/manifests/runtime/context.ts +156 -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 +550 -0
- package/vendor/base/src/manifests/runtime/usage.ts +87 -0
- package/vendor/base/src/manifests/source.ts +196 -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 +157 -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 +449 -0
- package/vendor/base/src/workflow/routing.ts +122 -0
- package/vendor/sdk/lib/core/store.ts +26 -6
- package/local/mcp.ts +0 -265
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SECTION-WORD compiler: a template layout's three words → wire claims +
|
|
3
|
+
* section projections. Split from compile.ts into this folder 2026-09-02; the
|
|
4
|
+
* rules are unchanged.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { UnoverseDefinition } from "../../model.js";
|
|
8
|
+
|
|
9
|
+
/** Expand a template layout's THREE-WORD sections (§9b, 2026-08-29; one director word
|
|
10
|
+
* since 2026-09-02) into the wire primitives the renderer already speaks, collecting
|
|
11
|
+
* the section projections as it goes. `static:`/`copywriter:` link a component
|
|
12
|
+
* (face = its basename) and become a limit-1 claim on that face; `director:` with a
|
|
13
|
+
* `component` is a SEARCH place (same limit-1 claim; the grammar rides the section for
|
|
14
|
+
* the fill path), and without one it is the open DELIVERY claim — its face is the
|
|
15
|
+
* `state:` pin when authored (arrivals are switched in and held), else the owning
|
|
16
|
+
* state's name (the free name-match dance). The SDK stays dumb: authors never see
|
|
17
|
+
* ComponentSlot/select/where, and the wire never sees the three words. */
|
|
18
|
+
export function expandTemplateSections(
|
|
19
|
+
node: unknown,
|
|
20
|
+
owningState: string,
|
|
21
|
+
templateName: string,
|
|
22
|
+
out: NonNullable<UnoverseDefinition["sections"]>[string],
|
|
23
|
+
): unknown {
|
|
24
|
+
if (Array.isArray(node)) return node.map((c) => expandTemplateSections(c, owningState, templateName, out));
|
|
25
|
+
if (!node || typeof node !== "object") return node;
|
|
26
|
+
const n = node as Record<string, unknown>;
|
|
27
|
+
const linked = typeof n.static === "string" ? "static" : typeof n.copywriter === "string" ? "copywriter" : null;
|
|
28
|
+
if (linked) {
|
|
29
|
+
const path = String(n[linked]);
|
|
30
|
+
const face = path.split("/").pop() ?? path;
|
|
31
|
+
out.push({ kind: linked, face, component: path, ref: `${templateName}/${face}` });
|
|
32
|
+
const { static: _s, copywriter: _c, ...rest } = n;
|
|
33
|
+
return {
|
|
34
|
+
type: "ComponentSlot",
|
|
35
|
+
select: { from: "all", where: { field: "state", eq: face }, limit: 1 },
|
|
36
|
+
...rest,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// RETIRED VOCABULARY IS A LOUD FAILURE, never a legacy-read (the no-shims law).
|
|
40
|
+
// `query:` merged into `director:` 2026-09-02 (§9e rule 10b REVERSED); `limit` and
|
|
41
|
+
// `ask` retired the same day (`pick` is the one cap word; the question is derived,
|
|
42
|
+
// rule 10d). A stale file fails at load with the fix in the message.
|
|
43
|
+
if (n.query !== undefined) {
|
|
44
|
+
throw new Error(`template "${templateName}": the query: word is retired (§9e rule 10b reversed 2026-09-02) — spell the place director: with the same component/items/type/include/pick`);
|
|
45
|
+
}
|
|
46
|
+
if (n.director !== undefined && (n.director === null || (typeof n.director === "object" && !Array.isArray(n.director)))) {
|
|
47
|
+
const d = (n.director ?? {}) as Record<string, unknown>;
|
|
48
|
+
if (d.limit !== undefined) throw new Error(`template "${templateName}": director limit is retired — the cap word is pick everywhere (§9e rule 9, renamed 2026-09-02)`);
|
|
49
|
+
if (d.ask !== undefined || d.query !== undefined) throw new Error(`template "${templateName}": a director place never states its question — it is derived (§9e rule 10d): the writing, the page's pool, the component's contract, or the guest's ask`);
|
|
50
|
+
/**
|
|
51
|
+
* A SEARCH PLACE (§9e, one word since 2026-09-02) — the director given a component.
|
|
52
|
+
*
|
|
53
|
+
* It LINKS the part exactly as `static:`/`copywriter:` do, so the slot is identical: a
|
|
54
|
+
* limit-1 claim on the part's own face. The content is a search the page runs itself,
|
|
55
|
+
* before any model has spoken; the QUESTION is never here (rule 10d — the writer's
|
|
56
|
+
* headings for an `items` place, the page's pool for a gallery, the component's own
|
|
57
|
+
* contract, or the guest's ask). The layout says only what KIND may answer (`include`)
|
|
58
|
+
* and how many survive (`pick`); none of it reaches the slot — it rides the section
|
|
59
|
+
* for the fill path.
|
|
60
|
+
*
|
|
61
|
+
* NOTHING DECLARES WHICH PROP TAKES THE ROWS. The schema already says: the prop whose
|
|
62
|
+
* items require only `ref`. So a searched part keeps BOTH lanes — its refs filled by
|
|
63
|
+
* search at shell open with no model, its headings and taglines written by the
|
|
64
|
+
* copywriter afterwards (§9e rule 10). Same part, both lanes, in that order.
|
|
65
|
+
*/
|
|
66
|
+
if (typeof d.component === "string" && d.component) {
|
|
67
|
+
const path = String(d.component);
|
|
68
|
+
const face = path.split("/").pop() ?? path;
|
|
69
|
+
// AN ITEMS PLACE (§9e rule 10c): the place holds a LIST the writer shapes — each
|
|
70
|
+
// item's heading arrives WITH its own search, so they can never disagree.
|
|
71
|
+
//
|
|
72
|
+
// THE SEARCH WORDS SIT ON THE THING THAT SEARCHES (owner ruling 2026-09-02): each
|
|
73
|
+
// ITEM searches, so type/include/pick nest INSIDE items: beside min/max, and pick
|
|
74
|
+
// reads unambiguously as per-item. The flat spelling on an items place is a LOUD
|
|
75
|
+
// failure — before this, flat `pick` meant two different scopes depending on
|
|
76
|
+
// whether items: happened to be present.
|
|
77
|
+
const items = d.items && typeof d.items === "object" && !Array.isArray(d.items) ? (d.items as Record<string, unknown>) : null;
|
|
78
|
+
if (items && (d.type !== undefined || d.include !== undefined || d.pick !== undefined))
|
|
79
|
+
throw new Error(
|
|
80
|
+
`template "${templateName}": an items place nests its search words INSIDE items: (each ITEM searches) — items: { min, max, type, include, pick }`,
|
|
81
|
+
);
|
|
82
|
+
const src = items ?? d;
|
|
83
|
+
out.push({
|
|
84
|
+
kind: "director",
|
|
85
|
+
face,
|
|
86
|
+
component: path,
|
|
87
|
+
ref: `${templateName}/${face}`,
|
|
88
|
+
...(src.type === "discovery" || src.type === "intent" ? { type: src.type } : {}),
|
|
89
|
+
...(Array.isArray(src.include) ? { include: src.include.map(String) } : {}),
|
|
90
|
+
...(typeof src.pick === "number" ? { pick: src.pick } : {}),
|
|
91
|
+
...(items ? { items: { min: Number(items.min) || 1, max: Number(items.max) || 1 } } : {}),
|
|
92
|
+
});
|
|
93
|
+
const { director: _d, ...rest } = n;
|
|
94
|
+
return {
|
|
95
|
+
type: "ComponentSlot",
|
|
96
|
+
select: { from: "all", where: { field: "state", eq: face }, limit: 1 },
|
|
97
|
+
...rest,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* THE COMPILER ADDRESSES THE PLACE (§9d, ruled 2026-08-31).
|
|
102
|
+
*
|
|
103
|
+
* A `director:` place is somewhere things may go — it carries no name, no prose and no
|
|
104
|
+
* state, because the ONE mind reading the page's brief already knows what the page is.
|
|
105
|
+
* But a slot still has to select SOMETHING, and two places on one layout may not claim
|
|
106
|
+
* the same face (the reaction contract's one-view-one-surface rule). So the address is
|
|
107
|
+
* handed out here, by position, and an author never types a face name.
|
|
108
|
+
*
|
|
109
|
+
* An AUTHORED `state:` still wins, for the flat-email case §9b rule 4 names — arrivals
|
|
110
|
+
* pinned into one declared state — and for any template written before this ruling.
|
|
111
|
+
*/
|
|
112
|
+
const nth = out.filter((sec) => sec.kind === "director" && !sec.component).length;
|
|
113
|
+
const face = typeof d.state === "string" ? d.state : nth === 0 ? owningState : `${owningState}-${nth + 1}`;
|
|
114
|
+
out.push({
|
|
115
|
+
kind: "director",
|
|
116
|
+
face,
|
|
117
|
+
...(typeof d.state === "string" ? { state: d.state } : {}),
|
|
118
|
+
...(typeof d.pick === "number" ? { pick: d.pick } : {}),
|
|
119
|
+
...(typeof d.rules === "string" ? { rules: d.rules } : {}),
|
|
120
|
+
...(Array.isArray(d.preview) ? { preview: d.preview.map(String) } : {}),
|
|
121
|
+
});
|
|
122
|
+
const { director: _d, ...rest } = n;
|
|
123
|
+
return {
|
|
124
|
+
type: "ComponentSlot",
|
|
125
|
+
select: { from: "all", where: { field: "state", eq: face }, ...(typeof d.pick === "number" ? { limit: d.pick } : {}) },
|
|
126
|
+
...rest,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const o: Record<string, unknown> = {};
|
|
130
|
+
for (const [k2, v] of Object.entries(n)) o[k2] = expandTemplateSections(v, owningState, templateName, out);
|
|
131
|
+
return o;
|
|
132
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The TEMPLATE-ROOT compiler: a template's states (folder form) or root (a flat
|
|
3
|
+
* BLOCK) → the served layouts, sections, previews and root. Split from compile.ts
|
|
4
|
+
* into this folder 2026-09-02; the dead `parts:`/`shapes:` legacy tails (retired
|
|
5
|
+
* §9b vocabulary no definition authored any more) were deleted in the same move.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { UnoverseDefinition } from "../../model.js";
|
|
9
|
+
import { loadDefinition } from "../load.js";
|
|
10
|
+
import { composeIncludes } from "../expand/index.js";
|
|
11
|
+
import { expandTemplateSections } from "./sections.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* ONE PREVIEWED INSTANCE'S SEED — `preview ?? default` per prop, the same values the
|
|
15
|
+
* Components screen has always seeded. An instance seeded with nothing draws as a page of
|
|
16
|
+
* ghosts, which is how a finished arrangement comes to read as a broken one.
|
|
17
|
+
*
|
|
18
|
+
* Shared by both authored forms (a folder's states and a flat block's root): a mock that
|
|
19
|
+
* differed by which form a template happened to be written in would be a preview nobody
|
|
20
|
+
* could trust.
|
|
21
|
+
*/
|
|
22
|
+
function seedFor(ref: string, org?: string): Record<string, unknown> | undefined {
|
|
23
|
+
const part = loadDefinition(ref, "component", "expanded", org);
|
|
24
|
+
const seed = Object.fromEntries(
|
|
25
|
+
Object.entries((part?.props ?? {}) as Record<string, { preview?: unknown; default?: unknown; type?: string }>).map(
|
|
26
|
+
([k, v]) => [k, v?.preview ?? v?.default ?? (v?.type === "array" ? [] : v?.type === "object" ? {} : "")],
|
|
27
|
+
),
|
|
28
|
+
);
|
|
29
|
+
return Object.keys(seed).length ? seed : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Stamp a section list's Studio stand-ins: which refs each face previews, seeded
|
|
33
|
+
* `preview ?? default` (keyed by REF, because a director band names several and may
|
|
34
|
+
* name one twice). A linked part previews ITSELF; a delivery band previews whatever
|
|
35
|
+
* its `preview:` names. */
|
|
36
|
+
function seedSections(
|
|
37
|
+
secs: NonNullable<UnoverseDefinition["sections"]>[string],
|
|
38
|
+
org: string | undefined,
|
|
39
|
+
previews: Record<string, string[]>,
|
|
40
|
+
): void {
|
|
41
|
+
for (const s of secs) {
|
|
42
|
+
// A LINKED part previews ITSELF — including a director SEARCH place, which carries a
|
|
43
|
+
// ref since the one-word merge (a kind check here hid every search place's mock the
|
|
44
|
+
// day the kinds merged). Only an unlinked DELIVERY band previews its `preview:` list.
|
|
45
|
+
const refs = s.ref ? [s.ref] : s.kind === "director" ? (s.preview ?? []) : [];
|
|
46
|
+
if (s.ref) previews[s.face] = [s.ref];
|
|
47
|
+
else if (s.preview?.length) previews[s.face] = s.preview;
|
|
48
|
+
const seeds: Record<string, Record<string, unknown>> = {};
|
|
49
|
+
for (const ref of new Set(refs)) {
|
|
50
|
+
const seed = seedFor(ref, org);
|
|
51
|
+
if (seed) seeds[ref] = seed;
|
|
52
|
+
}
|
|
53
|
+
if (Object.keys(seeds).length) s.seeds = seeds;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function synthesizeTemplateRoot(def: UnoverseDefinition, folderDir?: string): void {
|
|
58
|
+
if (def.root) {
|
|
59
|
+
/**
|
|
60
|
+
* A BLOCK — a flat template that declares a DRAWING instead of a tree (2026-08-31).
|
|
61
|
+
*
|
|
62
|
+
* Its three words still have to compile. Without this the `director:` line sat in the
|
|
63
|
+
* served root verbatim, unexpanded and inert: the renderer speaks ComponentSlot and
|
|
64
|
+
* knows no such word, so a block drew as an empty box wherever it was used (observed
|
|
65
|
+
* in Studio the moment the first one was authored). The folder form compiles its
|
|
66
|
+
* sections as it composes each state's layout; the flat form had no such pass.
|
|
67
|
+
*
|
|
68
|
+
* Compiled into the SAME projections a one-state template serves, so nothing
|
|
69
|
+
* downstream has to know which form it was authored in: `layouts`, `defaultLayout`,
|
|
70
|
+
* `sections`, `publicStates`. The author still writes only `root:`.
|
|
71
|
+
*/
|
|
72
|
+
if ((def as unknown as { states?: unknown }).states === undefined && def.layouts === undefined) {
|
|
73
|
+
const secs: NonNullable<UnoverseDefinition["sections"]>[string] = [];
|
|
74
|
+
const compiled = expandTemplateSections(def.root, def.name, def.name, secs);
|
|
75
|
+
if (secs.length) {
|
|
76
|
+
const previews: Record<string, string[]> = {};
|
|
77
|
+
seedSections(secs, def.org, previews);
|
|
78
|
+
if (Object.keys(previews).length) def.preview = { ...(def.preview ?? {}), ...previews };
|
|
79
|
+
}
|
|
80
|
+
// The projections are built WHETHER OR NOT the root carries section words
|
|
81
|
+
// (2026-09-02): a pure-arrangement block — `band`, `two-column`, style and an
|
|
82
|
+
// opening, no fill lane — is still a template, and without a layout Studio drew it
|
|
83
|
+
// as a blank canvas (observed the day blocks returned). Its own authored children
|
|
84
|
+
// are its preview.
|
|
85
|
+
def.root = compiled as UnoverseDefinition["root"];
|
|
86
|
+
def.layouts = { [def.name]: compiled as never };
|
|
87
|
+
def.defaultLayout = def.name;
|
|
88
|
+
if (secs.length) def.sections = { [def.name]: secs };
|
|
89
|
+
def.publicStates = [def.name];
|
|
90
|
+
def.initialView = def.name;
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// STATES SERVE THE APP-RUNTIME CONTRACT (§9b RULED 2026-08-29): a template's states
|
|
95
|
+
// compose into def.layouts — sections expanded to wire ComponentSlots, no baked
|
|
96
|
+
// mocks — with the FIRST state as the base (def.root + defaultLayout) and the rest
|
|
97
|
+
// as the reaction ladder (def.stateOrder). The SDK then does everything the rules
|
|
98
|
+
// say: an instance's face change surfaces the name-matched layout, its claim catches
|
|
99
|
+
// the instance, steps stay contained in the instance's own slice. Studio seeds real
|
|
100
|
+
// instances from the compiled sections and the same runtime runs.
|
|
101
|
+
const rawStates = (def as unknown as { states?: unknown }).states;
|
|
102
|
+
const states: Record<string, { layout?: unknown } | null> =
|
|
103
|
+
rawStates && typeof rawStates === "object" && !Array.isArray(rawStates)
|
|
104
|
+
? (rawStates as Record<string, { layout?: unknown } | null>)
|
|
105
|
+
: {};
|
|
106
|
+
const shapeNames = Object.keys(states);
|
|
107
|
+
if (!folderDir || !shapeNames.length) return;
|
|
108
|
+
const layouts: Record<string, unknown> = {};
|
|
109
|
+
const sections: NonNullable<UnoverseDefinition["sections"]> = {};
|
|
110
|
+
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
|
+
for (const name of shapeNames) {
|
|
117
|
+
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
|
+
if (typeof st.layout !== "string") continue;
|
|
125
|
+
const secs: NonNullable<UnoverseDefinition["sections"]>[string] = [];
|
|
126
|
+
layouts[name] = expandTemplateSections(composeIncludes({ $include: st.layout }, folderDir), name, def.name, secs);
|
|
127
|
+
sections[name] = secs;
|
|
128
|
+
// MOCK VALUES TRAVEL WITH THE SECTION — a preview host seeds instances from the wire.
|
|
129
|
+
seedSections(secs, def.org, compiledPreview);
|
|
130
|
+
}
|
|
131
|
+
if (!Object.keys(layouts).length) return;
|
|
132
|
+
def.layouts = layouts;
|
|
133
|
+
def.defaultLayout = shapeNames[0];
|
|
134
|
+
def.stateOrder = shapeNames.slice(1);
|
|
135
|
+
// THE FACE IT ARRIVES WEARING (2026-08-30). A template's states compiled into
|
|
136
|
+
// layouts and stopped there, so it carried no `publicStates`/`initialView` — the
|
|
137
|
+
// two projections that say "this is my interface, and this is the face I arrive
|
|
138
|
+
// in". Every consumer of a face was therefore blind to it: a preview could not
|
|
139
|
+
// seed it, a state chip could not activate it, and — the one that matters — an
|
|
140
|
+
// app's ladder had nothing to react to when a template STREAMED in, so a
|
|
141
|
+
// discovered page landed inline in the conversation instead of taking its
|
|
142
|
+
// surface. Stamped exactly as the component tier stamps them, because it is the
|
|
143
|
+
// same contract: states are the interface, the first declared one is arrival.
|
|
144
|
+
def.publicStates = shapeNames;
|
|
145
|
+
def.initialView = shapeNames[0];
|
|
146
|
+
if (Object.keys(latches).length) def.latches = latches;
|
|
147
|
+
if (Object.keys(sections).length) def.sections = sections;
|
|
148
|
+
if (Object.keys(compiledPreview).length) def.preview = { ...(def.preview ?? {}), ...compiledPreview };
|
|
149
|
+
delete (def as unknown as Record<string, unknown>).states;
|
|
150
|
+
// The base arrangement, wrapped in a container so responsive columns can
|
|
151
|
+
// query their surface width in any host.
|
|
152
|
+
def.root = { type: "Box", style: { width: "full", container: "inline-size" }, children: [layouts[shapeNames[0]]] };
|
|
153
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `$include` COMPOSITION: a definition's siblings, inlined.
|
|
3
|
+
*
|
|
4
|
+
* The authoring-time half of composition — a layout pulls in `components/header`, a state
|
|
5
|
+
* pulls in a shared shape. Resolved against the definition's own folder, recursively.
|
|
6
|
+
*
|
|
7
|
+
* Split from expand.ts 2026-08-30; the rules are unchanged.
|
|
8
|
+
*/
|
|
9
|
+
import { readDefCached, defPath } from "../../fsCache.js";
|
|
10
|
+
import type { AnyNode } from "./refs.js";
|
|
11
|
+
|
|
12
|
+
export function composeIncludes(value: any, folderDir: string): any {
|
|
13
|
+
if (Array.isArray(value)) return value.map((v) => composeIncludes(v, folderDir));
|
|
14
|
+
if (value && typeof value === "object") {
|
|
15
|
+
if (typeof value.$include === "string") {
|
|
16
|
+
const incPath = defPath(folderDir, value.$include);
|
|
17
|
+
// Name the missing include: the raw ENOENT names a `.yaml` that was never meant to
|
|
18
|
+
// exist, which reads as a resolver bug rather than a broken reference.
|
|
19
|
+
if (!incPath) throw new Error(`$include "${value.$include}" resolves to no file in ${folderDir}`);
|
|
20
|
+
return composeIncludes(readDefCached(incPath), folderDir);
|
|
21
|
+
}
|
|
22
|
+
const out: AnyNode = {};
|
|
23
|
+
for (const [k, v] of Object.entries(value)) out[k] = composeIncludes(v, folderDir);
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COMPOSITION — the surface. Split by JOB 2026-08-30, the way `markdown/` and
|
|
3
|
+
* `agent-mcp/` are: `expand.ts` had grown to five unrelated jobs in one file.
|
|
4
|
+
*
|
|
5
|
+
* refs.ts inlining a Ref: the field remaps and the `with` literals
|
|
6
|
+
* inherit.ts what an embedded piece carries up (its state, its prop contracts)
|
|
7
|
+
* walk.ts the expansion itself
|
|
8
|
+
* referenced.ts the reference-don't-copy form (each atom shipped once)
|
|
9
|
+
* includes.ts `$include` composition
|
|
10
|
+
*
|
|
11
|
+
* Every export keeps its name, so nothing that imports composition had to change.
|
|
12
|
+
*/
|
|
13
|
+
export { remapFields, applyLiterals, type AnyNode } from "./refs.js";
|
|
14
|
+
export { machineryKeys, inheritState, inheritProps, mergeInheritedProps } from "./inherit.js";
|
|
15
|
+
export { expandNode, expandRefs } from "./walk.js";
|
|
16
|
+
export { referencedDefinition } from "./referenced.js";
|
|
17
|
+
export { composeIncludes } from "./includes.js";
|
|
@@ -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
|
+
}
|