@veil-runtime/ui 0.0.0-stage → 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.
Files changed (81) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +6 -0
  4. package/README.md +100 -2
  5. package/SECURITY.md +11 -0
  6. package/dist/application.d.ts +56 -0
  7. package/dist/application.js +9 -0
  8. package/dist/build-manifest.json +95 -0
  9. package/dist/capability-view-derivation.d.ts +40 -0
  10. package/dist/capability-view-derivation.js +185 -0
  11. package/dist/capability-view.d.ts +170 -0
  12. package/dist/capability-view.js +1 -0
  13. package/dist/composed-surface.d.ts +67 -0
  14. package/dist/composed-surface.js +1 -0
  15. package/dist/composition-inspection.d.ts +51 -0
  16. package/dist/composition-inspection.js +102 -0
  17. package/dist/composition.d.ts +43 -0
  18. package/dist/composition.js +227 -0
  19. package/dist/derive-input-fields.d.ts +4 -0
  20. package/dist/derive-input-fields.js +63 -0
  21. package/dist/derive-output-view.d.ts +26 -0
  22. package/dist/derive-output-view.js +97 -0
  23. package/dist/dom.d.ts +31 -0
  24. package/dist/dom.js +554 -0
  25. package/dist/execution-request.d.ts +13 -0
  26. package/dist/execution-request.js +58 -0
  27. package/dist/experience-orchestration.d.ts +13 -0
  28. package/dist/experience-orchestration.js +31 -0
  29. package/dist/format-semantic-value.d.ts +4 -0
  30. package/dist/format-semantic-value.js +15 -0
  31. package/dist/index.d.ts +20 -0
  32. package/dist/index.js +20 -0
  33. package/dist/input-schema.d.ts +25 -0
  34. package/dist/input-schema.js +131 -0
  35. package/dist/interaction-resolution.d.ts +27 -0
  36. package/dist/interaction-resolution.js +179 -0
  37. package/dist/interface-transaction.d.ts +9 -0
  38. package/dist/interface-transaction.js +14 -0
  39. package/dist/interface.d.ts +196 -0
  40. package/dist/interface.js +375 -0
  41. package/dist/output-presentation-suggestion-schema.d.ts +12 -0
  42. package/dist/output-presentation-suggestion-schema.js +8 -0
  43. package/dist/output-schema.d.ts +26 -0
  44. package/dist/output-schema.js +213 -0
  45. package/dist/presentation-composition.d.ts +29 -0
  46. package/dist/presentation-composition.js +25 -0
  47. package/dist/presentation-context.d.ts +6 -0
  48. package/dist/presentation-context.js +1 -0
  49. package/dist/presentation-suggestion-schema.d.ts +15 -0
  50. package/dist/presentation-suggestion-schema.js +10 -0
  51. package/dist/registry.d.ts +21 -0
  52. package/dist/registry.js +71 -0
  53. package/dist/relationship-interpretation.d.ts +9 -0
  54. package/dist/relationship-interpretation.js +16 -0
  55. package/dist/validate-application.d.ts +24 -0
  56. package/dist/validate-application.js +253 -0
  57. package/dist/validation-safety.d.ts +6 -0
  58. package/dist/validation-safety.js +90 -0
  59. package/dist/workflow.d.ts +18 -0
  60. package/dist/workflow.js +3 -0
  61. package/docs/API.md +19 -0
  62. package/docs/COMPOSITION.md +67 -0
  63. package/docs/DECISIONS.md +16 -0
  64. package/docs/RELEASE-0.3.0-rc.1.md +25 -0
  65. package/docs/RELEASE-0.3.0-rc.2.md +9 -0
  66. package/docs/RELEASE-0.3.0.md +7 -0
  67. package/docs/THIRD_PARTY.md +16 -0
  68. package/docs/UNIVERSAL-INTERFACE.md +105 -0
  69. package/docs/third-party/ZOD-LICENSE.txt +21 -0
  70. package/examples/composed/app.mjs +44 -0
  71. package/examples/composed/build.mjs +7 -0
  72. package/examples/composed/index.html +34 -0
  73. package/examples/standalone/index.mjs +78 -0
  74. package/examples/standalone/package.json +8 -0
  75. package/examples/universal/agent.mjs +52 -0
  76. package/examples/universal/application.mjs +16 -0
  77. package/examples/universal/build.mjs +6 -0
  78. package/examples/universal/human.mjs +16 -0
  79. package/examples/universal/index.html +4 -0
  80. package/examples/universal/main.mjs +7 -0
  81. package/package.json +90 -4
@@ -0,0 +1,15 @@
1
+ export function formatSemanticValue(value, field) {
2
+ if (field?.semantic === "currency" && typeof value === "number" && field.currency) {
3
+ return new Intl.NumberFormat(undefined, {
4
+ style: "currency",
5
+ currency: field.currency,
6
+ }).format(value);
7
+ }
8
+ if (field?.semantic === "count" && typeof value === "number") {
9
+ return new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }).format(value);
10
+ }
11
+ if (field?.semantic === "ratio" && typeof value === "number") {
12
+ return new Intl.NumberFormat(undefined, { style: "percent", maximumFractionDigits: 1 }).format(value);
13
+ }
14
+ return value;
15
+ }
@@ -0,0 +1,20 @@
1
+ export * from "./input-schema.js";
2
+ export * from "./output-schema.js";
3
+ export * from "./application.js";
4
+ export * from "./capability-view.js";
5
+ export * from "./composed-surface.js";
6
+ export * from "./workflow.js";
7
+ export * from "./relationship-interpretation.js";
8
+ export * from "./validate-application.js";
9
+ export * from "./registry.js";
10
+ export * from "./derive-input-fields.js";
11
+ export * from "./derive-output-view.js";
12
+ export * from "./capability-view-derivation.js";
13
+ export * from "./interaction-resolution.js";
14
+ export * from "./experience-orchestration.js";
15
+ export * from "./presentation-context.js";
16
+ export * from "./presentation-composition.js";
17
+ export * from "./execution-request.js";
18
+ export * from "./format-semantic-value.js";
19
+ export * from "./composition.js";
20
+ export * from "./interface.js";
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ export * from "./input-schema.js";
2
+ export * from "./output-schema.js";
3
+ export * from "./application.js";
4
+ export * from "./capability-view.js";
5
+ export * from "./composed-surface.js";
6
+ export * from "./workflow.js";
7
+ export * from "./relationship-interpretation.js";
8
+ export * from "./validate-application.js";
9
+ export * from "./registry.js";
10
+ export * from "./derive-input-fields.js";
11
+ export * from "./derive-output-view.js";
12
+ export * from "./capability-view-derivation.js";
13
+ export * from "./interaction-resolution.js";
14
+ export * from "./experience-orchestration.js";
15
+ export * from "./presentation-context.js";
16
+ export * from "./presentation-composition.js";
17
+ export * from "./execution-request.js";
18
+ export * from "./format-semantic-value.js";
19
+ export * from "./composition.js";
20
+ export * from "./interface.js";
@@ -0,0 +1,25 @@
1
+ export type RichStringFormat = "date" | "date-time" | "multiline";
2
+ /** The shared field shape is structural so composition needs no Core installation. */
3
+ export interface RichCapabilityInputField {
4
+ type: "string" | "number" | "integer" | "boolean" | "object" | "array";
5
+ required: boolean;
6
+ description: string;
7
+ enum?: Array<string | number | boolean>;
8
+ format?: RichStringFormat;
9
+ semantic?: "currency";
10
+ currency?: string;
11
+ fields?: RichCapabilityInputSchema;
12
+ items?: RichCapabilityInputField;
13
+ minLength?: number;
14
+ maxLength?: number;
15
+ minimum?: number;
16
+ maximum?: number;
17
+ minItems?: number;
18
+ maxItems?: number;
19
+ }
20
+ export type RichCapabilityInputSchema = Record<string, RichCapabilityInputField>;
21
+ export declare function defineInputSchema<T extends RichCapabilityInputSchema>(schema: T): T;
22
+ export declare function validateRichInputField(field: RichCapabilityInputField, path: string): void;
23
+ export declare function validateRichInputSchema(schema: RichCapabilityInputSchema): void;
24
+ export declare function validateRichInputValue(value: unknown, field: RichCapabilityInputField, path: string): void;
25
+ export declare function validateRichCapabilityInput(schema: RichCapabilityInputSchema, input: unknown): asserts input is Record<string, unknown>;
@@ -0,0 +1,131 @@
1
+ import { assertBoundedData, isCalendarDate, isUTCInstant } from "./validation-safety.js";
2
+ export function defineInputSchema(schema) {
3
+ return schema;
4
+ }
5
+ function assertConstraintPair(minimum, maximum, path) {
6
+ if (minimum !== undefined && maximum !== undefined && minimum > maximum) {
7
+ throw new Error(`Invalid input schema at "${path}": minimum exceeds maximum`);
8
+ }
9
+ }
10
+ export function validateRichInputField(field, path) {
11
+ assertBoundedData(field);
12
+ const supported = ["string", "number", "integer", "boolean", "object", "array"];
13
+ if (!supported.includes(field.type)) {
14
+ throw new Error(`Unsupported input type "${field.type}" at "${path}"`);
15
+ }
16
+ if (field.enum) {
17
+ if (field.enum.length === 0)
18
+ throw new Error(`Empty enum at "${path}"`);
19
+ const primitive = field.type === "integer" ? "number" : field.type;
20
+ if (field.enum.some((value) => typeof value !== primitive)) {
21
+ throw new Error(`Enum value is incompatible with "${field.type}" at "${path}"`);
22
+ }
23
+ }
24
+ if (field.format && field.type !== "string") {
25
+ throw new Error(`Format requires a string field at "${path}"`);
26
+ }
27
+ if (field.semantic === "currency") {
28
+ if (!["number", "integer"].includes(field.type) || !field.currency) {
29
+ throw new Error(`Currency semantics require a numeric type and currency code at "${path}"`);
30
+ }
31
+ }
32
+ assertConstraintPair(field.minLength, field.maxLength, path);
33
+ assertConstraintPair(field.minimum, field.maximum, path);
34
+ assertConstraintPair(field.minItems, field.maxItems, path);
35
+ if (field.type === "object") {
36
+ if (!field.fields)
37
+ throw new Error(`Object field lacks nested fields at "${path}"`);
38
+ for (const [name, nested] of Object.entries(field.fields)) {
39
+ validateRichInputField(nested, `${path}.${name}`);
40
+ }
41
+ }
42
+ if (field.type === "array") {
43
+ if (!field.items)
44
+ throw new Error(`Array field lacks an item schema at "${path}"`);
45
+ validateRichInputField(field.items, `${path}[]`);
46
+ }
47
+ }
48
+ export function validateRichInputSchema(schema) {
49
+ assertBoundedData(schema);
50
+ for (const [name, field] of Object.entries(schema)) {
51
+ validateRichInputField(field, name);
52
+ }
53
+ }
54
+ function missing(value) {
55
+ return value === undefined || value === null || value === "";
56
+ }
57
+ export function validateRichInputValue(value, field, path) {
58
+ assertBoundedData(value);
59
+ assertBoundedData(field);
60
+ if (missing(value)) {
61
+ if (field.required)
62
+ throw new Error(`Required input omitted: ${path}`);
63
+ return;
64
+ }
65
+ if (field.enum && !field.enum.includes(value))
66
+ throw new Error(`Input "${path}" is not an allowed enum value`);
67
+ if (field.type === "string") {
68
+ if (typeof value !== "string")
69
+ throw new Error(`Input "${path}" must be a string`);
70
+ if (field.minLength !== undefined && value.length < field.minLength)
71
+ throw new Error(`Input "${path}" is shorter than minLength`);
72
+ if (field.maxLength !== undefined && value.length > field.maxLength)
73
+ throw new Error(`Input "${path}" exceeds maxLength`);
74
+ if (field.format === "date" && !isCalendarDate(value))
75
+ throw new Error(`Input "${path}" must use YYYY-MM-DD`);
76
+ if (field.format === "date-time" && !isUTCInstant(value))
77
+ throw new Error(`Input "${path}" must be an ISO-8601 UTC instant`);
78
+ return;
79
+ }
80
+ if (field.type === "number" || field.type === "integer") {
81
+ if (typeof value !== "number" || !Number.isFinite(value))
82
+ throw new Error(`Input "${path}" must be numeric`);
83
+ if (field.type === "integer" && !Number.isInteger(value))
84
+ throw new Error(`Input "${path}" must be an integer`);
85
+ if (field.minimum !== undefined && value < field.minimum)
86
+ throw new Error(`Input "${path}" is below minimum`);
87
+ if (field.maximum !== undefined && value > field.maximum)
88
+ throw new Error(`Input "${path}" exceeds maximum`);
89
+ return;
90
+ }
91
+ if (field.type === "boolean") {
92
+ if (typeof value !== "boolean")
93
+ throw new Error(`Input "${path}" must be boolean`);
94
+ return;
95
+ }
96
+ if (field.type === "object") {
97
+ if (!value || typeof value !== "object" || Array.isArray(value))
98
+ throw new Error(`Input "${path}" must be an object`);
99
+ const record = value;
100
+ const fields = field.fields ?? {};
101
+ for (const key of Object.keys(record))
102
+ if (!Object.hasOwn(fields, key))
103
+ throw new Error(`Unexpected nested field: ${path}.${key}`);
104
+ for (const [key, child] of Object.entries(fields))
105
+ validateRichInputValue(Object.hasOwn(record, key) ? record[key] : undefined, child, `${path}.${key}`);
106
+ return;
107
+ }
108
+ if (field.type === "array") {
109
+ if (!Array.isArray(value))
110
+ throw new Error(`Input "${path}" must be an array`);
111
+ if (field.minItems !== undefined && value.length < field.minItems)
112
+ throw new Error(`Input "${path}" is below minItems`);
113
+ if (field.maxItems !== undefined && value.length > field.maxItems)
114
+ throw new Error(`Input "${path}" exceeds maxItems`);
115
+ if (!field.items)
116
+ throw new Error(`Input "${path}" lacks an item schema`);
117
+ value.forEach((item, index) => validateRichInputValue(item, field.items, `${path}[${index}]`));
118
+ }
119
+ }
120
+ export function validateRichCapabilityInput(schema, input) {
121
+ assertBoundedData(schema);
122
+ assertBoundedData(input);
123
+ if (!input || typeof input !== "object" || Array.isArray(input))
124
+ throw new Error("Capability input must be an object");
125
+ const record = input;
126
+ for (const key of Object.keys(record))
127
+ if (!Object.hasOwn(schema, key))
128
+ throw new Error(`Unexpected supplied input: ${key}`);
129
+ for (const [key, field] of Object.entries(schema))
130
+ validateRichInputValue(Object.hasOwn(record, key) ? record[key] : undefined, field, key);
131
+ }
@@ -0,0 +1,27 @@
1
+ import type { CapabilityView, UIField } from "./capability-view.js";
2
+ export interface AuthoritativeInputBinding {
3
+ sourceField: string;
4
+ value: unknown;
5
+ }
6
+ export type AuthoritativeInputBindings = Record<string, AuthoritativeInputBinding>;
7
+ export type CapabilityInteractionMode = "form" | "action" | "confirmation" | "read";
8
+ export interface ResolvedCapabilityInput {
9
+ name: string;
10
+ source: "human" | "binding";
11
+ value?: unknown;
12
+ sourceField?: string;
13
+ field: UIField;
14
+ }
15
+ export interface CapabilityInteractionResolution {
16
+ mode: CapabilityInteractionMode;
17
+ suppliedInputs: Record<string, unknown>;
18
+ boundInputNames: string[];
19
+ humanFields: UIField[];
20
+ inputs: ResolvedCapabilityInput[];
21
+ unresolvedRequiredInputs: string[];
22
+ reason: string;
23
+ }
24
+ export declare function serializeDateTimeLocal(value: string): string;
25
+ export declare function validateFieldValue(value: unknown, field: UIField, path: string): void;
26
+ export declare function resolveCapabilityInteraction(view: CapabilityView, bindings?: AuthoritativeInputBindings): CapabilityInteractionResolution;
27
+ export declare function buildCapabilityExecutionPayload(resolution: CapabilityInteractionResolution, humanValues: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,179 @@
1
+ import { assertBoundedData, isCalendarDate, isUTCInstant } from "./validation-safety.js";
2
+ function valueMatchesField(value, field) {
3
+ try {
4
+ validateFieldValue(value, field, field.name);
5
+ return true;
6
+ }
7
+ catch {
8
+ return false;
9
+ }
10
+ }
11
+ export function serializeDateTimeLocal(value) {
12
+ return new Date(value).toISOString();
13
+ }
14
+ export function validateFieldValue(value, field, path) {
15
+ assertBoundedData(value);
16
+ assertBoundedData(field);
17
+ if (value === undefined || value === null || value === "") {
18
+ if (field.required)
19
+ throw new Error(`Required input omitted: ${path}`);
20
+ return;
21
+ }
22
+ if (field.options && !field.options.includes(value)) {
23
+ throw new Error(`Input "${path}" is not an allowed enum value`);
24
+ }
25
+ if (field.authoritativeType === "string") {
26
+ if (typeof value !== "string")
27
+ throw new Error(`Input "${path}" must be a string`);
28
+ if (field.minLength !== undefined && value.length < field.minLength)
29
+ throw new Error(`Input "${path}" is shorter than minLength`);
30
+ if (field.maxLength !== undefined && value.length > field.maxLength)
31
+ throw new Error(`Input "${path}" exceeds maxLength`);
32
+ if (field.format === "date" && !isCalendarDate(value))
33
+ throw new Error(`Input "${path}" must use YYYY-MM-DD`);
34
+ if (field.format === "date-time" && !isUTCInstant(value)) {
35
+ throw new Error(`Input "${path}" must be an ISO-8601 UTC instant`);
36
+ }
37
+ return;
38
+ }
39
+ if (field.authoritativeType === "number" || field.authoritativeType === "integer") {
40
+ if (typeof value !== "number" || !Number.isFinite(value))
41
+ throw new Error(`Input "${path}" must be numeric`);
42
+ if (field.authoritativeType === "integer" && !Number.isInteger(value))
43
+ throw new Error(`Input "${path}" must be an integer`);
44
+ if (field.minimum !== undefined && value < field.minimum)
45
+ throw new Error(`Input "${path}" is below minimum`);
46
+ if (field.maximum !== undefined && value > field.maximum)
47
+ throw new Error(`Input "${path}" exceeds maximum`);
48
+ return;
49
+ }
50
+ if (field.authoritativeType === "boolean") {
51
+ if (typeof value !== "boolean")
52
+ throw new Error(`Input "${path}" must be boolean`);
53
+ return;
54
+ }
55
+ if (field.authoritativeType === "object") {
56
+ if (!value || typeof value !== "object" || Array.isArray(value))
57
+ throw new Error(`Input "${path}" must be an object`);
58
+ const record = value;
59
+ const children = field.fields ?? [];
60
+ const names = new Set(children.map((child) => child.name));
61
+ for (const key of Object.keys(record))
62
+ if (!names.has(key))
63
+ throw new Error(`Unexpected nested field: ${path}.${key}`);
64
+ for (const child of children)
65
+ validateFieldValue(Object.hasOwn(record, child.name) ? record[child.name] : undefined, child, `${path}.${child.name}`);
66
+ return;
67
+ }
68
+ if (field.authoritativeType === "array") {
69
+ if (!Array.isArray(value))
70
+ throw new Error(`Input "${path}" must be an array`);
71
+ if (field.minItems !== undefined && value.length < field.minItems)
72
+ throw new Error(`Input "${path}" is below minItems`);
73
+ if (field.maxItems !== undefined && value.length > field.maxItems)
74
+ throw new Error(`Input "${path}" exceeds maxItems`);
75
+ if (!field.item)
76
+ throw new Error(`Input "${path}" lacks an item schema`);
77
+ value.forEach((item, index) => validateFieldValue(item, field.item, `${path}[${index}]`));
78
+ }
79
+ }
80
+ export function resolveCapabilityInteraction(view, bindings = {}) {
81
+ assertBoundedData(view);
82
+ assertBoundedData(bindings);
83
+ const fieldsByName = new Map(view.fields.map((field) => [field.name, field]));
84
+ const suppliedInputs = {};
85
+ const boundInputNames = Object.keys(bindings);
86
+ const inputs = [];
87
+ for (const [name, binding] of Object.entries(bindings)) {
88
+ const field = fieldsByName.get(name);
89
+ if (!field) {
90
+ throw new Error(`Authoritative binding references unknown input: ${name}`);
91
+ }
92
+ if (binding.value === undefined || binding.value === null || binding.value === "") {
93
+ if (field.required) {
94
+ throw new Error(`Required bound input "${name}" has no runtime value`);
95
+ }
96
+ inputs.push({
97
+ name, source: "binding", value: binding.value,
98
+ sourceField: binding.sourceField, field,
99
+ });
100
+ continue;
101
+ }
102
+ if (!valueMatchesField(binding.value, field)) {
103
+ throw new Error(`Bound input "${name}" does not match declared type "${field.type}"`);
104
+ }
105
+ suppliedInputs[name] = binding.value;
106
+ inputs.push({
107
+ name,
108
+ source: "binding",
109
+ value: binding.value,
110
+ sourceField: binding.sourceField,
111
+ field,
112
+ });
113
+ }
114
+ const boundNames = new Set(boundInputNames);
115
+ const humanFields = view.fields.filter((field) => !boundNames.has(field.name));
116
+ for (const field of humanFields) {
117
+ inputs.push({ name: field.name, source: "human", field });
118
+ }
119
+ const unresolvedRequiredInputs = humanFields
120
+ .filter((field) => field.required)
121
+ .map((field) => field.name);
122
+ const mode = humanFields.length > 0
123
+ ? "form"
124
+ : view.risk === "destructive"
125
+ ? "confirmation"
126
+ : view.risk === "read"
127
+ ? "read"
128
+ : "action";
129
+ if (mode === "confirmation" && !view.confirmation?.required) {
130
+ throw new Error("Destructive zero-input interaction is missing confirmation metadata");
131
+ }
132
+ const reason = humanFields.length > 0
133
+ ? `${humanFields.length} input${humanFields.length === 1 ? "" : "s"} remain for the human`
134
+ : mode === "confirmation"
135
+ ? 'all inputs are supplied and authoritative risk is "destructive"'
136
+ : mode === "read"
137
+ ? "all inputs are supplied and the capability is read-only"
138
+ : "all inputs are supplied and the capability is a write";
139
+ return {
140
+ mode,
141
+ suppliedInputs,
142
+ boundInputNames,
143
+ humanFields,
144
+ inputs,
145
+ unresolvedRequiredInputs,
146
+ reason,
147
+ };
148
+ }
149
+ export function buildCapabilityExecutionPayload(resolution, humanValues) {
150
+ assertBoundedData(resolution);
151
+ assertBoundedData(humanValues);
152
+ const humanNames = new Set(resolution.humanFields.map((field) => field.name));
153
+ const boundNames = new Set(resolution.boundInputNames);
154
+ for (const name of Object.keys(humanValues)) {
155
+ if (boundNames.has(name)) {
156
+ throw new Error(`Human input cannot overwrite authoritative bound input: ${name}`);
157
+ }
158
+ if (!humanNames.has(name)) {
159
+ throw new Error(`Unexpected supplied input: ${name}`);
160
+ }
161
+ }
162
+ const payload = {};
163
+ for (const field of resolution.humanFields) {
164
+ const value = Object.hasOwn(humanValues, field.name) ? humanValues[field.name] : undefined;
165
+ if (field.required && (value === undefined || value === null || value === "")) {
166
+ throw new Error(`Required human input omitted: ${field.name}`);
167
+ }
168
+ if (value !== undefined && value !== "") {
169
+ validateFieldValue(value, field, field.name);
170
+ payload[field.name] = value;
171
+ }
172
+ }
173
+ for (const name of resolution.unresolvedRequiredInputs) {
174
+ if (!Object.prototype.hasOwnProperty.call(payload, name)) {
175
+ throw new Error(`Resolved execution payload is missing required input: ${name}`);
176
+ }
177
+ }
178
+ return payload;
179
+ }
@@ -0,0 +1,9 @@
1
+ /** Internal bridge between a neutral controller and any mounted renderers. */
2
+ export interface PreparedInterfaceRender {
3
+ commit(): void;
4
+ discard(): void;
5
+ }
6
+ export type InterfaceRenderParticipant<T> = (candidate: T) => PreparedInterfaceRender;
7
+ export declare function attachInterfaceRenderers<T>(controller: object, participants: Set<InterfaceRenderParticipant<T>>): void;
8
+ export declare function detachInterfaceRenderers(controller: object): void;
9
+ export declare function registerInterfaceRenderer<T>(controller: object, renderer: InterfaceRenderParticipant<T>): () => void;
@@ -0,0 +1,14 @@
1
+ const renderers = new WeakMap();
2
+ export function attachInterfaceRenderers(controller, participants) {
3
+ renderers.set(controller, participants);
4
+ }
5
+ export function detachInterfaceRenderers(controller) {
6
+ renderers.delete(controller);
7
+ }
8
+ export function registerInterfaceRenderer(controller, renderer) {
9
+ const participants = renderers.get(controller);
10
+ if (!participants)
11
+ throw new Error("Interface controller is disposed or unsupported");
12
+ participants.add(renderer);
13
+ return () => { participants.delete(renderer); };
14
+ }
@@ -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 {};