@unoverse-platform/studio 0.1.100 → 0.1.102
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/sample-identity/brand.yaml +2 -0
- package/package.json +1 -1
- package/screens/TemplatesLibraryView.tsx +58 -98
- package/screens/TemplatesView.tsx +18 -2
- package/vendor/base/src/auth/auth.ts +7 -5
- package/vendor/base/src/auth/authConfig.ts +1 -1
- package/vendor/base/src/definitions/load/assets.ts +65 -0
- package/vendor/base/src/definitions/load/compile/component.ts +1 -0
- package/vendor/base/src/definitions/load/compile/index.ts +1 -1
- package/vendor/base/src/definitions/load/compile/sections.ts +26 -116
- package/vendor/base/src/definitions/load/compile/template.ts +41 -25
- package/vendor/base/src/definitions/load/expand/walk.ts +4 -1
- package/vendor/base/src/definitions/load/load.ts +6 -0
- package/vendor/base/src/definitions/model.ts +20 -41
- package/vendor/base/src/definitions/registry/homes.ts +9 -1
- package/vendor/base/src/lint/design/file.mjs +29 -5
- package/vendor/base/src/lint/design/vocabulary.mjs +1 -1
- package/vendor/base/src/lint/design/walk.mjs +31 -156
- package/vendor/base/src/manifests/executor/context.ts +2 -2
- package/vendor/base/src/manifests/executor/toolBridge.ts +3 -29
- package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +35 -12
- package/vendor/base/src/manifests/runtime/duplex/session.ts +123 -48
- package/vendor/base/src/manifests/runtime/index.ts +1 -1
- package/vendor/base/src/platform/audioManager.ts +89 -14
- package/vendor/base/src/platform/clientTransport.ts +8 -0
- package/vendor/base/src/security/permissions.ts +2 -2
- package/vendor/sdk/appEngine.tsx +104 -46
- package/vendor/sdk/lib/UnoverseComponent.tsx +58 -5
- package/vendor/sdk/lib/app.tsx +9 -5
- package/vendor/sdk/lib/connection.tsx +15 -4
- package/vendor/sdk/lib/core/actions.ts +5 -2
- package/vendor/sdk/lib/index.ts +2 -2
- package/vendor/sdk/lib/primitives.tsx +38 -4
- package/vendor/sdk/lib/realtime/useAudioPlayback.ts +6 -1
- package/vendor/sdk/lib/voice.tsx +96 -13
- package/vendor/sdk/main.tsx +1 -1
- package/vendor/sdk/pageInterfaces.tsx +383 -0
|
@@ -9,7 +9,7 @@ import type { UnoverseDefinition } from "../../model.js";
|
|
|
9
9
|
import { loadDefinition } from "../load.js";
|
|
10
10
|
import { composeIncludes } from "../expand/index.js";
|
|
11
11
|
import { expandTemplateSections } from "./sections.js";
|
|
12
|
-
import { placesOfStates } from "./component.js";
|
|
12
|
+
import { placesOfStates, pointStepsAtStep, STEP } from "./component.js";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* ONE PREVIEWED INSTANCE'S SEED — `preview ?? default` per prop, the same values the
|
|
@@ -30,28 +30,17 @@ function seedFor(ref: string, org?: string): Record<string, unknown> | undefined
|
|
|
30
30
|
return Object.keys(seed).length ? seed : undefined;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/** Stamp a section list's Studio stand-ins:
|
|
34
|
-
* `preview ?? default`
|
|
35
|
-
* name one twice). A linked part previews ITSELF; a delivery band previews whatever
|
|
36
|
-
* its `preview:` names. */
|
|
33
|
+
/** Stamp a section list's Studio stand-ins: each placed part previews ITSELF, seeded
|
|
34
|
+
* `preview ?? default` and keyed by its ref. */
|
|
37
35
|
function seedSections(
|
|
38
36
|
secs: NonNullable<UnoverseDefinition["sections"]>[string],
|
|
39
37
|
org: string | undefined,
|
|
40
38
|
previews: Record<string, string[]>,
|
|
41
39
|
): void {
|
|
42
40
|
for (const s of secs) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const refs = s.ref ? [s.ref] : s.kind === "director" ? (s.preview ?? []) : [];
|
|
47
|
-
if (s.ref) previews[s.face] = [s.ref];
|
|
48
|
-
else if (s.preview?.length) previews[s.face] = s.preview;
|
|
49
|
-
const seeds: Record<string, Record<string, unknown>> = {};
|
|
50
|
-
for (const ref of new Set(refs)) {
|
|
51
|
-
const seed = seedFor(ref, org);
|
|
52
|
-
if (seed) seeds[ref] = seed;
|
|
53
|
-
}
|
|
54
|
-
if (Object.keys(seeds).length) s.seeds = seeds;
|
|
41
|
+
previews[s.face] = [s.ref];
|
|
42
|
+
const seed = seedFor(s.ref, org);
|
|
43
|
+
if (seed) s.seeds = { [s.ref]: seed };
|
|
55
44
|
}
|
|
56
45
|
}
|
|
57
46
|
|
|
@@ -60,11 +49,9 @@ export function synthesizeTemplateRoot(def: UnoverseDefinition, folderDir?: stri
|
|
|
60
49
|
/**
|
|
61
50
|
* A BLOCK — a flat template that declares a DRAWING instead of a tree (2026-08-31).
|
|
62
51
|
*
|
|
63
|
-
* Its
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* in Studio the moment the first one was authored). The folder form compiles its
|
|
67
|
-
* sections as it composes each state's layout; the flat form had no such pass.
|
|
52
|
+
* Its placed parts still have to compile: the renderer speaks ComponentSlot, and a
|
|
53
|
+
* part left as a bare `Ref` into `components/` would draw nothing. The folder form
|
|
54
|
+
* compiles its parts as it composes each state's layout; the flat form does it here.
|
|
68
55
|
*
|
|
69
56
|
* Compiled into the SAME projections a one-state template serves, so nothing
|
|
70
57
|
* downstream has to know which form it was authored in: `layouts`, `defaultLayout`,
|
|
@@ -72,13 +59,13 @@ export function synthesizeTemplateRoot(def: UnoverseDefinition, folderDir?: stri
|
|
|
72
59
|
*/
|
|
73
60
|
if ((def as unknown as { states?: unknown }).states === undefined && def.layouts === undefined) {
|
|
74
61
|
const secs: NonNullable<UnoverseDefinition["sections"]>[string] = [];
|
|
75
|
-
const compiled = expandTemplateSections(def.root, def.name,
|
|
62
|
+
const compiled = expandTemplateSections(def.root, def.name, secs);
|
|
76
63
|
if (secs.length) {
|
|
77
64
|
const previews: Record<string, string[]> = {};
|
|
78
65
|
seedSections(secs, def.org, previews);
|
|
79
66
|
if (Object.keys(previews).length) def.preview = { ...(def.preview ?? {}), ...previews };
|
|
80
67
|
}
|
|
81
|
-
// The projections are built WHETHER OR NOT the root
|
|
68
|
+
// The projections are built WHETHER OR NOT the root places parts
|
|
82
69
|
// (2026-09-02): a pure-arrangement block — `band`, `two-column`, style and an
|
|
83
70
|
// opening, no fill lane — is still a template, and without a layout Studio drew it
|
|
84
71
|
// as a blank canvas (observed the day blocks returned). Its own authored children
|
|
@@ -106,6 +93,16 @@ export function synthesizeTemplateRoot(def: UnoverseDefinition, folderDir?: stri
|
|
|
106
93
|
: {};
|
|
107
94
|
const shapeNames = Object.keys(states);
|
|
108
95
|
if (!folderDir || !shapeNames.length) return;
|
|
96
|
+
// STEPS, exactly as a component's (compile/component.ts, "One word: state"): a state's
|
|
97
|
+
// nested substates are its steps. The author writes `state` for them; they live in the
|
|
98
|
+
// hidden step value, so a step never moves the template. Rewritten in the AUTHORED layout,
|
|
99
|
+
// before its sections compile, so a part's own claim (which names the part) is never
|
|
100
|
+
// mistaken for a step of the same name.
|
|
101
|
+
const steps = new Set<string>();
|
|
102
|
+
for (const n of shapeNames) {
|
|
103
|
+
const subs = (states[n] as { states?: Record<string, unknown> } | null)?.states;
|
|
104
|
+
if (subs && typeof subs === "object") Object.keys(subs).forEach((sub) => steps.add(sub));
|
|
105
|
+
}
|
|
109
106
|
const layouts: Record<string, unknown> = {};
|
|
110
107
|
const sections: NonNullable<UnoverseDefinition["sections"]> = {};
|
|
111
108
|
const compiledPreview: Record<string, string[]> = {};
|
|
@@ -113,7 +110,8 @@ export function synthesizeTemplateRoot(def: UnoverseDefinition, folderDir?: stri
|
|
|
113
110
|
const st = (states[name] ?? {}) as Record<string, unknown>;
|
|
114
111
|
if (typeof st.layout !== "string") continue;
|
|
115
112
|
const secs: NonNullable<UnoverseDefinition["sections"]>[string] = [];
|
|
116
|
-
|
|
113
|
+
const authored = composeIncludes({ $include: st.layout }, folderDir);
|
|
114
|
+
layouts[name] = expandTemplateSections(steps.size ? pointStepsAtStep(authored, steps) : authored, def.name, secs);
|
|
117
115
|
sections[name] = secs;
|
|
118
116
|
// MOCK VALUES TRAVEL WITH THE SECTION — a preview host seeds instances from the wire.
|
|
119
117
|
seedSections(secs, def.org, compiledPreview);
|
|
@@ -135,6 +133,24 @@ export function synthesizeTemplateRoot(def: UnoverseDefinition, folderDir?: stri
|
|
|
135
133
|
def.initialView = shapeNames[0];
|
|
136
134
|
const statePlaces = placesOfStates(states);
|
|
137
135
|
if (statePlaces) def.statePlaces = statePlaces;
|
|
136
|
+
// THE STEP TREE, served like a component's: what an Agent navigates and a viewer lists.
|
|
137
|
+
// Components and templates are both tasks.
|
|
138
|
+
def.stateTree = shapeNames.map((n) => {
|
|
139
|
+
const subs = (states[n] as { states?: Record<string, { layout?: unknown } | null> } | null)?.states;
|
|
140
|
+
const names = subs && typeof subs === "object" ? Object.keys(subs) : [];
|
|
141
|
+
// A state that takes the whole screen says so; the SDK takes the browser fullscreen for it.
|
|
142
|
+
const full = (states[n] as { fullscreen?: unknown } | null)?.fullscreen === true ? { fullscreen: true } : {};
|
|
143
|
+
if (!names.length) return { name: n, ...full };
|
|
144
|
+
return {
|
|
145
|
+
name: n,
|
|
146
|
+
...full,
|
|
147
|
+
on: STEP,
|
|
148
|
+
initial: names[0],
|
|
149
|
+
states: names.map((sub) => ({ name: sub, ...(typeof subs![sub]?.layout === "string" ? { layout: subs![sub]!.layout as string } : {}) })),
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
const first = def.stateTree.find((e) => e.states?.length)?.initial;
|
|
153
|
+
if (first) def.state = { ...(def.state ?? {}), [STEP]: first };
|
|
138
154
|
if (Object.keys(sections).length) def.sections = sections;
|
|
139
155
|
if (Object.keys(compiledPreview).length) def.preview = { ...(def.preview ?? {}), ...compiledPreview };
|
|
140
156
|
delete (def as unknown as Record<string, unknown>).states;
|
|
@@ -25,8 +25,11 @@ export function expandNode(node: AnyNode, contextOrg?: string, inherit?: Record<
|
|
|
25
25
|
// so an atom `Ref` inside a repeated item is inlined like anywhere else.
|
|
26
26
|
// An `Each`'s `item` draws ONE ITEM, so everything inside it belongs to that
|
|
27
27
|
// array's item shape (see `inheritProps`).
|
|
28
|
+
// An `Each` over LITERAL `items` draws the author's own content: its item fields are
|
|
29
|
+
// nobody's to fill, so an atom inside it hands the host no contract.
|
|
28
30
|
const itemsField = node?.type === "Each" ? (node.bind as AnyNode | undefined)?.items : undefined;
|
|
29
|
-
|
|
31
|
+
const literalItems = node?.type === "Each" && Array.isArray(node.items);
|
|
32
|
+
if (node?.item) node.item = expandNode(node.item, contextOrg, inherit, literalItems ? undefined : props, typeof itemsField === "string" ? [...scope, itemsField] : scope);
|
|
30
33
|
// `Switch` carries its branches in `cases` (a value→subtree map) — expand each, so an
|
|
31
34
|
// atom `Ref` inside a Switch branch is inlined like anywhere else (else the branch
|
|
32
35
|
// renders blank). Parallel to children/item.
|
|
@@ -27,6 +27,7 @@ import { normalizeDeclaredKind, defaultLayoutOf } from "../model.js";
|
|
|
27
27
|
|
|
28
28
|
import { composeIncludes, expandNode, expandRefs, referencedDefinition, inheritState, mergeInheritedProps } from "./expand/index.js";
|
|
29
29
|
import { compileComponentTree, synthesizeTemplateRoot } from "./compile/index.js";
|
|
30
|
+
import { inlineAssets } from "./assets.js";
|
|
30
31
|
type AnyNode = Record<string, any>;
|
|
31
32
|
|
|
32
33
|
/** Resolve a definition by name (case-insensitive), searching the relevant folder(s).
|
|
@@ -101,6 +102,7 @@ export function loadDefinition(
|
|
|
101
102
|
// The FLAT single-file form compiles its state tree exactly like the
|
|
102
103
|
// folder form (unified 2026-08-29 — a template-local part is flat).
|
|
103
104
|
if (k === "component") compileComponentTree(d);
|
|
105
|
+
inlineAssets(d, dirname(path));
|
|
104
106
|
return form(d);
|
|
105
107
|
});
|
|
106
108
|
}
|
|
@@ -180,6 +182,9 @@ export function loadDefinition(
|
|
|
180
182
|
if (m[key] != null) (def as unknown as Record<string, unknown>)[key] = m[key];
|
|
181
183
|
}
|
|
182
184
|
if (m.preview && typeof m.preview === "object") def.preview = m.preview as Record<string, string[]>;
|
|
185
|
+
// THE WORKFLOW THAT WORKS IT (UNOVERSE_HOST_PAGE.md rule 4): the manifest names it,
|
|
186
|
+
// and the page reads it here to start a call on it.
|
|
187
|
+
if (m.binding && typeof m.binding === "object") def.binding = m.binding as { workflow?: string; trigger?: string };
|
|
183
188
|
// SPATIAL-CAPABLE, exactly as a component is (§9c, ruled 2026-08-30): a
|
|
184
189
|
// template is found by what a guest says, through the same projection, the
|
|
185
190
|
// same registry switch and the same embedded text. The difference is only
|
|
@@ -469,6 +474,7 @@ export function loadDefinition(
|
|
|
469
474
|
}
|
|
470
475
|
if (k === "component") compileComponentTree(def);
|
|
471
476
|
if (k === "template") synthesizeTemplateRoot(def, folderDir);
|
|
477
|
+
inlineAssets(def, folderDir);
|
|
472
478
|
return k === "atom" ? def : form(def);
|
|
473
479
|
});
|
|
474
480
|
}
|
|
@@ -17,58 +17,35 @@ export interface UnoverseDefinition {
|
|
|
17
17
|
/** COMPONENTS AND TEMPLATES: the place each state names. An interface entering a state
|
|
18
18
|
* that names a place moves there (UNOVERSE_SCREEN.md §2.3). */
|
|
19
19
|
statePlaces?: Record<string, string>;
|
|
20
|
-
/** TEMPLATES (§9b
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* this; the wire layouts carry the expanded ComponentSlots. */
|
|
20
|
+
/** TEMPLATES (§9b): the compiled SECTIONS of each state's layout, one entry per
|
|
21
|
+
* placed part (`type: Ref` + `ref: components/<name>`), in arrangement order. The
|
|
22
|
+
* face is the part's basename, and the wire layouts carry the expanded
|
|
23
|
+
* ComponentSlots. Studio's table and seeding read this; so do the fill schema and
|
|
24
|
+
* the Task Runner. What an Agent writes into a part is each prop's `input`. */
|
|
26
25
|
sections?: Record<
|
|
27
26
|
string,
|
|
28
27
|
{
|
|
29
|
-
kind: "
|
|
28
|
+
kind: "part";
|
|
30
29
|
face: string;
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
* page's pool (galleries), the component's own contract, or the guest's ask.
|
|
40
|
-
* Present on the wire only after derivation; a layout carrying one fails to load. */
|
|
41
|
-
ask?: string;
|
|
42
|
-
/** SEARCH places: which `object_type`s may answer, as the six `include*` flags
|
|
43
|
-
* (`ranking.ts`). A day is made of SERVICES; nothing should draw a need, a skill or
|
|
44
|
-
* an app into a moment. APPS ARE ALWAYS OFF on this lane, whatever is listed: the
|
|
45
|
-
* page's searches are SILENT — no app unlocked, no tool minted, no card lane, no
|
|
46
|
-
* director woken — because otherwise every place would rain cards onto the screen
|
|
47
|
-
* while the page was still assembling itself. */
|
|
48
|
-
include?: string[];
|
|
49
|
-
/** The previewed components' own mock values (`preview ?? default` per prop),
|
|
50
|
-
* keyed by REF — a linked part's one component, or each of the director's
|
|
51
|
-
* stand-ins. A preview host seeds instances with these the way the Components
|
|
52
|
-
* screen does; without them everything draws as ghosts and a finished arrangement
|
|
53
|
-
* reads as a broken one. Never used at runtime: a delivery fills the instance. */
|
|
30
|
+
/** The authored path (components/masthead). */
|
|
31
|
+
component: string;
|
|
32
|
+
/** The resolvable scoped ref (email-digest/masthead). */
|
|
33
|
+
ref: string;
|
|
34
|
+
/** The part's own mock values (`preview ?? default` per prop), keyed by REF. A
|
|
35
|
+
* preview host seeds instances with these the way the Components screen does;
|
|
36
|
+
* without them everything draws as ghosts and a finished arrangement reads as a
|
|
37
|
+
* broken one. Never used at runtime: a fill writes the instance. */
|
|
54
38
|
seeds?: Record<string, Record<string, unknown>>;
|
|
55
|
-
/** Delivery bands only (a director section with no component). */
|
|
56
|
-
state?: string;
|
|
57
|
-
rules?: string;
|
|
58
|
-
preview?: string[];
|
|
59
|
-
/** Both director modes: how many the director keeps — the ONE cap word. */
|
|
60
|
-
pick?: number;
|
|
61
|
-
/** Search places: which search answers (authored; derived when absent). */
|
|
62
|
-
type?: "discovery" | "intent";
|
|
63
|
-
/** An items place (§9e rule 10c): the writer shapes min–max items, each heading
|
|
64
|
-
* arriving with its own search. */
|
|
65
|
-
items?: { min: number; max: number };
|
|
66
39
|
}[]
|
|
67
40
|
>;
|
|
68
41
|
/** TEMPLATES: Studio preview mocks per slot — interface refs, each rendering its
|
|
69
42
|
* own defaults (the interface-model rule: mocks are the interface's OWN defaults,
|
|
70
43
|
* never authored preview data). From the template's manifest.yaml. */
|
|
71
44
|
preview?: Record<string, string[]>;
|
|
45
|
+
/** TEMPLATES: the workflow that works this template as a task, from its manifest.yaml
|
|
46
|
+
* (docs/unoverse/design/sdk/UNOVERSE_HOST_PAGE.md rule 4). A page starts a call on it from
|
|
47
|
+
* the avatar; a template without one starts nothing. */
|
|
48
|
+
binding?: { workflow?: string; trigger?: string };
|
|
72
49
|
/** Source basename for FILE definitions (atoms) — the identity refs resolve by. */
|
|
73
50
|
file?: string;
|
|
74
51
|
/** The client org it belongs to (server-injected from its folder, never
|
|
@@ -176,6 +153,8 @@ export interface UnoverseDefinition {
|
|
|
176
153
|
on?: string;
|
|
177
154
|
initial?: string;
|
|
178
155
|
states?: { name: string; layout?: string }[];
|
|
156
|
+
/** The state takes the whole screen (the authored `fullscreen: true`). */
|
|
157
|
+
fullscreen?: boolean;
|
|
179
158
|
}[];
|
|
180
159
|
}
|
|
181
160
|
|
|
@@ -302,7 +302,8 @@ export function resolveComponentDirs(
|
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
/** Resolve a component ref (bare or `<org>/<name>`, either tier) to its ON-DISK
|
|
305
|
-
* FOLDER — the home of its manifest and any lifecycle handler files (onstart.js …).
|
|
305
|
+
* FOLDER — the home of its manifest and any lifecycle handler files (onstart.js …). A
|
|
306
|
+
* template resolves here too, after components: both are tasks.
|
|
306
307
|
* Returns null for a flat/marketplace component with no folder. Used by the MCP
|
|
307
308
|
* render path to run component lifecycle handlers. */
|
|
308
309
|
export function componentFolder(ref: string): string | null {
|
|
@@ -312,5 +313,12 @@ export function componentFolder(ref: string): string | null {
|
|
|
312
313
|
const folder = join(dir, lower);
|
|
313
314
|
if (defPath(folder, "manifest") || defPath(folder, lower)) return folder;
|
|
314
315
|
}
|
|
316
|
+
// A TEMPLATE IS A TASK TOO (ruled 2026-09-07: components and templates are both tasks), so
|
|
317
|
+
// its manifest's lifecycle, analytics and finish line are found the same way. A component
|
|
318
|
+
// of the same name wins, as it always has.
|
|
319
|
+
for (const { dir } of templateDirs(refOrg)) {
|
|
320
|
+
const folder = join(dir, lower);
|
|
321
|
+
if (defPath(folder, "manifest") || defPath(folder, lower)) return folder;
|
|
322
|
+
}
|
|
315
323
|
return null;
|
|
316
324
|
}
|
|
@@ -413,6 +413,28 @@ function lintFile(file) {
|
|
|
413
413
|
}
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
+
// AN ORG'S OWN IMAGES (definitions/load/assets.ts): `src: assets/<file>` must name a file in
|
|
417
|
+
// an assets/ folder from the file's own folder up to the org's (a template keeps its own), or
|
|
418
|
+
// the shared one, of an image type, under 256 KB.
|
|
419
|
+
{
|
|
420
|
+
const home = (orgDirs ?? []).find((d) => file.startsWith(d + sep)) ?? null;
|
|
421
|
+
const shared = DS ?? null;
|
|
422
|
+
const walk = (n) => {
|
|
423
|
+
if (Array.isArray(n)) return n.forEach(walk);
|
|
424
|
+
if (!n || typeof n !== "object") return;
|
|
425
|
+
if (n.type === "Image" && typeof n.src === "string" && n.src.startsWith("assets/")) {
|
|
426
|
+
const dirs = [];
|
|
427
|
+
for (let d = dirname(file); home && d.startsWith(home); d = dirname(d)) dirs.push(d);
|
|
428
|
+
const hit = [...dirs, shared].map((d) => d && join(d, n.src)).find((f) => f && existsSync(f));
|
|
429
|
+
if (!hit) report("error", file, `image "${n.src}" is not in this org's assets/ folder (docs.unoverse.ai/design/styles-and-tokens)`);
|
|
430
|
+
else if (!/\.(svg|png|jpe?g|webp|gif)$/i.test(n.src)) report("error", file, `image "${n.src}" is not an image type the platform serves (svg, png, jpg, webp, gif)`);
|
|
431
|
+
else if (statSync(hit).size > 256 * 1024) report("error", file, `image "${n.src}" is over 256 KB; a photo belongs on a CDN, not in the design folder`);
|
|
432
|
+
}
|
|
433
|
+
Object.values(n).forEach(walk);
|
|
434
|
+
};
|
|
435
|
+
walk(json);
|
|
436
|
+
}
|
|
437
|
+
|
|
416
438
|
// A STATE'S PLACE (UNOVERSE_SCREEN.md §2.3) is `chat` or a Place some app declares. A name no
|
|
417
439
|
// app has sends the interface to the conversation without a word, so it is refused here.
|
|
418
440
|
if (placeNames && !isAppPath(file) && json && typeof json === "object" && !Array.isArray(json) && json.states && typeof json.states === "object") {
|
|
@@ -552,8 +574,10 @@ function lintFile(file) {
|
|
|
552
574
|
report("error", file, `preview."${state}" must be an array of component names, or an object of template-state mock data`);
|
|
553
575
|
continue;
|
|
554
576
|
}
|
|
577
|
+
// `name#state` names the state the seed opens in (the address grammar, owner
|
|
578
|
+
// 2026-09-11); the lookup is the name before the hash.
|
|
555
579
|
for (const c of list)
|
|
556
|
-
if (comps && !comps.has(String(c).toLowerCase()))
|
|
580
|
+
if (comps && !comps.has(String(c).split("#")[0].toLowerCase()))
|
|
557
581
|
report("error", file, `preview."${state}" names unknown component "${c}". No match in design/marketplace/components/ or this org's components/ (org-privacy: another org's components are out of reach; lookup is case-insensitive)`);
|
|
558
582
|
}
|
|
559
583
|
}
|
|
@@ -676,13 +700,13 @@ function lintFile(file) {
|
|
|
676
700
|
// EVERY PROP CARRIES A DESCRIPTION (UNOVERSE_AUTHORING §3b, RULED 2026-08-30).
|
|
677
701
|
//
|
|
678
702
|
// It reads BOTH ways, which is why it is compulsory rather than encouraged. A
|
|
679
|
-
// description is not only what
|
|
703
|
+
// description is not only what an Agent fills against: it is what lets an agent
|
|
680
704
|
// UNDERSTAND the screen it is looking at. `read` reports where the person is; the
|
|
681
705
|
// descriptions say what is actually in front of them. A screen of unnamed fields is
|
|
682
706
|
// one no agent can reason about, in either direction.
|
|
683
707
|
//
|
|
684
|
-
// AND IT SAYS NOTHING ABOUT PERMISSION. That is the
|
|
685
|
-
//
|
|
708
|
+
// AND IT SAYS NOTHING ABOUT PERMISSION. That is the prop's `input` (UNOVERSE_AUTHORING
|
|
709
|
+
// "input decides"), never its description. The rule this
|
|
686
710
|
// replaces was "a described prop is a writable prop", which made silence the safety
|
|
687
711
|
// mechanism: a field was protected because nobody had explained it, and explaining it
|
|
688
712
|
// was what exposed it. Describing a field is now free of consequence, so there is no
|
|
@@ -691,7 +715,7 @@ function lintFile(file) {
|
|
|
691
715
|
.filter(([, v]) => !(v && typeof v === "object" && typeof v.description === "string" && v.description.trim()))
|
|
692
716
|
.map(([k]) => k);
|
|
693
717
|
if (undescribed.length)
|
|
694
|
-
report("error", file, `props [${undescribed.join(", ")}] carry no "description". Every prop says what it IS, so an agent can read the screen as well as fill it. A description grants nothing: permission is the
|
|
718
|
+
report("error", file, `props [${undescribed.join(", ")}] carry no "description". Every prop says what it IS, so an agent can read the screen as well as fill it. A description grants nothing: permission is the prop's input: true or false (UNOVERSE_AUTHORING §3b)`);
|
|
695
719
|
|
|
696
720
|
// only components that ADOPTED the structure are held to the full discipline
|
|
697
721
|
if (hasLayouts || stateFiles.length || hasStateBlock) {
|
|
@@ -114,7 +114,7 @@ export const STYLE_KEYS = new Set([
|
|
|
114
114
|
"width", "height", "maxWidth", "minWidth", "minHeight", "maxHeight", "flex",
|
|
115
115
|
"padding", "margin", "gap", "overflow",
|
|
116
116
|
"position", "inset", "top", "right", "bottom", "left", "zIndex",
|
|
117
|
-
"direction", "wrap", "align", "justify", "display", "columns", "span", "stackBelow", "container", "hideBelow", "hideAbove",
|
|
117
|
+
"direction", "wrap", "align", "justify", "display", "columns", "span", "stackBelow", "stage", "container", "hideBelow", "hideAbove",
|
|
118
118
|
"background", "radial", "border", "borderTop", "borderRight", "borderBottom", "borderLeft",
|
|
119
119
|
"outline", "shadow", "radius", "radiusTopLeft", "radiusTopRight", "radiusBottomLeft", "radiusBottomRight",
|
|
120
120
|
"font", "weight", "lineHeight", "color", "textAlign", "fit",
|
|
@@ -13,26 +13,19 @@
|
|
|
13
13
|
* The context is created per run by index.mjs, so nothing survives between runs.
|
|
14
14
|
*/
|
|
15
15
|
import { existsSync, statSync, readdirSync, readFileSync } from "node:fs";
|
|
16
|
-
import { join, dirname,
|
|
16
|
+
import { join, dirname, relative, sep } from "node:path";
|
|
17
17
|
import { PRIMITIVES, CONDITION_KEYS, STYLE_KEYS, RAW_VALUE, CHILD_NODE_KEYS, PARTIAL_DIRS, DIMENSION_KEYS, UNIVERSAL_PROPS, PRIMITIVE_PROPS, PRIMITIVE_BINDS } from "./vocabulary.mjs";
|
|
18
|
-
import { isDefFile,
|
|
18
|
+
import { isDefFile, defPath } from "./defs.mjs";
|
|
19
19
|
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// page's own searches are SILENT (no app unlocked, no tool minted, no card lane, no
|
|
27
|
-
// director woken), or every place would rain cards onto a page still assembling itself.
|
|
28
|
-
// `products` since migration 035 (2026-09-05; `services` retired 2026-09-07, `actions` with it:
|
|
29
|
-
// no row is written as one). Each word maps to a declared spatial type (guarded by a test).
|
|
30
|
-
const QUERY_TYPES = ["products", "images", "needs"];
|
|
20
|
+
// RETIRED LAYOUT WORDS. A template layout places its own parts with the ordinary element,
|
|
21
|
+
// `type: Ref` + `ref: components/<name>`, and what an Agent writes into a part is each
|
|
22
|
+
// prop's `input`, never the element that places it (UNOVERSE_TEMPLATE_MODEL §9b).
|
|
23
|
+
const RETIRED_WORDS = ["static", "director", "copywriter", "query"];
|
|
24
|
+
/** The one way a layout places a part: a `Ref` into the template's own `components/`. */
|
|
25
|
+
const PART_PREFIX = "components/";
|
|
31
26
|
|
|
32
27
|
export function makeWalkNode(ctx) {
|
|
33
|
-
const { report, checkCondition, checkDimension, checkToken, appSizesForFile,
|
|
34
|
-
// One director per LAYOUT FILE, tracked for this run only (each file is walked once).
|
|
35
|
-
const directorsSeen = new Map();
|
|
28
|
+
const { report, checkCondition, checkDimension, checkToken, appSizesForFile, refResolves, canonicalRef, declaredProps, atomsDirExists, stepList, spaceSteps, isAppPath, isTemplatePath, isTemplatePart, defRoot } = ctx;
|
|
36
29
|
|
|
37
30
|
function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
|
|
38
31
|
if (Array.isArray(node)) return node.forEach((n) => walkNode(n, file, root, widthCap));
|
|
@@ -54,147 +47,29 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
|
|
|
54
47
|
return; // the included file is linted on its own
|
|
55
48
|
}
|
|
56
49
|
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
* writing, the page's pool, the component's contract, or the guest's ask.
|
|
76
|
-
*/
|
|
77
|
-
const d = node.director;
|
|
78
|
-
if (!d || typeof d !== "object" || Array.isArray(d)) {
|
|
79
|
-
report("error", file, `"director:" is a block, not a path (§9b rule 1)`);
|
|
80
|
-
} else if (d.limit !== undefined) {
|
|
81
|
-
report("error", file, `director.limit is retired — the one cap word is "pick": how many the director keeps (§9e rule 9, renamed 2026-09-02)`);
|
|
82
|
-
} else if (d.ask !== undefined || d.query !== undefined) {
|
|
83
|
-
report("error", file, `a director place never states its question — it is DERIVED (§9e rule 10d): the writer's headings (items), the page's pool (a gallery), the component's own contract, or the guest's ask`);
|
|
84
|
-
} else if (typeof d.component === "string" && d.component) {
|
|
85
|
-
// A SEARCH PLACE. The checks here are the ones that fail SILENTLY at runtime: a
|
|
86
|
-
// place missing include or pick is skipped, and a kind nobody stores draws
|
|
87
|
-
// nothing — both read as "the design is broken" with no log saying why.
|
|
88
|
-
//
|
|
89
|
-
// THE SEARCH WORDS SIT ON THE THING THAT SEARCHES (owner ruling 2026-09-02): on
|
|
90
|
-
// an items place each ITEM searches, so type/include/pick nest INSIDE items:
|
|
91
|
-
// beside min/max, and pick reads unambiguously as per-item. Spelled flat, the
|
|
92
|
-
// same word meant two scopes depending on whether items: was present.
|
|
93
|
-
const itemed = d.items && typeof d.items === "object" && !Array.isArray(d.items);
|
|
94
|
-
if (d.items !== undefined && !itemed)
|
|
95
|
-
report("error", file, `director.items is a block — items: { min, max, type, include, pick } (§9e rule 10c)`);
|
|
96
|
-
if (itemed && (d.type !== undefined || d.include !== undefined || d.pick !== undefined))
|
|
97
|
-
report("error", file, `an items place nests its search words INSIDE items: { min, max, type, include, pick } — each ITEM searches, so pick reads per item (§9e rule 10c, 2026-09-02)`);
|
|
98
|
-
const TOP_KEYS = itemed ? ["component", "items"] : SEARCH_KEYS;
|
|
99
|
-
for (const k of Object.keys(d))
|
|
100
|
-
if (!TOP_KEYS.includes(k))
|
|
101
|
-
report("error", file, `a search place has no "${k}". It reads ${TOP_KEYS.map((x) => `"${x}"`).join(", ")} (§9e; "state"/"rules" belong to a delivery band — a place with a component is governed by the page's brief)`);
|
|
102
|
-
if (!defPath(root, d.component) && !existsSync(join(root, d.component)))
|
|
103
|
-
report("error", file, `director.component "${d.component}" does not resolve under ${relative(process.cwd(), root)}/ (§9e)`);
|
|
104
|
-
const ITEM_KEYS = ["min", "max", "type", "include", "pick"];
|
|
105
|
-
if (itemed) {
|
|
106
|
-
for (const k of Object.keys(d.items))
|
|
107
|
-
if (!ITEM_KEYS.includes(k))
|
|
108
|
-
report("error", file, `director.items has no "${k}". It reads ${ITEM_KEYS.map((x) => `"${x}"`).join(", ")} (§9e rule 10c)`);
|
|
109
|
-
const { min, max } = d.items;
|
|
110
|
-
if (typeof min !== "number" || typeof max !== "number" || min < 1 || max < min)
|
|
111
|
-
report("error", file, `director.items carries { min, max } — how many items the writer may shape, 1 <= min <= max (§9e rule 10c)`);
|
|
112
|
-
}
|
|
113
|
-
// The search words, read where they live: the place itself, or each item.
|
|
114
|
-
const s = itemed ? d.items : d;
|
|
115
|
-
const at = itemed ? "director.items" : "director";
|
|
116
|
-
// REQUIRED, both (2026-09-01, carried over). The engine supplies NO defaults: a
|
|
117
|
-
// default include or pick would be one template's answer written into the platform.
|
|
118
|
-
if (s.include === undefined)
|
|
119
|
-
report("error", file, `${at}.include is REQUIRED on a search place — what KIND may answer; the engine has no default kind (§9e rule 6)`);
|
|
120
|
-
else if (!Array.isArray(s.include) || s.include.some((t) => typeof t !== "string"))
|
|
121
|
-
report("error", file, `${at}.include is an array of result types — ${QUERY_TYPES.map((t) => `"${t}"`).join(", ")} (§9e rule 6)`);
|
|
122
|
-
else
|
|
123
|
-
for (const t of s.include)
|
|
124
|
-
if (!QUERY_TYPES.includes(String(t)))
|
|
125
|
-
report("error", file, `${at}.include has no "${t}". A place may admit ${QUERY_TYPES.map((x) => `"${x}"`).join(", ")}; apps and skills are never included, because the page's own searches are SILENT (§9e rule 6)`);
|
|
126
|
-
if (s.pick === undefined)
|
|
127
|
-
report("error", file, `${at}.pick is REQUIRED — how many the director keeps${itemed ? " PER ITEM" : ""}; the engine has no default count (§9e rule 9)`);
|
|
128
|
-
else if (typeof s.pick !== "number" || s.pick < 1)
|
|
129
|
-
report("error", file, `${at}.pick is a NUMBER — how many the director keeps${itemed ? " per item" : ""}, a cap the machine enforces (§9e rule 9)`);
|
|
130
|
-
// `type` is OPTIONAL since the question derives (rule 10d): authored it stands;
|
|
131
|
-
// absent, a contract subject searches as intent and the guest's ask as discovery.
|
|
132
|
-
if (s.type !== undefined && s.type !== "discovery" && s.type !== "intent")
|
|
133
|
-
report("error", file, `${at}.type is "discovery" (what lives around a topic) or "intent" (a named need) — or omitted, and the derivation decides (§9e rule 10d)`);
|
|
134
|
-
} else {
|
|
135
|
-
// A DELIVERY BAND.
|
|
136
|
-
for (const k of Object.keys(d))
|
|
137
|
-
if (!DELIVERY_KEYS.includes(k))
|
|
138
|
-
report("error", file, `a delivery band has no "${k}". It reads ${DELIVERY_KEYS.map((x) => `"${x}"`).join(", ")} (§9b; a place that names a component is a search place)`);
|
|
139
|
-
if (d.pick !== undefined && (typeof d.pick !== "number" || d.pick < 1))
|
|
140
|
-
report("error", file, `director.pick is a NUMBER — how many the director keeps, a cap the machine enforces (§9e rule 9)`);
|
|
141
|
-
// NOTHING READS `rules`: the director's model was removed (UNOVERSE_SCREEN.md §2.7).
|
|
142
|
-
if (d.rules !== undefined)
|
|
143
|
-
report("warn", file, `director.rules is ignored: no model reads it any more. A band holds what arrives in search rank, up to pick. Remove it`);
|
|
144
|
-
if (d.preview !== undefined && (!Array.isArray(d.preview) || d.preview.some((c) => typeof c !== "string")))
|
|
145
|
-
report("error", file, `director.preview is an array of component names — Studio's stand-ins until a real run exists (§9b rule 6)`);
|
|
146
|
-
else if (Array.isArray(d.preview)) {
|
|
147
|
-
const comps = componentNamesForFile(file);
|
|
148
|
-
for (const c of d.preview)
|
|
149
|
-
if (comps && !comps.has(String(c).toLowerCase()))
|
|
150
|
-
report("error", file, `director.preview names unknown component "${c}". No match in the design system or this org's components (lookup is case-insensitive)`);
|
|
151
|
-
}
|
|
152
|
-
// A PLACE NEEDS NO NAME (§9d): the compiler addresses delivery bands by
|
|
153
|
-
// position; an AUTHORED state: still stands (the flat-email pin), so two bands
|
|
154
|
-
// pinned to the SAME face remains the one genuine ambiguity.
|
|
155
|
-
const seen = directorsSeen.get(file) ?? [];
|
|
156
|
-
const face = typeof d?.state === "string" ? d.state : null;
|
|
157
|
-
if (face && seen.includes(face))
|
|
158
|
-
report("error", file, `two directed bands claim the face "${face}". A face has exactly one surface, or the active one is ambiguous (§9b rule 3, the reaction contract)`);
|
|
159
|
-
directorsSeen.set(file, [...seen, face]);
|
|
160
|
-
}
|
|
161
|
-
} else if (typeof node[word] !== "string") {
|
|
162
|
-
report("error", file, `"${word}:" links a component by PATH (${word}: components/<part>) — that is what makes it a known part (§9b rule 1)`);
|
|
163
|
-
} else {
|
|
164
|
-
const path = node[word];
|
|
165
|
-
if (!defPath(root, path) && !existsSync(join(root, path)))
|
|
166
|
-
report("error", file, `${word} "${path}" does not resolve under ${relative(process.cwd(), root)}/ (§9b rule 1)`);
|
|
167
|
-
}
|
|
168
|
-
// Everything else on the line rides through to the slot it compiles into. The select
|
|
169
|
-
// is SYNTHESIZED here exactly as the compiler builds it (a linked part claims its own
|
|
170
|
-
// face, limit 1; the director's claim is its `state:` pin) so the rest of the walk
|
|
171
|
-
// sees what will actually be served — and an authored one is the ceremony §9b rule 9
|
|
172
|
-
// exists to abolish.
|
|
50
|
+
// RETIRED: the section words. A stale layout fails here with the fix in the message.
|
|
51
|
+
const retired = RETIRED_WORDS.find((w) => node[w] !== undefined);
|
|
52
|
+
if (retired) {
|
|
53
|
+
report("error", file, `"${retired}:" is retired. Place the part with type: Ref and ref: components/<name>, and mark each of its props input: true (an Agent fills it) or input: false (drawn from its default) (UNOVERSE_AUTHORING "input decides")`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// A PLACED PART (UNOVERSE_TEMPLATE_MODEL §9b): in a template's layout, a `Ref` into the
|
|
58
|
+
// template's own `components/` folder. It compiles to a ComponentSlot at serve time,
|
|
59
|
+
// claiming the part's own face (its basename) at limit 1, so the linter checks the part
|
|
60
|
+
// resolves and hands the rest of the node to the ordinary walk as the slot it becomes.
|
|
61
|
+
if (node.type === "Ref" && typeof node.ref === "string" && node.ref.startsWith(PART_PREFIX) && isTemplatePath(file) && !isTemplatePart(file)) {
|
|
62
|
+
const path = node.ref;
|
|
63
|
+
if (!defPath(root, path) && !existsSync(join(root, path)))
|
|
64
|
+
report("error", file, `part "${path}" does not resolve under ${relative(process.cwd(), root)}/. A layout places the template's own parts: components/<name> (§9b)`);
|
|
65
|
+
if (node.props !== undefined || node.with !== undefined)
|
|
66
|
+
report("error", file, `a placed part takes no "props" or "with": its props are its own, filled by an Agent where marked input: true and drawn from their defaults where input: false (§9b)`);
|
|
67
|
+
// An authored select is the ceremony §9b rule 9 abolishes: the claim is synthesized.
|
|
173
68
|
if (node.select !== undefined)
|
|
174
|
-
report("error", file, `a
|
|
175
|
-
const {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// comes from whichever state in the envelope declares this layout.
|
|
179
|
-
const owningState = () => {
|
|
180
|
-
const env = defPath(root, basename(root));
|
|
181
|
-
const states = env ? readDef(env)?.states : undefined;
|
|
182
|
-
if (!states || typeof states !== "object") return undefined;
|
|
183
|
-
const here = defName(basename(file));
|
|
184
|
-
return Object.keys(states).find((s) => {
|
|
185
|
-
const l = states[s]?.layout;
|
|
186
|
-
return typeof l === "string" && basename(l).toLowerCase() === here.toLowerCase();
|
|
187
|
-
});
|
|
188
|
-
};
|
|
189
|
-
// A QUERIED place links its component under `component:`, and claims that part's own
|
|
190
|
-
// face at limit 1 exactly as a linked part does — its `limit:` counts ROWS inside the
|
|
191
|
-
// part, not instances of it, so it never reaches the slot (§9e).
|
|
192
|
-
const linkedPath = word === "query" ? node.query?.component : node[word];
|
|
193
|
-
const face =
|
|
194
|
-
word === "director" ? (node.director?.state ?? owningState()) : typeof linkedPath === "string" ? linkedPath.split("/").pop() : undefined;
|
|
195
|
-
const limit = word === "director" ? node.director?.limit : 1;
|
|
196
|
-
const select = { from: "all", ...(face ? { where: { field: "state", eq: face } } : {}), ...(typeof limit === "number" ? { limit } : {}) };
|
|
197
|
-
return walkNode({ type: "ComponentSlot", select, ...rest }, file, root, widthCap);
|
|
69
|
+
report("error", file, `a placed part never authors "select". It claims its own face at serve time (§9b rule 9)`);
|
|
70
|
+
const { type: _t, ref: _r, props: _p, with: _w, select: _sel, ...rest } = node;
|
|
71
|
+
const face = path.split("/").pop();
|
|
72
|
+
return walkNode({ type: "ComponentSlot", select: { from: "all", where: { field: "state", eq: face }, limit: 1 }, ...rest }, file, root, widthCap);
|
|
198
73
|
}
|
|
199
74
|
|
|
200
75
|
const t = node.type;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* calls execute, so {{signal.x}} and {{prompt.y}} an author wrote in a config field are
|
|
6
6
|
* plain text by the time they reach here.
|
|
7
7
|
*
|
|
8
|
-
* `executionContext.auth.accessToken` — the CALLER'S JWT (
|
|
8
|
+
* `executionContext.auth.accessToken` — the CALLER'S JWT (docs/auth-security/IDENTITY.md) — is
|
|
9
9
|
* DELIBERATELY WITHHELD, and must stay that way. A code node may reach it because a code
|
|
10
10
|
* node is bounded by provenance; a manifest is data that can arrive by paste or database
|
|
11
11
|
* row. The blast radii are not comparable: a third-party API key reaches that vendor
|
|
@@ -56,7 +56,7 @@ export function contextFor(node: ComposedNode, inputs: any, config: any, executi
|
|
|
56
56
|
//
|
|
57
57
|
// `conversationId` and `chatId` are here because the AUDIO LANE is keyed by conversation.
|
|
58
58
|
// Their absence was a silent, total failure of the voice node: `runDuplexSession` fell back
|
|
59
|
-
// to the node TYPE as its key, so
|
|
59
|
+
// to the node TYPE as its key, so the audio lane compared the client's real session id
|
|
60
60
|
// against the string "OpenAIRealtimeVoice", dropped every microphone frame, and sent the
|
|
61
61
|
// model's audio to a conversation nobody was listening on. No error anywhere — just silence.
|
|
62
62
|
//
|