diff --git a/renderers/web_core/package-lock.json b/renderers/web_core/package-lock.json index 0d8881461..c6b77cf10 100644 --- a/renderers/web_core/package-lock.json +++ b/renderers/web_core/package-lock.json @@ -10,13 +10,13 @@ "license": "Apache-2.0", "dependencies": { "@preact/signals-core": "^1.13.0", - "zod": "^3.25.76" + "zod": "^3.25.76", + "zod-to-json-schema": "^3.25.1" }, "devDependencies": { "@types/node": "^24.11.0", "typescript": "^5.8.3", - "wireit": "^0.15.0-pre.2", - "zod-to-json-schema": "^3.25.1" + "wireit": "^0.15.0-pre.2" } }, "node_modules/@nodelib/fs.scandir": { @@ -471,7 +471,6 @@ "node_modules/zod-to-json-schema": { "version": "3.25.1", "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", - "dev": true, "license": "ISC", "peerDependencies": { "zod": "^3.25 || ^4" diff --git a/renderers/web_core/package.json b/renderers/web_core/package.json index 5b0e275d5..5babd86fe 100644 --- a/renderers/web_core/package.json +++ b/renderers/web_core/package.json @@ -88,11 +88,11 @@ "devDependencies": { "@types/node": "^24.11.0", "typescript": "^5.8.3", - "wireit": "^0.15.0-pre.2", - "zod-to-json-schema": "^3.25.1" + "wireit": "^0.15.0-pre.2" }, "dependencies": { "@preact/signals-core": "^1.13.0", - "zod": "^3.25.76" + "zod": "^3.25.76", + "zod-to-json-schema": "^3.25.1" } } diff --git a/renderers/web_core/src/v0_9/catalog/types.ts b/renderers/web_core/src/v0_9/catalog/types.ts index 2e79fdaab..7248d9aad 100644 --- a/renderers/web_core/src/v0_9/catalog/types.ts +++ b/renderers/web_core/src/v0_9/catalog/types.ts @@ -113,15 +113,25 @@ export class Catalog { */ readonly functions: ReadonlyMap; + /** + * The schema for theme parameters used by this catalog. + */ + readonly themeSchema?: z.ZodObject; + /** * A ready-to-use FunctionInvoker callback that delegates to this catalog's functions. * Can be passed directly to a DataContext. */ readonly invoker: FunctionInvoker; - constructor(id: string, components: T[], functions: FunctionImplementation[] = []) { + constructor( + id: string, + components: T[], + functions: FunctionImplementation[] = [], + themeSchema?: z.ZodObject, + ) { this.id = id; - + const compMap = new Map(); for (const comp of components) { compMap.set(comp.name, comp); @@ -134,6 +144,8 @@ export class Catalog { } this.functions = funcMap; + this.themeSchema = themeSchema; + this.invoker = (name, rawArgs, ctx, abortSignal) => { const fn = this.functions.get(name); if (!fn) { diff --git a/renderers/web_core/src/v0_9/processing/message-processor.test.ts b/renderers/web_core/src/v0_9/processing/message-processor.test.ts index a4c4b0ad1..9cffde829 100644 --- a/renderers/web_core/src/v0_9/processing/message-processor.test.ts +++ b/renderers/web_core/src/v0_9/processing/message-processor.test.ts @@ -18,6 +18,7 @@ import assert from "node:assert"; import { describe, it, beforeEach } from "node:test"; import { MessageProcessor } from "./message-processor.js"; import { Catalog, ComponentApi } from "../catalog/types.js"; +import { z } from "zod"; describe("MessageProcessor", () => { let processor: MessageProcessor; @@ -32,6 +33,196 @@ describe("MessageProcessor", () => { }); }); + describe("getClientCapabilities", () => { + it("generates basic client capabilities with supportedCatalogIds", () => { + const caps: any = processor.getClientCapabilities(); + assert.strictEqual((caps["v0.9"] as any).inlineCatalogs, undefined); + assert.deepStrictEqual(caps, { + "v0.9": { + supportedCatalogIds: ["test-catalog"], + }, + }); + }); + + it("generates inline catalogs when requested", () => { + const buttonApi: ComponentApi = { + name: "Button", + schema: z.object({ + label: z.string().describe("The button label"), + }), + }; + const cat = new Catalog("cat-1", [buttonApi]); + const proc = new MessageProcessor([cat]); + + const caps = proc.getClientCapabilities({ includeInlineCatalogs: true }); + const inlineCat = caps["v0.9"].inlineCatalogs![0]; + + assert.strictEqual(inlineCat.catalogId, "cat-1"); + const buttonSchema = inlineCat.components!.Button; + + assert.ok(buttonSchema.allOf); + assert.strictEqual( + buttonSchema.allOf[0].$ref, + "common_types.json#/$defs/ComponentCommon", + ); + assert.strictEqual(buttonSchema.allOf[1].properties.component.const, "Button"); + assert.strictEqual( + buttonSchema.allOf[1].properties.label.description, + "The button label", + ); + assert.deepStrictEqual(buttonSchema.allOf[1].required, ["component", "label"]); + }); + + it("transforms REF: descriptions into valid $ref nodes", () => { + const customApi: ComponentApi = { + name: "Custom", + schema: z.object({ + title: z + .string() + .describe("REF:common_types.json#/$defs/DynamicString|The title"), + }), + }; + const cat = new Catalog("cat-ref", [customApi]); + const proc = new MessageProcessor([cat]); + + const caps = proc.getClientCapabilities({ includeInlineCatalogs: true }); + const titleSchema = + caps["v0.9"].inlineCatalogs![0].components!.Custom.allOf[1].properties.title; + + assert.strictEqual(titleSchema.$ref, "common_types.json#/$defs/DynamicString"); + assert.strictEqual(titleSchema.description, "The title"); + // Ensure Zod's 'type: string' was removed + assert.strictEqual(titleSchema.type, undefined); + }); + + it("generates inline catalogs with functions and theme schema", () => { + const buttonApi: ComponentApi = { + name: "Button", + schema: z.object({ + label: z.string(), + }), + }; + const addFn = { + name: "add", + returnType: "number" as const, + schema: z.object({ + a: z.number().describe("First number"), + b: z.number().describe("Second number"), + }), + execute: (args: any) => args.a + args.b, + }; + + const themeSchema = z.object({ + primaryColor: z.string().describe("REF:common_types.json#/$defs/Color|The main color"), + }); + + const cat = new Catalog("cat-full", [buttonApi], [addFn], themeSchema); + const proc = new MessageProcessor([cat]); + + const caps = proc.getClientCapabilities({ includeInlineCatalogs: true }); + const inlineCat = caps["v0.9"].inlineCatalogs![0]; + + assert.strictEqual(inlineCat.catalogId, "cat-full"); + + // Verify Functions + assert.ok(inlineCat.functions); + assert.strictEqual(inlineCat.functions.length, 1); + const fn = inlineCat.functions[0]; + assert.strictEqual(fn.name, "add"); + assert.strictEqual(fn.returnType, "number"); + assert.strictEqual(fn.parameters.properties.a.description, "First number"); + + // Verify Theme + assert.ok(inlineCat.theme); + assert.ok(inlineCat.theme.primaryColor); + assert.strictEqual(inlineCat.theme.primaryColor.$ref, "common_types.json#/$defs/Color"); + assert.strictEqual(inlineCat.theme.primaryColor.description, "The main color"); + }); + + it("omits functions and theme when catalog has none", () => { + const compApi: ComponentApi = { name: "EmptyComp", schema: z.object({}) }; + const cat = new Catalog("cat-empty", [compApi]); + const proc = new MessageProcessor([cat]); + const caps = proc.getClientCapabilities({ includeInlineCatalogs: true }); + const inlineCat = caps["v0.9"].inlineCatalogs![0]; + + assert.strictEqual(inlineCat.catalogId, "cat-empty"); + assert.strictEqual(inlineCat.functions, undefined); + assert.strictEqual(inlineCat.theme, undefined); + }); + + it("processes REF: tags deeply nested in schema arrays and objects", () => { + const deepApi: ComponentApi = { + name: "DeepComp", + schema: z.object({ + items: z.array(z.object({ + action: z.string().describe("REF:common_types.json#/$defs/Action|The action to perform") + })) + }) + }; + const cat = new Catalog("cat-deep", [deepApi]); + const proc = new MessageProcessor([cat]); + const caps = proc.getClientCapabilities({ includeInlineCatalogs: true }); + + const properties = caps["v0.9"].inlineCatalogs![0].components!.DeepComp.allOf[1].properties; + const actionSchema = properties.items.items.properties.action; + + assert.strictEqual(actionSchema.$ref, "common_types.json#/$defs/Action"); + assert.strictEqual(actionSchema.description, "The action to perform"); + assert.strictEqual(actionSchema.type, undefined); + }); + + it("handles REF: tags without pipes or with multiple pipes", () => { + const edgeApi: ComponentApi = { + name: "EdgeComp", + schema: z.object({ + noPipe: z.string().describe("REF:common_types.json#/$defs/NoPipe"), + multiPipe: z.string().describe("REF:common_types.json#/$defs/MultiPipe|First|Second"), + }) + }; + const cat = new Catalog("cat-edge", [edgeApi]); + const proc = new MessageProcessor([cat]); + const caps = proc.getClientCapabilities({ includeInlineCatalogs: true }); + + const properties = caps["v0.9"].inlineCatalogs![0].components!.EdgeComp.allOf[1].properties; + + assert.strictEqual(properties.noPipe.$ref, "common_types.json#/$defs/NoPipe"); + assert.strictEqual(properties.noPipe.description, undefined); + + assert.strictEqual(properties.multiPipe.$ref, "common_types.json#/$defs/MultiPipe"); + assert.strictEqual(properties.multiPipe.description, "First"); + }); + + it("handles multiple catalogs correctly", () => { + const compApi: ComponentApi = { name: "C1", schema: z.object({}) }; + const cat1 = new Catalog("cat-1", [compApi]); + + const addFn = { + name: "add", + returnType: "number" as const, + schema: z.object({}), + execute: () => 0, + }; + const themeSchema = z.object({ color: z.string() }); + const cat2 = new Catalog("cat-2", [], [addFn], themeSchema); + + const proc = new MessageProcessor([cat1, cat2]); + const caps = proc.getClientCapabilities({ includeInlineCatalogs: true }); + + assert.strictEqual(caps["v0.9"].inlineCatalogs!.length, 2); + + const inlineCat1 = caps["v0.9"].inlineCatalogs![0]; + assert.strictEqual(inlineCat1.catalogId, "cat-1"); + assert.strictEqual(inlineCat1.functions, undefined); + assert.strictEqual(inlineCat1.theme, undefined); + + const inlineCat2 = caps["v0.9"].inlineCatalogs![1]; + assert.strictEqual(inlineCat2.catalogId, "cat-2"); + assert.strictEqual(inlineCat2.functions!.length, 1); + assert.ok(inlineCat2.theme); + }); + }); + it("creates surface", () => { processor.processMessages([ { diff --git a/renderers/web_core/src/v0_9/processing/message-processor.ts b/renderers/web_core/src/v0_9/processing/message-processor.ts index df8c95c52..880a4376d 100644 --- a/renderers/web_core/src/v0_9/processing/message-processor.ts +++ b/renderers/web_core/src/v0_9/processing/message-processor.ts @@ -19,6 +19,7 @@ import { Catalog, ComponentApi } from "../catalog/types.js"; import { SurfaceGroupModel } from "../state/surface-group-model.js"; import { ComponentModel } from "../state/component-model.js"; import { Subscription } from "../common/events.js"; +import { zodToJsonSchema } from "zod-to-json-schema"; import { A2uiMessage, @@ -27,8 +28,20 @@ import { UpdateDataModelMessage, DeleteSurfaceMessage, } from "../schema/server-to-client.js"; +import { + A2uiClientCapabilities, + InlineCatalog, +} from "../schema/client-capabilities.js"; import { A2uiStateError, A2uiValidationError } from "../errors.js"; +/** + * Options for generating client capabilities. + */ +export interface CapabilitiesOptions { + /** If true, the full definition of all catalogs will be included. */ + includeInlineCatalogs?: boolean; +} + /** * The central processor for A2UI messages. * @template T The concrete type of the ComponentApi. @@ -52,6 +65,122 @@ export class MessageProcessor { } } + /** + * Generates the a2uiClientCapabilities object for the current processor. + * + * @param options Configuration for capability generation. + * @returns The capabilities object. + */ + getClientCapabilities(options?: CapabilitiesOptions): A2uiClientCapabilities { + const capabilities: A2uiClientCapabilities = { + "v0.9": { + supportedCatalogIds: this.catalogs.map((c) => c.id), + }, + }; + + if (options?.includeInlineCatalogs) { + capabilities["v0.9"].inlineCatalogs = this.catalogs.map((c) => + this.generateInlineCatalog(c), + ); + } + + return capabilities; + } + + private generateInlineCatalog(catalog: Catalog): InlineCatalog { + const components: Record = {}; + + for (const [name, api] of catalog.components.entries()) { + const zodSchema = zodToJsonSchema(api.schema, { + target: "jsonSchema2019-09", + }) as any; + + // Clean up Zod-specific artifacts and process REF: tags + this.processRefs(zodSchema); + + // Wrap in standard A2UI component envelope (ComponentCommon) + components[name] = { + allOf: [ + { $ref: "common_types.json#/$defs/ComponentCommon" }, + { + properties: { + component: { const: name }, + ...zodSchema.properties, + }, + required: ["component", ...(zodSchema.required || [])], + }, + ], + }; + } + + const functions: any[] = []; + for (const api of catalog.functions.values()) { + const zodSchema = zodToJsonSchema(api.schema, { + target: "jsonSchema2019-09", + }) as any; + + this.processRefs(zodSchema); + + functions.push({ + name: api.name, + description: api.schema.description, + returnType: api.returnType, + parameters: zodSchema, + }); + } + + let theme: Record | undefined; + if (catalog.themeSchema) { + const zodSchema = zodToJsonSchema(catalog.themeSchema, { + target: "jsonSchema2019-09", + }) as any; + + this.processRefs(zodSchema); + theme = zodSchema.properties; + } + + return { + catalogId: catalog.id, + components, + functions: functions.length > 0 ? functions : undefined, + theme, + }; + } + + private processRefs(node: any): void { + if (typeof node !== "object" || node === null) return; + + // If the node itself is a REF target, transform it and stop recursion. + if (typeof node.description === "string" && node.description.startsWith("REF:")) { + const parts = node.description.substring(4).split("|"); + const ref = parts[0]; + const desc = parts[1] || ""; + + // Clear the node of all other properties. + for (const k of Object.keys(node)) { + delete node[k]; + } + + // Re-add only the $ref and an optional description. + node["$ref"] = ref; + if (desc) { + node["description"] = desc; + } + return; + } + + // If not a REF target, recurse into its children. + if (Array.isArray(node)) { + for (const item of node) { + this.processRefs(item); + } + } else { + for (const key of Object.keys(node)) { + this.processRefs(node[key]); + } + } + } + /** * Subscribes to surface creation events. */ diff --git a/renderers/web_core/src/v0_9/schema/client-capabilities.ts b/renderers/web_core/src/v0_9/schema/client-capabilities.ts new file mode 100644 index 000000000..0bb3893a2 --- /dev/null +++ b/renderers/web_core/src/v0_9/schema/client-capabilities.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Represents a JSON Schema definition. + * Typed as Record to allow standard JSON schema properties + * without importing heavy schema types. + */ +export type JsonSchema = Record; + +/** + * Describes a function's interface within an inline catalog. + */ +export interface FunctionDefinition { + name: string; + description?: string; + parameters: JsonSchema; + returnType: 'string' | 'number' | 'boolean' | 'array' | 'object' | 'any' | 'void'; +} + +/** + * Defines a catalog inline for the a2uiClientCapabilities object. + */ +export interface InlineCatalog { + catalogId: string; + components?: Record; + functions?: FunctionDefinition[]; + theme?: Record; +} + +/** + * The capabilities structure sent from the client to the server as part of transport metadata. + */ +export interface A2uiClientCapabilities { + "v0.9": { + supportedCatalogIds: string[]; + inlineCatalogs?: InlineCatalog[]; + }; +} diff --git a/renderers/web_core/src/v0_9/schema/common-types.ts b/renderers/web_core/src/v0_9/schema/common-types.ts index 632330a5d..1a3ee1cf9 100644 --- a/renderers/web_core/src/v0_9/schema/common-types.ts +++ b/renderers/web_core/src/v0_9/schema/common-types.ts @@ -20,7 +20,7 @@ export const DataBindingSchema = z.object({ path: z .string() .describe("A JSON Pointer path to a value in the data model."), -}); +}).describe("REF:common_types.json#/$defs/DataBinding|A JSON Pointer path to a value in the data model."); export const FunctionCallSchema = z.object({ call: z.string().describe("The name of the function to call."), @@ -28,7 +28,7 @@ export const FunctionCallSchema = z.object({ returnType: z .enum(["string", "number", "boolean", "array", "object", "any", "void"]) .default("boolean"), -}); +}).describe("REF:common_types.json#/$defs/FunctionCall|Invokes a named function on the client."); export const LogicExpressionSchema: z.ZodType = z.lazy(() => z.union([ @@ -42,32 +42,32 @@ export const LogicExpressionSchema: z.ZodType = z.lazy(() => z.object({ true: z.literal(true) }), z.object({ false: z.literal(false) }), ]), -); +).describe("REF:common_types.json#/$defs/LogicExpression|A logical expression representation."); export const DynamicStringSchema = z.union([ z.string(), DataBindingSchema, // FunctionCall returning string (simplified schema for Zod, stricter in JSON Schema) FunctionCallSchema, -]); +]).describe("REF:common_types.json#/$defs/DynamicString|Represents a string"); export const DynamicNumberSchema = z.union([ z.number(), DataBindingSchema, FunctionCallSchema, -]); +]).describe("REF:common_types.json#/$defs/DynamicNumber|Represents a value that can be either a literal number, a path to a number in the data model, or a function call returning a number."); export const DynamicBooleanSchema = z.union([ z.boolean(), DataBindingSchema, LogicExpressionSchema, -]); +]).describe("REF:common_types.json#/$defs/DynamicBoolean|A boolean value that can be a literal, a path, or a function call returning a boolean."); export const DynamicStringListSchema = z.union([ z.array(z.string()), DataBindingSchema, FunctionCallSchema, -]); +]).describe("REF:common_types.json#/$defs/DynamicStringList|Represents a value that can be either a literal array of strings, a path to a string array in the data model, or a function call returning a string array."); export const DynamicValueSchema = z.union([ z.string(), @@ -76,7 +76,7 @@ export const DynamicValueSchema = z.union([ z.array(z.any()), DataBindingSchema, FunctionCallSchema, -]); +]).describe("REF:common_types.json#/$defs/DynamicValue|A value that can be a literal, a path, or a function call returning any type."); /** A JSON Pointer path to a value in the data model. */ export type DataBinding = z.infer; @@ -97,7 +97,7 @@ export type DynamicValue = z.infer; export const ComponentIdSchema = z .string() - .describe("The unique identifier for a component."); + .describe("REF:common_types.json#/$defs/ComponentId|The unique identifier for a component."); /** The unique identifier for a component. */ export type ComponentId = z.infer; @@ -113,7 +113,7 @@ export const ChildListSchema = z.union([ ), }) .describe("A template for generating a dynamic list of children."), -]); +]).describe("REF:common_types.json#/$defs/ChildList"); /** A static list of child component IDs or a dynamic list template. */ export type ChildList = z.infer; @@ -131,7 +131,7 @@ export const ActionSchema = z.union([ functionCall: FunctionCallSchema, }) .describe("Executes a local client-side function."), -]); +]).describe("REF:common_types.json#/$defs/Action"); /** Triggers a server-side event or a local client-side function. */ export type Action = z.infer; @@ -142,7 +142,7 @@ export const CheckRuleSchema = z.intersection( .string() .describe("The error message to display if the check fails."), }), -); +).describe("REF:common_types.json#/$defs/CheckRule|A check rule consisting of a condition and an error message."); /** A check rule consisting of a condition and an error message. */ export type CheckRule = z.infer; @@ -151,7 +151,7 @@ export const CheckableSchema = z.object({ .array(CheckRuleSchema) .optional() .describe("A list of checks to perform."), -}); +}).describe("REF:common_types.json#/$defs/Checkable|Properties for components that support client-side checks."); /** An object that contains checks. */ export type Checkable = z.infer; diff --git a/renderers/web_core/src/v0_9/schema/index.ts b/renderers/web_core/src/v0_9/schema/index.ts index d98dfd978..6e8434988 100644 --- a/renderers/web_core/src/v0_9/schema/index.ts +++ b/renderers/web_core/src/v0_9/schema/index.ts @@ -16,3 +16,4 @@ export * from "./common-types.js"; export * from "./server-to-client.js"; +export * from "./client-capabilities.js"; diff --git a/specification/v0_9/docs/renderer_guide.md b/specification/v0_9/docs/renderer_guide.md index 0eebda2ba..88a671d39 100644 --- a/specification/v0_9/docs/renderer_guide.md +++ b/specification/v0_9/docs/renderer_guide.md @@ -135,7 +135,22 @@ The model is designed to support high-performance rendering through granular upd * **Property Changes**: The `ComponentModel` notifies when its specific configuration changes. * **Data Changes**: The `DataModel` notifies only subscribers to the specific path that changed. -### The Models +### Protocol Models & Serialization + +The framework-agnostic layer is responsible for defining strict, native type representations of the A2UI JSON schemas. Renderers should not pass raw generic dictionaries (like `Map` or `Record`) directly into the state layer. + +Developers must create data classes, structs, or interfaces (e.g., `data class` in Kotlin, `Codable struct` in Swift, or Zod-validated `interface` in TypeScript) that perfectly mirror the official JSON specifications. This creates a safe boundary between the raw network stream and the internal state models. + +**Required Data Structures:** +* **Server-to-Client Messages:** `A2uiMessage` (a union/protocol type), `CreateSurfaceMessage`, `UpdateComponentsMessage`, `UpdateDataModelMessage`, `DeleteSurfaceMessage`. +* **Client-to-Server Events:** `ClientEvent` (a union/protocol type), `ActionMessage`, `ErrorMessage`. +* **Client Metadata:** `A2uiClientCapabilities`, `InlineCatalog`, `FunctionDefinition`, `ClientDataModel`. + +**JSON Serialization & Validation:** +* **Inbound (Parsing)**: The core library must provide a mechanism to deserialize a raw JSON string into a strongly-typed `A2uiMessage`. If the payload violates the A2UI JSON schema, this layer must throw an `A2uiValidationError` *before* the message reaches the state models. +* **Outbound (Stringifying)**: The core library must serialize client-to-server events and capabilities from their strict native types back into valid JSON strings to hand off to the transport layer. + +### The State Models #### SurfaceGroupModel & SurfaceModel The root containers for active surfaces and their catalogs, data, and components. @@ -267,9 +282,12 @@ class MessageProcessor { constructor(catalogs: Catalog[], actionHandler: ActionListener); - processMessages(messages: any[]): void; + // Accepts validated, strongly-typed message objects, not raw JSON + processMessages(messages: A2uiMessage[]): void; addLifecycleListener(l: SurfaceLifecycleListener): () => void; - getClientCapabilities(options?: CapabilitiesOptions): any; + + // Returns a strictly typed capabilities object ready for JSON serialization + getClientCapabilities(options?: CapabilitiesOptions): A2uiClientCapabilities; } ``` @@ -282,15 +300,15 @@ To dynamically generate the `a2uiClientCapabilities` payload (specifically `inli **Detectable Common Types**: Shared definitions (like `DynamicString`) must emit external JSON Schema `$ref` pointers. This is achieved by "tagging" the schemas using their `description` property (e.g., `REF:common_types.json#/$defs/DynamicString`). -When `getClientCapabilities()` converts internal schemas: -1. Translate the definition into a raw JSON Schema. -2. Traverse the tree looking for descriptions starting with `REF:`. -3. Strip the tag and replace the node with a valid JSON Schema `$ref` object. -4. Wrap property schemas in the standard A2UI component envelope (`allOf` containing `ComponentCommon`). +When `getClientCapabilities()` converts internal schemas to generate `inlineCatalogs`: +1. **Components**: Translate each component schema into a raw JSON Schema. Wrap it in the standard A2UI component envelope (`allOf` containing `ComponentCommon`). +2. **Functions**: Map each function in the catalog to a `FunctionDefinition` object, converting its argument schema to JSON Schema. +3. **Theme**: Convert the catalog's theme schema into a JSON Schema representation. +4. **Reference Processing**: For all generated schemas (components, functions, and themes), traverse the tree looking for descriptions starting with `REF:`. Strip the tag and replace the node with a valid JSON Schema `$ref` object. ## 4. The Catalog API & Functions -A catalog groups component definitions and function definitions together. +A catalog groups component definitions and function definitions together, along with an optional theme schema. ```typescript interface FunctionApi { @@ -313,9 +331,9 @@ class Catalog { readonly id: string; // Unique catalog URI readonly components: ReadonlyMap; readonly functions?: ReadonlyMap; - readonly theme?: Schema; + readonly themeSchema?: Schema; - constructor(id: string, components: T[], functions?: FunctionImplementation[], theme?: Schema) { + constructor(id: string, components: T[], functions?: FunctionImplementation[], themeSchema?: Schema) { // Initializes the properties } } @@ -341,7 +359,7 @@ myCustomCatalog = Catalog( id="https://mycompany.com/catalogs/custom_catalog.json", functions=basicCatalog.functions, components=basicCatalog.components + [MyCompanyLogoComponent()], - theme=basicCatalog.theme # Inherit theme schema + themeSchema=basicCatalog.themeSchema # Inherit theme schema ) ``` @@ -635,11 +653,12 @@ Create a comprehensive design document detailing: ### 3. Core Model Layer Implement the framework-agnostic Data Layer (Section 3). * Implement event streams and stateful signals. +* Implement strict Protocol Models (`A2uiMessage`, `A2uiClientCapabilities`, etc.) with JSON serialization/deserialization and schema validation logic. * Implement `DataModel`, ensuring correct JSON pointer resolution and the cascade/bubble notification strategy. * Implement `ComponentModel`, `SurfaceComponentsModel`, `SurfaceModel`, and `SurfaceGroupModel`. * Implement `DataContext` and `ComponentContext`. * Implement `MessageProcessor` and ClientCapabilities generation. -* **Action**: Write unit tests for the `DataModel` (especially pointer resolution/cascade logic) and `MessageProcessor`. Ensure they pass before continuing. +* **Action**: Write unit tests for JSON validation, the `DataModel` (especially pointer resolution/cascade logic), and `MessageProcessor`. Ensure they pass before continuing. ### 4. Framework-Specific Layer Implement the bridge between models and native UI (Section 5 & 6).