@veil-runtime/ui 0.1.0 → 0.3.0

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.
@@ -0,0 +1,196 @@
1
+ export type InterfaceValue = string | number | boolean | null;
2
+ export interface InterfaceBinding {
3
+ source: "data" | "state";
4
+ path: string;
5
+ }
6
+ export interface InterfaceCondition {
7
+ binding: InterfaceBinding;
8
+ equals: InterfaceValue;
9
+ }
10
+ export interface InterfaceColumn {
11
+ key: string;
12
+ label: string;
13
+ }
14
+ export interface InterfaceNode {
15
+ key: string;
16
+ component: string;
17
+ children?: InterfaceNode[];
18
+ text?: string | InterfaceBinding;
19
+ binding?: InterfaceBinding;
20
+ visible?: InterfaceCondition;
21
+ action?: string;
22
+ label?: string;
23
+ inputType?: "text" | "number" | "checkbox";
24
+ columns?: InterfaceColumn[];
25
+ ariaLabel?: string;
26
+ }
27
+ export interface InterfaceDefinition {
28
+ id: string;
29
+ screen: InterfaceNode;
30
+ actions?: string[];
31
+ data?: Record<string, unknown>;
32
+ state?: Record<string, unknown>;
33
+ }
34
+ /** Use `satisfies BuiltinInterfaceDefinition` for compile-time checks on JSON-only built-in screens. */
35
+ type BuiltinBase = Pick<InterfaceNode, "key" | "visible" | "ariaLabel">;
36
+ export type BuiltinInterfaceNode = (BuiltinBase & {
37
+ component: "Stack" | "Row";
38
+ children?: BuiltinInterfaceNode[];
39
+ }) | (BuiltinBase & {
40
+ component: "Text" | "Status";
41
+ text?: string | InterfaceBinding;
42
+ }) | (BuiltinBase & {
43
+ component: "Heading";
44
+ text: string | InterfaceBinding;
45
+ }) | (BuiltinBase & {
46
+ component: "Input";
47
+ label: string;
48
+ binding: InterfaceBinding & {
49
+ source: "state";
50
+ };
51
+ inputType?: "text" | "number" | "checkbox";
52
+ }) | (BuiltinBase & {
53
+ component: "Button";
54
+ text: string | InterfaceBinding;
55
+ action: string;
56
+ }) | (BuiltinBase & {
57
+ component: "Table";
58
+ binding: InterfaceBinding;
59
+ columns: InterfaceColumn[];
60
+ }) | (BuiltinBase & {
61
+ component: "List";
62
+ binding: InterfaceBinding;
63
+ });
64
+ export type BuiltinInterfaceDefinition = Omit<InterfaceDefinition, "screen"> & {
65
+ screen: BuiltinInterfaceNode;
66
+ };
67
+ export interface InterfaceEvent {
68
+ action: string;
69
+ source: string;
70
+ state: Record<string, unknown>;
71
+ revision: number;
72
+ }
73
+ export interface InterfaceSnapshot {
74
+ definition: InterfaceDefinition;
75
+ data: Record<string, unknown>;
76
+ state: Record<string, unknown>;
77
+ revision: number;
78
+ }
79
+ export interface InterfaceController {
80
+ snapshot(): InterfaceSnapshot;
81
+ updateData(data: Record<string, unknown>, expectedRevision?: number): boolean;
82
+ updateDefinition(definition: InterfaceDefinition, expectedRevision?: number): boolean;
83
+ setState(path: string, value: InterfaceValue): void;
84
+ dispatch(action: string, source: string): void;
85
+ subscribe(listener: (snapshot: InterfaceSnapshot) => void): () => void;
86
+ dispose(): void;
87
+ }
88
+ /** JSON-safe description of the built-in vocabulary. Host extensions are trusted code and are never advertised here. */
89
+ export declare function getInterfaceCapabilities(): {
90
+ version: number;
91
+ components: {
92
+ readonly Stack: {
93
+ readonly properties: readonly ["key", "component", "visible", "ariaLabel", "children"];
94
+ readonly required: readonly [];
95
+ readonly children: true;
96
+ readonly event: null;
97
+ };
98
+ readonly Row: {
99
+ readonly properties: readonly ["key", "component", "visible", "ariaLabel", "children"];
100
+ readonly required: readonly [];
101
+ readonly children: true;
102
+ readonly event: null;
103
+ };
104
+ readonly Text: {
105
+ readonly properties: readonly ["key", "component", "visible", "ariaLabel", "text"];
106
+ readonly required: readonly [];
107
+ readonly children: false;
108
+ readonly event: null;
109
+ };
110
+ readonly Heading: {
111
+ readonly properties: readonly ["key", "component", "visible", "ariaLabel", "text"];
112
+ readonly required: readonly ["text"];
113
+ readonly children: false;
114
+ readonly event: null;
115
+ };
116
+ readonly Status: {
117
+ readonly properties: readonly ["key", "component", "visible", "ariaLabel", "text"];
118
+ readonly required: readonly [];
119
+ readonly children: false;
120
+ readonly event: null;
121
+ };
122
+ readonly Input: {
123
+ readonly properties: readonly ["key", "component", "visible", "ariaLabel", "label", "binding", "inputType"];
124
+ readonly required: readonly ["label", "binding"];
125
+ readonly children: false;
126
+ readonly event: "state change";
127
+ };
128
+ readonly Button: {
129
+ readonly properties: readonly ["key", "component", "visible", "ariaLabel", "text", "action"];
130
+ readonly required: readonly ["text", "action"];
131
+ readonly children: false;
132
+ readonly event: "action";
133
+ };
134
+ readonly Table: {
135
+ readonly properties: readonly ["key", "component", "visible", "ariaLabel", "binding", "columns"];
136
+ readonly required: readonly ["binding", "columns"];
137
+ readonly children: false;
138
+ readonly event: null;
139
+ };
140
+ readonly List: {
141
+ readonly properties: readonly ["key", "component", "visible", "ariaLabel", "binding"];
142
+ readonly required: readonly ["binding"];
143
+ readonly children: false;
144
+ readonly event: null;
145
+ };
146
+ };
147
+ bindings: {
148
+ sources: string[];
149
+ path: string;
150
+ missing: string;
151
+ visibility: string;
152
+ };
153
+ propertyTypes: {
154
+ key: string;
155
+ component: string;
156
+ children: string;
157
+ text: string;
158
+ binding: string;
159
+ visible: string;
160
+ action: string;
161
+ label: string;
162
+ inputType: string[];
163
+ columns: string;
164
+ ariaLabel: string;
165
+ };
166
+ events: {
167
+ action: {
168
+ fields: string[];
169
+ declaration: string;
170
+ };
171
+ state: {
172
+ type: string;
173
+ source: string;
174
+ };
175
+ };
176
+ limits: {
177
+ treeDepth: number;
178
+ nodes: number;
179
+ textLength: number;
180
+ tableRows: number;
181
+ tableColumns: number;
182
+ listItems: number;
183
+ transportBytes: number;
184
+ };
185
+ };
186
+ export declare function defineInterface<const T extends InterfaceDefinition>(definition: T): T;
187
+ /** Validate untrusted structured data before creating a controller or mounting it. */
188
+ export declare function validateInterfaceDefinition(value: unknown, components?: readonly string[]): asserts value is InterfaceDefinition;
189
+ export declare function createInterfaceController(definition: InterfaceDefinition, options?: {
190
+ components?: readonly string[];
191
+ allowedActions?: readonly string[];
192
+ onEvent?: (event: InterfaceEvent) => void | Promise<void>;
193
+ onError?: (error: unknown) => void;
194
+ }): InterfaceController;
195
+ export declare function resolveInterfaceBinding(snapshot: InterfaceSnapshot, bindingValue: InterfaceBinding): unknown;
196
+ export {};
@@ -0,0 +1,375 @@
1
+ import { assertBoundedData, assertSafeKey, copyData } from "./validation-safety.js";
2
+ import { attachInterfaceRenderers, detachInterfaceRenderers } from "./interface-transaction.js";
3
+ const identifier = /^[A-Za-z][A-Za-z0-9._-]{0,127}$/;
4
+ const commonProperties = ["key", "component", "visible", "ariaLabel"];
5
+ const vocabulary = {
6
+ Stack: { properties: [...commonProperties, "children"], required: [], children: true, event: null },
7
+ Row: { properties: [...commonProperties, "children"], required: [], children: true, event: null },
8
+ Text: { properties: [...commonProperties, "text"], required: [], children: false, event: null },
9
+ Heading: { properties: [...commonProperties, "text"], required: ["text"], children: false, event: null },
10
+ Status: { properties: [...commonProperties, "text"], required: [], children: false, event: null },
11
+ Input: { properties: [...commonProperties, "label", "binding", "inputType"], required: ["label", "binding"], children: false, event: "state change" },
12
+ Button: { properties: [...commonProperties, "text", "action"], required: ["text", "action"], children: false, event: "action" },
13
+ Table: { properties: [...commonProperties, "binding", "columns"], required: ["binding", "columns"], children: false, event: null },
14
+ List: { properties: [...commonProperties, "binding"], required: ["binding"], children: false, event: null },
15
+ };
16
+ const builtins = new Set(Object.keys(vocabulary));
17
+ const nodeKeys = new Set(["key", "component", "children", "text", "binding", "visible", "action", "label", "inputType", "columns", "ariaLabel"]);
18
+ /** JSON-safe description of the built-in vocabulary. Host extensions are trusted code and are never advertised here. */
19
+ export function getInterfaceCapabilities() {
20
+ return copyData({ version: 1, components: vocabulary, bindings: { sources: ["data", "state"], path: "dot-separated own-property path", missing: "empty display", visibility: "strict scalar equality" },
21
+ propertyTypes: { key: "identifier", component: "component name", children: "node array", text: "string or binding", binding: "binding", visible: "{binding,equals:scalar}", action: "declared action identifier", label: "string", inputType: ["text", "number", "checkbox"], columns: "array of {key:identifier,label:string}", ariaLabel: "string" },
22
+ events: { action: { fields: ["action", "source", "state", "revision"], declaration: "definition.actions and host allowedActions" }, state: { type: "scalar", source: "state binding" } },
23
+ limits: { treeDepth: 16, nodes: 512, textLength: 4096, tableRows: 100, tableColumns: 20, listItems: 100, transportBytes: 1048576 } });
24
+ }
25
+ const plain = (value) => !!value && typeof value === "object" && !Array.isArray(value);
26
+ const exact = (value, allowed) => {
27
+ for (const key of Object.keys(value))
28
+ if (!allowed.has(key))
29
+ throw new Error(`Unknown interface property: ${key}`);
30
+ };
31
+ const name = (value) => typeof value === "string" && identifier.test(value);
32
+ const scalar = (value) => value === null || typeof value === "string" || typeof value === "boolean" || (typeof value === "number" && Number.isFinite(value));
33
+ function binding(value) {
34
+ if (!plain(value))
35
+ throw new Error("Invalid binding");
36
+ exact(value, new Set(["source", "path"]));
37
+ if ((value.source !== "data" && value.source !== "state") || !name(value.path))
38
+ throw new Error("Invalid binding");
39
+ for (const part of value.path.split("."))
40
+ assertSafeKey(part);
41
+ }
42
+ function valueAt(source, path) {
43
+ let value = source;
44
+ for (const part of path.split("."))
45
+ value = plain(value) && Object.hasOwn(value, part) ? value[part] : undefined;
46
+ return value;
47
+ }
48
+ function visitNodes(node, callback) {
49
+ callback(node);
50
+ for (const child of node.children ?? [])
51
+ visitNodes(child, callback);
52
+ }
53
+ function validateBindings(definition, data, state) {
54
+ visitNodes(definition.screen, (node) => {
55
+ if (node.component === "Table") {
56
+ const rows = valueAt(node.binding.source === "data" ? data : state, node.binding.path);
57
+ if (rows === undefined)
58
+ return;
59
+ if (!Array.isArray(rows) || rows.length > 100 || rows.some((row) => !plain(row)))
60
+ throw new Error("Invalid or oversized table rows");
61
+ for (const row of rows)
62
+ for (const col of node.columns ?? [])
63
+ if (Object.hasOwn(row, col.key) && !scalar(row[col.key]))
64
+ throw new Error("Invalid table cell");
65
+ }
66
+ if (node.component === "List") {
67
+ const items = valueAt(node.binding.source === "data" ? data : state, node.binding.path);
68
+ if (items !== undefined && (!Array.isArray(items) || items.length > 100 || items.some((item) => !scalar(item))))
69
+ throw new Error("Invalid or oversized list items");
70
+ }
71
+ if (node.component === "Input") {
72
+ const current = valueAt(state, node.binding.path);
73
+ if (current !== undefined && !scalar(current))
74
+ throw new Error("Invalid input state");
75
+ }
76
+ });
77
+ }
78
+ /** A definition owns its state shape. Retain values only for unchanged input identities. */
79
+ function retainedState(previous, next, oldState) {
80
+ const state = copyData(next.state ?? {});
81
+ if (previous.id !== next.id)
82
+ return state;
83
+ const inputs = new Map();
84
+ visitNodes(previous.screen, (node) => { if (node.component === "Input")
85
+ inputs.set(node.key, node); });
86
+ visitNodes(next.screen, (node) => {
87
+ if (node.component !== "Input")
88
+ return;
89
+ const old = inputs.get(node.key);
90
+ if (!old || old.inputType !== node.inputType || old.binding?.path !== node.binding?.path)
91
+ return;
92
+ const path = node.binding.path;
93
+ const oldValue = valueAt(oldState, path);
94
+ const defaultValue = valueAt(state, path);
95
+ if (!scalar(oldValue) || defaultValue === undefined || typeof oldValue !== typeof defaultValue)
96
+ return;
97
+ const parts = path.split(".");
98
+ let target = state;
99
+ for (const part of parts.slice(0, -1)) {
100
+ if (!plain(target[part]))
101
+ return;
102
+ target = target[part];
103
+ }
104
+ target[parts.at(-1)] = oldValue;
105
+ });
106
+ return state;
107
+ }
108
+ export function defineInterface(definition) { return definition; }
109
+ /** Validate untrusted structured data before creating a controller or mounting it. */
110
+ export function validateInterfaceDefinition(value, components = []) {
111
+ assertBoundedData(value);
112
+ if (!plain(value))
113
+ throw new Error("Invalid interface definition");
114
+ exact(value, new Set(["id", "screen", "actions", "data", "state"]));
115
+ if (!name(value.id) || !plain(value.screen))
116
+ throw new Error("Invalid interface identity or screen");
117
+ for (const key of ["data", "state"])
118
+ if (value[key] !== undefined && !plain(value[key]))
119
+ throw new Error(`Invalid ${key}`);
120
+ if (value.actions !== undefined && (!Array.isArray(value.actions) || value.actions.some((action) => !name(action)) || new Set(value.actions).size !== value.actions.length))
121
+ throw new Error("Invalid actions");
122
+ const actions = (value.actions ?? []);
123
+ if (components.some((component) => !name(component) || builtins.has(component)) || new Set(components).size !== components.length)
124
+ throw new Error("Invalid component registry");
125
+ const known = new Set([...builtins, ...components]);
126
+ const keys = new Set();
127
+ let count = 0;
128
+ function visit(node, depth) {
129
+ if (!plain(node) || depth > 16 || ++count > 512)
130
+ throw new Error("Interface tree limit exceeded");
131
+ exact(node, nodeKeys);
132
+ if (!name(node.key) || keys.has(node.key))
133
+ throw new Error("Invalid or duplicate node key");
134
+ keys.add(node.key);
135
+ if (typeof node.component !== "string" || !known.has(node.component))
136
+ throw new Error(`Unknown component: ${String(node.component)}`);
137
+ if (builtins.has(node.component)) {
138
+ const spec = vocabulary[node.component];
139
+ for (const property of Object.keys(node))
140
+ if (!spec.properties.includes(property))
141
+ throw new Error(`Unsupported ${node.component} property: ${property}`);
142
+ for (const property of spec.required)
143
+ if (node[property] === undefined)
144
+ throw new Error(`${node.component} needs ${property}`);
145
+ }
146
+ if (node.text !== undefined) {
147
+ if (typeof node.text === "string") {
148
+ if (node.text.length > 4096)
149
+ throw new Error("Text limit exceeded");
150
+ }
151
+ else
152
+ binding(node.text);
153
+ }
154
+ if (node.binding !== undefined)
155
+ binding(node.binding);
156
+ if (node.visible !== undefined) {
157
+ if (!plain(node.visible))
158
+ throw new Error("Invalid visibility condition");
159
+ exact(node.visible, new Set(["binding", "equals"]));
160
+ binding(node.visible.binding);
161
+ if (!scalar(node.visible.equals))
162
+ throw new Error("Invalid visibility condition");
163
+ }
164
+ for (const field of ["label", "ariaLabel"])
165
+ if (node[field] !== undefined && (typeof node[field] !== "string" || node[field].length > 256))
166
+ throw new Error(`Invalid ${field}`);
167
+ if (node.action !== undefined && (!name(node.action) || !actions.includes(node.action)))
168
+ throw new Error("Action is not declared by the host");
169
+ if (node.inputType !== undefined && !["text", "number", "checkbox"].includes(node.inputType))
170
+ throw new Error("Invalid input type");
171
+ if (node.columns !== undefined) {
172
+ if (!Array.isArray(node.columns) || node.columns.length < 1 || node.columns.length > 20 || node.columns.some((col) => !plain(col) || Object.keys(col).some((key) => key !== "key" && key !== "label") || !name(col.key) || typeof col.label !== "string" || col.label.length > 256))
173
+ throw new Error("Invalid table columns");
174
+ }
175
+ if (node.component === "Input" && (!node.binding || node.binding.source !== "state" || !node.label))
176
+ throw new Error("Input needs a state binding and label");
177
+ if (node.component === "Button" && (!node.action || !node.text))
178
+ throw new Error("Button needs an action and text");
179
+ if (node.component === "Table" && (!node.binding || !node.columns))
180
+ throw new Error("Table needs rows and columns");
181
+ if (node.component !== "Button" && node.action !== undefined)
182
+ throw new Error("Only Button accepts an action");
183
+ if (node.component !== "Input" && node.inputType !== undefined)
184
+ throw new Error("Only Input accepts inputType");
185
+ if (node.component !== "Table" && node.columns !== undefined)
186
+ throw new Error("Only Table accepts columns");
187
+ if (node.children !== undefined && (!Array.isArray(node.children) || (!(["Stack", "Row"].includes(node.component)) && !components.includes(node.component))))
188
+ throw new Error("Invalid children");
189
+ for (const child of node.children ?? [])
190
+ visit(child, depth + 1);
191
+ }
192
+ visit(value.screen, 0);
193
+ validateBindings(value, (value.data ?? {}), (value.state ?? {}));
194
+ }
195
+ export function createInterfaceController(definition, options = {}) {
196
+ const components = options.components ?? [];
197
+ validateInterfaceDefinition(definition, components);
198
+ if (options.allowedActions && definition.actions?.some((action) => !options.allowedActions.includes(action)))
199
+ throw new Error("Action is not allowed by host");
200
+ let current = copyData(definition);
201
+ let data = copyData(definition.data ?? {});
202
+ let state = copyData(definition.state ?? {});
203
+ validateBindings(current, data, state);
204
+ let revision = 0;
205
+ let disposed = false;
206
+ let dispatching = false;
207
+ let emitting = false;
208
+ let pending = false;
209
+ let updating = false;
210
+ const listeners = new Set();
211
+ const participants = new Set();
212
+ const copySnapshot = (value) => ({
213
+ definition: copyData(value.definition), data: copyData(value.data), state: copyData(value.state), revision: value.revision,
214
+ });
215
+ const snapshot = () => copySnapshot({ definition: current, data, state, revision });
216
+ const report = (error) => { try {
217
+ options.onError?.(error);
218
+ }
219
+ catch { /* Error reporting must not enter the interface runtime. */ } };
220
+ const emit = () => {
221
+ if (emitting) {
222
+ pending = true;
223
+ return;
224
+ }
225
+ emitting = true;
226
+ try {
227
+ let passes = 0;
228
+ do {
229
+ pending = false;
230
+ const accepted = snapshot();
231
+ for (const listener of listeners) {
232
+ try {
233
+ listener(copySnapshot(accepted));
234
+ }
235
+ catch (error) {
236
+ report(error);
237
+ }
238
+ if (revision !== accepted.revision) {
239
+ pending = true;
240
+ break;
241
+ }
242
+ }
243
+ if (++passes >= 32 && pending) {
244
+ report(new Error("Interface update loop limit exceeded"));
245
+ pending = false;
246
+ break;
247
+ }
248
+ } while (pending && !disposed);
249
+ }
250
+ finally {
251
+ emitting = false;
252
+ }
253
+ };
254
+ const accept = (nextDefinition, nextData, nextState) => {
255
+ if (updating)
256
+ throw new Error("Interface update already in progress");
257
+ updating = true;
258
+ const prepared = [];
259
+ try {
260
+ const candidate = { definition: nextDefinition, data: nextData, state: nextState, revision: revision + 1 };
261
+ for (const renderer of participants)
262
+ prepared.push(renderer(copySnapshot(candidate)));
263
+ if (disposed)
264
+ throw new Error("Interface controller was disposed during update");
265
+ for (const renderer of prepared)
266
+ renderer.commit();
267
+ current = nextDefinition;
268
+ data = nextData;
269
+ state = nextState;
270
+ revision++;
271
+ }
272
+ catch (error) {
273
+ for (const renderer of prepared.reverse()) {
274
+ try {
275
+ renderer.discard();
276
+ }
277
+ catch (cleanupError) {
278
+ report(cleanupError);
279
+ }
280
+ }
281
+ throw error;
282
+ }
283
+ finally {
284
+ updating = false;
285
+ }
286
+ emit();
287
+ };
288
+ const controller = {
289
+ snapshot,
290
+ updateData(next, expectedRevision) { if (disposed || (expectedRevision !== undefined && expectedRevision !== revision))
291
+ return false; if (updating)
292
+ throw new Error("Interface update already in progress"); assertBoundedData(next); if (!plain(next))
293
+ throw new Error("Invalid data"); const copy = copyData(next); validateBindings(current, copy, state); accept(current, copy, state); return true; },
294
+ updateDefinition(next, expectedRevision) {
295
+ if (disposed || (expectedRevision !== undefined && expectedRevision !== revision))
296
+ return false;
297
+ if (updating)
298
+ throw new Error("Interface update already in progress");
299
+ validateInterfaceDefinition(next, components);
300
+ if (options.allowedActions && next.actions?.some((action) => !options.allowedActions.includes(action)))
301
+ throw new Error("Action is not allowed by host");
302
+ const copy = copyData(next);
303
+ const nextData = copyData(copy.data ?? {});
304
+ const nextState = retainedState(current, copy, state);
305
+ assertBoundedData(nextState);
306
+ validateBindings(copy, nextData, nextState);
307
+ accept(copy, nextData, nextState);
308
+ return true;
309
+ },
310
+ setState(path, value) {
311
+ if (disposed)
312
+ return;
313
+ if (updating)
314
+ throw new Error("Interface update already in progress");
315
+ if (!name(path) || !scalar(value))
316
+ throw new Error("Invalid state update");
317
+ const parts = path.split(".");
318
+ for (const part of parts)
319
+ assertSafeKey(part);
320
+ const next = copyData(state);
321
+ let target = next;
322
+ for (const part of parts.slice(0, -1)) {
323
+ const child = target[part];
324
+ if (child !== undefined && !plain(child))
325
+ throw new Error("State path is not an object");
326
+ if (child === undefined)
327
+ target[part] = {};
328
+ target = target[part];
329
+ }
330
+ target[parts.at(-1)] = value;
331
+ assertBoundedData(next);
332
+ validateBindings(current, data, next);
333
+ accept(current, data, next);
334
+ },
335
+ dispatch(action, source) {
336
+ if (disposed || dispatching)
337
+ return;
338
+ if (updating)
339
+ throw new Error("Interface update already in progress");
340
+ if (!current.actions?.includes(action) || !findAction(current.screen, source, action, data, state))
341
+ throw new Error("Action is not available");
342
+ dispatching = true;
343
+ try {
344
+ const result = options.onEvent?.({ action, source, state: copyData(state), revision });
345
+ if (result && typeof result === "object" && "then" in result)
346
+ void Promise.resolve(result).catch(report);
347
+ }
348
+ catch (error) {
349
+ report(error);
350
+ }
351
+ finally {
352
+ dispatching = false;
353
+ }
354
+ },
355
+ subscribe(listener) { if (disposed)
356
+ throw new Error("Interface controller is disposed"); listeners.add(listener); try {
357
+ listener(snapshot());
358
+ }
359
+ catch (error) {
360
+ listeners.delete(listener);
361
+ throw error;
362
+ } return () => { listeners.delete(listener); }; },
363
+ dispose() { disposed = true; listeners.clear(); participants.clear(); detachInterfaceRenderers(controller); },
364
+ };
365
+ attachInterfaceRenderers(controller, participants);
366
+ return controller;
367
+ }
368
+ function findAction(node, key, action, data, state) {
369
+ if (node.visible && valueAt(node.visible.binding.source === "data" ? data : state, node.visible.binding.path) !== node.visible.equals)
370
+ return false;
371
+ return (node.key === key && node.action === action) || (node.children?.some((child) => findAction(child, key, action, data, state)) ?? false);
372
+ }
373
+ export function resolveInterfaceBinding(snapshot, bindingValue) {
374
+ return valueAt(bindingValue.source === "data" ? snapshot.data : snapshot.state, bindingValue.path);
375
+ }
@@ -13,8 +13,8 @@ export declare const presentationPlanSchema: z.ZodObject<{
13
13
  subtle: "subtle";
14
14
  }>;
15
15
  density: z.ZodEnum<{
16
- compact: "compact";
17
16
  comfortable: "comfortable";
17
+ compact: "compact";
18
18
  }>;
19
19
  sections: z.ZodArray<z.ZodString>;
20
20
  }, z.core.$strict>>;
@@ -1,6 +1,8 @@
1
1
  import { assertBoundedData, copyData } from "./validation-safety.js";
2
2
  import { z } from "zod";
3
- const groupSchema = z.object({ id: z.string().regex(/^[a-zA-Z0-9_-]+$/).max(48), title: z.string().min(1).max(80).optional(), description: z.string().min(1).max(240).optional(), emphasis: z.enum(["normal", "prominent", "subtle"]), density: z.enum(["comfortable", "compact"]), sections: z.array(z.string().min(1).max(120)).min(1).max(256) }).strict();
3
+ const emphasisSchema = z.enum(["normal", "prominent", "subtle"]);
4
+ const densitySchema = z.enum(["comfortable", "compact"]);
5
+ const groupSchema = z.object({ id: z.string().regex(/^[a-zA-Z0-9_-]+$/).max(48), title: z.string().min(1).max(80).optional(), description: z.string().min(1).max(240).optional(), emphasis: emphasisSchema, density: densitySchema, sections: z.array(z.string().min(1).max(120)).min(1).max(256) }).strict();
4
6
  export const presentationPlanSchema = z.object({ title: z.string().min(1).max(100).optional(), description: z.string().min(1).max(240).optional(), groups: z.array(groupSchema).max(12) }).strict();
5
7
  export function sectionId(kind, capability) { return kind + ":" + capability; }
6
8
  export function buildPresentationEnvelope(primary, related) { return { applicationSurface: primary.capability, sections: [{ id: sectionId("primary", primary.capability), capability: primary.capability, surface: primary.view.surface, label: primary.view.title, required: true, movable: false, groupable: false, defaultOrder: 0 }, ...related.map((section, index) => ({ id: sectionId("related", section.capability), capability: section.capability, surface: section.view?.surface ?? "error", label: section.label, required: true, movable: true, groupable: true, defaultOrder: index + 1 }))], immutable: ["capabilities", "fields", "metrics", "bindings", "workflow", "risk", "actions", "surface semantics", "execution"] }; }
package/docs/API.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Public API inventory
2
2
 
3
- The preview keeps all existing root exports; no existing symbol or subpath was removed. The root index exports modules below. All interfaces/types exported by those modules remain available in the declarations. Package consumers import through the root or `/inspection`, not paths inside dist.
3
+ The v0.3.0-rc.2 candidate adds `@veil-runtime/ui/interface` (`defineInterface`, `validateInterfaceDefinition`, `createInterfaceController`, `getInterfaceCapabilities`, `resolveInterfaceBinding`, and types including `BuiltinInterfaceDefinition`) and `mountInterface`, `createMountedInterface`, plus `InterfaceDOMComponentInstance` in `/dom`. `InterfaceDOMComponent` may still return an `HTMLElement`; it may now return `{ element, update?, dispose? }`. Updates prepare detached custom instances before controller acceptance. See [UNIVERSAL-INTERFACE.md](UNIVERSAL-INTERFACE.md). The v0.2.0 composition API remains available through `/composition` and `/dom`. The inventory below describes preserved v0.1.0 APIs.
4
+
5
+ The preview keeps all existing root exports; no existing symbol or subpath was removed. The root index exports modules below. All interfaces/types exported by those modules remain available in the declarations. Package consumers import through the root or documented `/inspection`, `/composition`, `/interface`, and `/dom` subpaths, not paths inside dist.
4
6
 
5
7
  | Area | Root modules / representative API |
6
8
  |---|---|
@@ -14,4 +16,4 @@ The preview keeps all existing root exports; no existing symbol or subpath was r
14
16
  | Experimental optional presentation | `capability-view-derivation` provider interfaces and `deriveCapabilityView`; `presentation-composition`, `buildPresentationEnvelope`, `deterministicPresentation`, `validatePresentationPlan`, `composePresentation`, `presentationPlanSchema`, `sectionId` |
15
17
  | Inspection subpath | `@veil-runtime/ui/inspection`: `buildCompositionInspection` and inspection model types |
16
18
 
17
- The qualification consumer exercises both supported subpaths, actual Core capability creation, strict public declarations, and browser bundling. Published export conditions intentionally support ESM import, not direct CommonJS require. Core is a nonoptional peer for the TypeScript capability contract; UI has no compiled Core runtime import. A host may use UI's descriptors in a browser and keep Core execution on its server.
19
+ Qualification exercises the legacy Core capability API and the independent universal interface subpaths, strict public declarations, and browser bundling. Published export conditions intentionally support ESM import, not direct CommonJS require. Core is an optional peer and is unnecessary for `/interface` and `/dom` consumers. UI has no compiled Core runtime import. A host may use UI's descriptors in a browser and keep Core execution on its server.