Builder Spec Model
The authoritative source is src/lib/builder/spec.ts. The following excerpt is copied verbatim from that file; do not edit this page to evolve the types before changing the source contract.
export const SPEC_VERSION = 1 as const;
export type NodeId = string;
/** A single node on the builder canvas. */
export type BuilderNode = {
id: NodeId;
/** key into the block registry (src/lib/builder/blocks.ts) */
type: string;
props: Record<string, unknown>;
/** present only for container blocks */
children?: BuilderNode[];
};
export type BuilderSpec = {
version: typeof SPEC_VERSION;
title: string;
root: BuilderNode[];
};
/** Mutation vocabulary shared by the UI, the relay API and the persisted log. */
export type BuilderOp =
| { op: 'set-spec'; spec: BuilderSpec }
| { op: 'set-title'; title: string }
| { op: 'add'; node: Omit<BuilderNode, 'id'> & { id?: NodeId }; parentId?: NodeId | null; index?: number }
| { op: 'update'; id: NodeId; props: Record<string, unknown> }
| { op: 'remove'; id: NodeId }
| { op: 'move'; id: NodeId; parentId?: NodeId | null; index: number }
| { op: 'select'; id: NodeId | null }
| { op: 'clear' };Model semantics
versionis the format discriminator and currently has the literal value1.rootis a forest, so a screen can contain several top-level nodes.typeis a lookup key, not a Svelte component reference. This keeps the document serialisable.childrenis present only when the block is a container.propsis open at the transport level. The selected block’s metadata supplies the editable field contract.set-specandclearoperate at document scale; the remaining mutation cases address title, nodes, placement, props, or selection.addmay omit the id so the store can allocate one, and may address either a container or the root throughparentId.moverequires the destination index and optionally addresses a container.
Related serialisable contracts
The same source file defines chat and relay state:
export type ChatRole = 'user' | 'agent' | 'system';
export type ChatMessage = {
id: string;
role: ChatRole;
text: string;
/** epoch ms */
at: number;
/** ops the agent applied alongside this message, for the transcript */
ops?: BuilderOp[];
};
/** Everything the relay API hands an agent in one read. */
export type BuilderState = {
version: typeof SPEC_VERSION;
spec: BuilderSpec;
selectedId: NodeId | null;
messages: ChatMessage[];
/** user messages with no agent reply after them */
pending: ChatMessage[];
};BuilderState.pending is derived state for relay consumption; the persisted session contract is { spec, messages }, not the whole BuilderState object.