Voce IR
AI-native intermediate representation for user interfaces.
The code is gone. The experience remains.
Voce IR is a binary IR format, like SPIR-V for graphics but for UI. AI generates typed IR from natural language, a validator enforces quality rules, and a compiler emits optimized output across 7 targets:
| Target | Output |
|---|---|
| DOM | Single-file HTML with inline CSS, zero-dependency JS, ARIA attributes |
| WebGPU | WGSL shaders, PBR materials, particle systems |
| WASM | State machines and compute as WebAssembly functions |
| Hybrid | Per-component target analysis (DOM + WebGPU + WASM) |
| iOS | SwiftUI views with VoiceOver accessibility |
| Android | Jetpack Compose with Material Design and TalkBack |
| Table-based HTML with inline CSS for cross-client support |
Pipeline
Natural Language
→ [AI Bridge] → JSON IR
→ [Validator] → 9 quality passes (46 rules)
→ [Compiler] → Optimized output
→ [Deploy] → Vercel / Cloudflare / Netlify / Static
Quick Start
# Install
cargo install voce-validator
# Validate an IR file
voce validate my-page.voce.json
# Compile to HTML
voce compile my-page.voce.json
# Deploy
voce deploy my-page.voce.json --adapter static
Three Pillars
Every design decision in Voce IR is anchored to three non-negotiable pillars:
-
Stability — Security is a compile error, not a configuration option. CSRF required on mutations, HTTPS enforced, auth routes need redirects.
-
Experience — Zero-runtime output. No framework JS shipped. Spring physics solved at compile time. Every byte serves the user.
-
Accessibility — Missing
SemanticNodeis a validation error, not a warning. Keyboard equivalents required on every gesture. Heading hierarchy enforced.
Learn More
- Getting Started — install and compile your first IR
- Schema Reference — every node type documented
- Architecture — how the pipeline works
- Contributing — help build Voce IR
Installation
This guide walks you through installing the Voce IR toolchain.
Prerequisites
-
Rust 1.85 or later – Voce IR uses Rust edition 2024. Install Rust via rustup if you don’t have it:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -
A terminal – All Voce commands run from the command line.
Install the CLI
The voce binary ships as part of the voce-validator crate. Install it with Cargo:
cargo install voce-validator
This compiles and installs the voce binary to your Cargo bin directory (typically ~/.cargo/bin/).
Verify the installation
voce --version
You should see output like:
voce 1.0.0
If the command is not found, ensure ~/.cargo/bin is in your PATH:
export PATH="$HOME/.cargo/bin:$PATH"
Add that line to your shell profile (~/.bashrc, ~/.zshrc, etc.) to make it permanent.
Available commands
Run voce --help to see all available subcommands:
Usage: voce <COMMAND>
Commands:
validate Validate a .voce.json IR file
compile Compile IR to a target output (HTML, WebGPU, etc.)
inspect Print a summary of an IR file
preview Compile and open in a browser
json2bin Convert JSON IR to binary FlatBuffers format
bin2json Convert binary FlatBuffers IR back to JSON
help Print this message or the help of the given subcommand(s)
Updating
To update to the latest version:
cargo install voce-validator --force
Building from source
If you want to work with the development version:
git clone https://github.com/nicholasgriffintn/voce-ir.git
cd voce-ir
cargo build --workspace
The compiled binary will be at target/debug/voce.
Next steps
Now that you have the CLI installed, continue to Your First IR File to create a minimal Voce IR document by hand.
Your First IR File
Voce IR documents are JSON files with a .voce.json extension. In this guide you will create a minimal “Hello World” IR file by hand and validate it.
The minimal document
Create a file called hello.voce.json with the following content:
{
"schema_version_major": 1,
"schema_version_minor": 0,
"root": {
"node_id": "root",
"viewport_width": { "value": 1024, "unit": "Px" },
"children": [
{
"value_type": "TextNode",
"value": {
"node_id": "greeting",
"content": "Hello, world!",
"heading_level": 1,
"font_size": { "value": 36, "unit": "Px" },
"font_weight": "Bold",
"color": { "r": 0, "g": 0, "b": 0, "a": 255 }
}
}
],
"metadata": {
"title": "Hello World",
"description": "A minimal Voce IR document."
}
},
"metadata": {
"title": "Hello World",
"description": "A minimal Voce IR document.",
"language": "en"
}
}
Structure breakdown
Top-level fields
| Field | Purpose |
|---|---|
schema_version_major | Major version of the Voce IR schema. Breaking changes increment this. |
schema_version_minor | Minor version. Additive changes increment this. |
root | The ViewRoot node – every document has exactly one. |
metadata | Document-level metadata (title, description, language). |
The ViewRoot (root)
The root node represents the top-level viewport:
node_id– A unique string identifier. The root is conventionally called"root".viewport_width– The design viewport width. Uses a value/unit pair (e.g.,1024pixels).children– An array of child nodes. Each child is a tagged union withvalue_typeandvalue.metadata– Page metadata used for SEO and document identification.
Child nodes (the tagged union)
Every child in the children array has two fields:
value_type– The node kind. Common types includeTextNode,Container,Surface,MediaNode,FormNode, and many others.value– The node’s data, whose shape depends on thevalue_type.
The TextNode
The simplest visible node. In our example:
node_id– Unique identifier for this node ("greeting").content– The text string to display.heading_level– Semantic heading level (1-6). Setting this makes the compiler emit an<h1>-<h6>tag. Omit it for body text.font_size– Size with unit. SupportsPx,Rem,Em,Vw,Vh,Percent.font_weight– One ofThin,Light,Regular,Medium,SemiBold,Bold,ExtraBold,Black.color– RGBA color with values 0-255.
Validate your file
Run the validator to confirm your IR is structurally correct:
voce validate hello.voce.json
If everything is valid, you will see:
hello.voce.json: VALID (1 node, 0 warnings)
If there are problems – say you forgot the node_id – the validator reports the exact issue:
hello.voce.json: INVALID
[STR001] root.children[0]: TextNode missing required field "node_id"
Adding more nodes
Here is the same document with a subtitle added below the heading:
{
"schema_version_major": 1,
"schema_version_minor": 0,
"root": {
"node_id": "root",
"viewport_width": { "value": 1024, "unit": "Px" },
"children": [
{
"value_type": "TextNode",
"value": {
"node_id": "heading",
"content": "Hello, world!",
"heading_level": 1,
"font_size": { "value": 36, "unit": "Px" },
"font_weight": "Bold",
"color": { "r": 0, "g": 0, "b": 0, "a": 255 }
}
},
{
"value_type": "TextNode",
"value": {
"node_id": "subtitle",
"content": "This page was built with Voce IR.",
"font_size": { "value": 18, "unit": "Px" },
"color": { "r": 100, "g": 100, "b": 100, "a": 255 }
}
}
],
"metadata": {
"title": "Hello World",
"description": "A minimal Voce IR document."
}
},
"metadata": {
"title": "Hello World",
"description": "A minimal Voce IR document.",
"language": "en"
}
}
Note that the subtitle omits heading_level – it will render as a paragraph, not a heading.
Next steps
Your IR file is ready. Continue to Compiling to HTML to turn it into a working web page.
Compiling to HTML
Once you have a valid .voce.json file, the Voce compiler transforms it into production-ready output. This guide covers the DOM compile target, which produces a single-file HTML page.
Validate first
Always validate before compiling. The compiler assumes valid input:
voce validate hello.voce.json
hello.voce.json: VALID (2 nodes, 0 warnings)
Compile
Run the compile command:
voce compile hello.voce.json
By default this writes output to the dist/ directory:
Compiling hello.voce.json → dist/hello.html
2 nodes compiled
Output: dist/hello.html (4.2 KB)
What the compiler produces
The output is a single self-contained HTML file. Open dist/hello.html in any browser and you will see your rendered page.
Key properties of the compiled output:
- Zero runtime dependencies. No JavaScript frameworks, no CSS libraries, no CDN links. The HTML file contains everything it needs inline.
- Semantic markup. A
TextNodewithheading_level: 1becomes an<h1>. Containers become appropriate structural elements. Accessibility attributes are wired automatically fromSemanticNodereferences. - Surgical DOM updates. When the IR includes state machines, the compiler emits minimal JavaScript that performs targeted DOM mutations – similar to what Svelte or SolidJS produce, but generated from binary IR rather than a component DSL.
- No supply chain risk. Because there are zero external dependencies in the output, there is no attack surface from third-party packages.
Output options
Custom output directory
voce compile hello.voce.json --output ./build
JSON output mode
For debugging, you can emit a JSON representation of the compile plan:
voce compile hello.voce.json --format json
Preview in the browser
The preview command compiles and immediately opens the result in your default browser:
voce preview hello.voce.json
This is the fastest way to iterate. It compiles to a temporary directory and launches the browser in one step.
Compile targets
The DOM/HTML target is the default. Voce supports multiple compile targets:
| Target | Flag | Output |
|---|---|---|
| DOM (HTML) | --target dom | Single-file .html |
| WebGPU | --target webgpu | HTML + WebGPU rendering |
| WASM | --target wasm | WebAssembly module |
| Hybrid | --target hybrid | DOM for content, WebGPU for effects |
| iOS (SwiftUI) | --target ios | Swift source files |
| Android (Compose) | --target android | Kotlin source files |
--target email | Email-safe HTML |
For example, to compile for email:
voce compile hello.voce.json --target email
Inspecting the IR
Before compiling, you can get a quick summary of what is in your IR file:
voce inspect hello.voce.json
Document: Hello World
Schema: v1.0
Nodes: 2 (1 TextNode, 1 ViewRoot)
Language: en
Warnings: 0
A complete workflow
Putting it all together, a typical workflow looks like this:
# 1. Create or generate the IR file
# (by hand, via AI, or from an existing tool)
# 2. Validate
voce validate my-page.voce.json
# 3. Compile
voce compile my-page.voce.json
# 4. Preview
voce preview my-page.voce.json
# 5. Deploy the output
# dist/my-page.html is a static file -- serve it from anywhere
Next steps
Writing IR by hand works for learning, but Voce is designed for AI authorship. Continue to AI Generation to learn how to generate IR from natural language.
AI Generation
Voce IR is designed from the ground up for AI authorship. Rather than having an AI write framework code, the AI generates typed IR directly and a compiler handles the output. There are two ways to generate IR with AI: the TypeScript SDK and the MCP server.
Approaches
TypeScript SDK
The @voce-ir/ai-bridge package provides a high-level API for generating IR from natural language prompts. It handles schema context, validation, and iterative refinement automatically.
MCP Server
The Voce MCP (Model Context Protocol) server exposes IR generation as a tool that any MCP-compatible client can call – including Claude Desktop, Claude Code, and other AI assistants. This lets you generate and compile IR through conversation without writing any integration code.
SDK quickstart
Install
npm install @voce-ir/ai-bridge
Set your API key
The SDK uses the Anthropic API to generate IR. Export your key:
export ANTHROPIC_API_KEY=sk-ant-...
Generate IR from a prompt
import { VoceGenerator } from "@voce-ir/ai-bridge";
const generator = new VoceGenerator({
apiKey: process.env.ANTHROPIC_API_KEY,
});
// Describe what you want in natural language
const result = await generator.generate(
"A landing page with a bold headline that says 'Ship faster with Voce', " +
"a subtitle explaining the product, and a blue call-to-action button."
);
// result.ir contains the validated .voce.json document
console.log(JSON.stringify(result.ir, null, 2));
// result.warnings contains any validation warnings
if (result.warnings.length > 0) {
console.warn("Warnings:", result.warnings);
}
Generate and compile in one step
import { VoceGenerator } from "@voce-ir/ai-bridge";
import { writeFileSync } from "fs";
const generator = new VoceGenerator({
apiKey: process.env.ANTHROPIC_API_KEY,
});
const result = await generator.generateAndCompile(
"A contact form with name, email, and message fields. " +
"Include proper validation and a submit button.",
{ target: "dom" }
);
// result.html contains the compiled single-file HTML
writeFileSync("dist/contact.html", result.html);
Iterative refinement
The SDK supports multi-turn refinement. The AI asks clarifying questions when the prompt is ambiguous, and you can provide follow-up instructions:
const session = generator.createSession();
// Initial generation
let result = await session.generate(
"A pricing page with three tiers"
);
// Refine based on the result
result = await session.refine(
"Make the middle tier visually prominent and add a 'Most Popular' badge"
);
// Further refinement
result = await session.refine(
"Add a toggle to switch between monthly and annual pricing"
);
// The final IR incorporates all refinements
writeFileSync("pricing.voce.json", JSON.stringify(result.ir, null, 2));
MCP server
Setup with Claude Desktop
Add the Voce MCP server to your Claude Desktop configuration (claude_desktop_config.json):
{
"mcpServers": {
"voce-ir": {
"command": "npx",
"args": ["@voce-ir/mcp-server"],
"env": {
"ANTHROPIC_API_KEY": "sk-ant-..."
}
}
}
}
Once configured, you can ask Claude to generate Voce IR through normal conversation. The MCP server exposes tools for generating, validating, compiling, and previewing IR.
Available MCP tools
| Tool | Description |
|---|---|
voce_generate | Generate IR from a natural language description |
voce_validate | Validate an IR document |
voce_compile | Compile IR to a target format |
voce_preview | Compile and return a preview URL |
voce_inspect | Return a summary of an IR document |
The playground
Try Voce IR without installing anything at voce-ir.xyz. The playground provides:
- A prompt input for natural language descriptions
- Live IR preview with syntax highlighting
- One-click compilation to HTML
- A visual preview of the compiled output
Note that the playground requires an API key for generation features. Compilation and validation work without one.
How generation works
Under the hood, the AI generation pipeline works as follows:
- Schema context. The SDK provides the AI model with the full Voce IR schema – all node types, their fields, valid values, and constraints.
- Generation. The model generates a
.voce.jsondocument from your natural language prompt. - Validation. The generated IR is run through the full validator (structural checks, accessibility, security, SEO, forms, i18n).
- Auto-repair. If validation fails, the SDK sends the errors back to the model for automatic correction. This loop runs up to 3 times.
- Output. The validated IR is returned, ready for compilation.
This pipeline ensures that AI-generated IR is always valid and meets all quality gates – accessibility, security, and SEO checks are compile errors, not optional linting.
Next steps
- Explore the Schema Reference to understand all available node types
- Read about Validation Passes to understand what the validator checks
- See the CLI Reference for all command options
voce validate
Run the full Voce IR validation suite against a .voce.json file. The validator
executes 9 independent passes covering structural integrity, accessibility,
security, SEO, and more. Any failing pass causes a non-zero exit.
Usage
voce validate <FILE> [OPTIONS]
Arguments
| Argument | Description |
|---|---|
<FILE> | Path to a .voce.json IR file (required) |
Options
| Flag | Default | Description |
|---|---|---|
--format <FORMAT> | terminal | Output format: terminal (colored, human-readable) or json (machine-readable) |
--warn-as-error | off | Treat warnings as errors, causing a non-zero exit code |
Validation Passes
The validator runs the following passes in order:
| Pass | Code Prefix | What it checks |
|---|---|---|
| Structural | STR001–STR005 | Required fields, node completeness, document structure |
| References | REF001–REF009 | All node refs resolve, no dangling IDs, no cycles |
| State Machine | STA001–STA004 | Valid states, transitions, initial state exists |
| Accessibility | A11Y001–A11Y005 | Keyboard equivalents, heading hierarchy, alt text, form labels |
| Security | SEC001–SEC004 | CSRF on mutations, auth redirects, HTTPS enforcement, password autocomplete |
| SEO | SEO001–SEO007 | Title present, description length, single h1, OpenGraph completeness |
| Forms | FRM001–FRM009 | Fields required, labels present, unique names, email pattern validation |
| Internationalization | I18N001–I18N003 | Localized key non-empty, default value present, consistency across locales |
| Motion | MOT001–MOT005 | ReducedMotion fallback required, damping > 0, duration warnings |
Exit Codes
| Code | Meaning |
|---|---|
0 | All passes succeeded (no errors, no warnings or --warn-as-error not set) |
1 | One or more validation errors (or warnings when --warn-as-error is set) |
Examples
Validate a file with colored terminal output:
voce validate examples/landing-page.voce.json
Validate and get JSON output (useful for CI or piping to jq):
voce validate examples/landing-page.voce.json --format json
Fail the build on any warning:
voce validate my-app.voce.json --warn-as-error
Combine with other tools:
# Validate, then compile only if valid
voce validate app.voce.json && voce compile app.voce.json
JSON Output Schema
When --format json is used, the output is a JSON object:
{
"valid": false,
"errors": [
{ "pass": "A11Y", "code": "A11Y003", "message": "Image node missing alt text", "node_id": "img-hero" }
],
"warnings": [
{ "pass": "MOT", "code": "MOT005", "message": "Animation duration exceeds 5s", "node_id": "fade-in" }
]
}
voce compile
Compile a validated Voce IR file into a single-file HTML document. The compiler runs the full validation suite first – if validation fails, compilation is aborted and errors are printed.
Usage
voce compile <FILE> [OPTIONS]
Arguments
| Argument | Description |
|---|---|
<FILE> | Path to a .voce.json IR file (required) |
Options
| Flag | Default | Description |
|---|---|---|
-o, --output <PATH> | dist/<stem>.html | Output file path. If omitted, derives from the input filename. |
--debug | off | Add data-voce-id attributes to every emitted DOM element, mapping each back to its IR node ID. |
How It Works
- Validate – all 9 validation passes run. Any error aborts compilation.
- Resolve layout – Taffy computes flexbox/grid geometry at compile time.
- Emit HTML – a single self-contained
.htmlfile is written. All styles are inlined. There are zero runtime dependencies.
The compiled output follows the SolidJS/Svelte pattern of surgical DOM construction – no virtual DOM, no framework runtime.
Exit Codes
| Code | Meaning |
|---|---|
0 | Compilation succeeded, output file written |
1 | Validation failed (errors printed to stderr) |
2 | Compilation error (e.g., unsupported node type, I/O failure) |
Examples
Compile with default output path:
voce compile examples/landing-page.voce.json
# writes dist/landing-page.html
Compile to a specific output file:
voce compile app.voce.json -o build/index.html
Compile with debug attributes for development:
voce compile app.voce.json --debug
# Each element gets: <div data-voce-id="container-main">...</div>
Validate and compile in sequence:
voce validate app.voce.json --format json && voce compile app.voce.json
Debug Mode
When --debug is passed, every emitted HTML element receives a
data-voce-id attribute containing the IR node ID it was generated from.
This is useful for:
- Tracing compiled output back to the source IR
- Browser DevTools inspection
- Integration with
voce inspectfor cross-referencing
Do not ship debug builds to production – the extra attributes increase file size and expose internal structure.
Output Format
The compiled HTML file is fully self-contained:
- Inline
<style>block with all computed styles - Inline
<script>block for state machines and event handlers (if present) - No external dependencies, CDN links, or network requests
- Valid HTML5 with lang attribute and semantic structure
voce deploy
Validate, compile, and prepare a deployment bundle for a target hosting platform. Wraps the full pipeline (validate, compile) and adds platform-specific configuration files.
Usage
voce deploy <FILE> [OPTIONS]
Arguments
| Argument | Description |
|---|---|
<FILE> | Path to a .voce.json IR file (required) |
Options
| Flag | Default | Description |
|---|---|---|
--adapter <ADAPTER> | static | Deployment target: static, vercel, cloudflare, netlify |
--dry-run | off | Show what would be generated without writing any files |
Adapters
Each adapter produces a deployment-ready bundle in dist/:
| Adapter | Output |
|---|---|
static | Compiled HTML only. Suitable for any static file host (S3, GitHub Pages, rsync). |
vercel | HTML plus vercel.json with routing and cache headers. |
cloudflare | HTML plus _headers and _redirects files for Cloudflare Pages. |
netlify | HTML plus netlify.toml with headers and redirect rules. |
Configuration
The deploy command reads defaults from .voce/config.toml if present:
[deploy]
adapter = "vercel"
output_dir = "dist"
Command-line flags override config file values.
Exit Codes
| Code | Meaning |
|---|---|
0 | Deployment bundle created successfully |
1 | Validation failed |
2 | Compilation failed |
3 | Deployment preparation failed (e.g., missing config, I/O error) |
Examples
Deploy as static files (default):
voce deploy app.voce.json
# writes dist/app.html
Deploy to Vercel:
voce deploy app.voce.json --adapter vercel
# writes dist/app.html and dist/vercel.json
Preview what a Cloudflare deploy would produce:
voce deploy app.voce.json --adapter cloudflare --dry-run
Deploy with config file defaults:
# With .voce/config.toml setting adapter = "netlify"
voce deploy app.voce.json
# uses netlify adapter from config
Dry Run
The --dry-run flag prints the list of files that would be written and their
approximate sizes, without creating or modifying anything on disk. Use this to
verify adapter output before committing to a deploy.
$ voce deploy app.voce.json --adapter vercel --dry-run
[dry-run] dist/app.html (12.4 KB)
[dry-run] dist/vercel.json (0.3 KB)
Pipeline
The deploy command runs the full pipeline internally:
voce validate <FILE>– abort on errorsvoce compile <FILE>– produce HTML- Generate adapter-specific files
- Write all files to the output directory
voce inspect
Display a human-readable summary of a Voce IR file. Shows node counts, type distribution, state machines, and feature usage without compiling or validating.
Usage
voce inspect <FILE>
Arguments
| Argument | Description |
|---|---|
<FILE> | Path to a .voce.json IR file (required) |
Output
The inspect command prints a structured summary to stdout. There are no output format options – the output is always a human-readable table.
Summary Sections
Document overview – total node count, document-level metadata (title, locale, auth configuration).
Node type distribution – count of each node type present in the IR (e.g., Container: 5, TextNode: 12, MediaNode: 3).
State machines – names, state counts, and transition counts for each StateMachine node.
Features detected – which optional IR features are in use: animations, forms, navigation/routing, i18n, SEO metadata, theming, accessibility annotations, data/backend bindings.
Exit Codes
| Code | Meaning |
|---|---|
0 | IR file parsed and summary printed |
1 | File could not be read or parsed as valid JSON |
Examples
Inspect a landing page IR:
voce inspect examples/landing-page.voce.json
Example output:
Voce IR Summary
===============
Document: landing-page
Nodes: 37
Types: 11 distinct
Node Distribution:
Container 8
Surface 4
TextNode 12
MediaNode 3
FormNode 1
FormField 4
StateMachine 1
SemanticNode 2
PageMetadata 1
AnimationTransition 1
State Machines:
form-states 3 states, 4 transitions
Features:
[x] Accessibility
[x] Forms
[x] SEO metadata
[x] Animation
[ ] Navigation
[ ] Internationalization
[ ] Theming
[ ] Data bindings
Use Cases
- Quick audit – understand what an IR file contains before validating or compiling it.
- CI reporting – log IR complexity metrics alongside build output.
- Debugging – verify that AI-generated IR includes the expected node types and features.
- Diffing – compare inspect output across versions to spot structural changes.
Notes
The inspect command does not validate the IR. A file with structural errors
can still be inspected as long as it is parseable JSON. To check correctness,
use voce validate.
voce preview
Compile a Voce IR file and open the result in the default web browser.
A convenience command that combines voce compile with a platform-aware
browser launch.
Usage
voce preview <FILE>
Arguments
| Argument | Description |
|---|---|
<FILE> | Path to a .voce.json IR file (required) |
How It Works
- Validate – runs the full 9-pass validation suite.
- Compile – produces a self-contained HTML file in a temporary
location (or
dist/if it exists). - Open – launches the compiled HTML in the default browser using the platform-appropriate command.
Platform Commands
| Platform | Command used |
|---|---|
| macOS | open <file> |
| Linux | xdg-open <file> |
| Windows | start <file> |
Exit Codes
| Code | Meaning |
|---|---|
0 | File compiled and browser launch initiated |
1 | Validation failed |
2 | Compilation failed |
3 | Browser could not be opened |
Examples
Preview a landing page:
voce preview examples/landing-page.voce.json
Preview after making changes to the IR:
# Edit the IR (or regenerate via AI), then preview
voce preview app.voce.json
Chain with validation for verbose feedback:
voce validate app.voce.json && voce preview app.voce.json
Comparison with compile
voce compile | voce preview | |
|---|---|---|
| Validates | Yes | Yes |
| Writes HTML | To -o path or dist/ | To temporary or dist/ |
| Opens browser | No | Yes |
| Debug attributes | --debug flag | Not available |
| Custom output path | -o flag | Not available |
For production builds, use voce compile. For quick iteration during
development, use voce preview.
Notes
- The preview command always compiles a fresh copy. It does not cache previous builds.
- Debug attributes (
data-voce-id) are not included in preview builds. Usevoce compile --debugif you need them, then open the output file manually. - If no default browser is configured on Linux,
xdg-openmay fail silently. Set theBROWSERenvironment variable as a fallback. - The compiled HTML is fully self-contained with no external dependencies, so it renders correctly when opened as a local file.
Schema Overview
Voce IR uses FlatBuffers as its binary serialization format. Every UI is represented as a VoceDocument containing a tree of typed nodes spanning 11 domains: layout, state, motion, navigation, accessibility, theming, data, forms, SEO, and i18n.
Binary Format
FlatBuffers provides zero-copy deserialization, schema evolution with forward/backward compatibility, and a compact binary representation. Voce IR files use the VOCE file identifier and the .voce extension.
The binary format is immutable by design. Runtime mutable state lives in a separate reactive layer managed by the compiler output, not in the buffer itself.
JSON Canonical Representation
Every Voce IR binary round-trips losslessly to and from JSON. The JSON representation serves as:
- AI generation target – LLMs emit JSON, which is then compiled to binary
- Debugging format – human-inspectable when needed
- Version control diffing – text diffs for review workflows
- Escape hatch – interop with tools that cannot read FlatBuffers
The JSON form is not intended for hand-authoring. It is a machine-readable text serialization of the IR.
ChildUnion Pattern
FlatBuffers does not support vectors of unions directly in Rust codegen. Voce IR works around this with a wrapper table:
{
"children": [
{ "value_type": "Container", "value": { "node_id": "c1", "layout": "Flex" } },
{ "value_type": "TextNode", "value": { "node_id": "t1", "content": "Hello" } }
]
}
The ChildUnion is a union of all 27 node types across all domains. Each child entry is wrapped in a ChildNode table containing a single value field of type ChildUnion.
ChildUnion Members
| Domain | Node Types |
|---|---|
| Layout | Container, Surface, TextNode, MediaNode |
| State | StateMachine, DataNode, ComputeNode, EffectNode, ContextNode |
| Motion | AnimationTransition, Sequence, GestureHandler, ScrollBinding, PhysicsBody |
| Navigation | RouteMap |
| Accessibility | SemanticNode, LiveRegion, FocusTrap |
| Theming | ThemeNode, PersonalizationSlot, ResponsiveRule |
| Data | ActionNode, SubscriptionNode, AuthContextNode, ContentSlot, RichTextNode |
| Forms | FormNode |
VoceDocument
The root table of every Voce IR file.
| Field | Type | Required | Description |
|---|---|---|---|
| schema_version_major | int32 | no | Major schema version (default 0) |
| schema_version_minor | int32 | no | Minor schema version (default 1) |
| root | ViewRoot | yes | Top-level view root for the document |
| routes | RouteMap | no | Application-level route map for multi-route apps |
| theme | ThemeNode | no | Primary theme |
| alternate_themes | [ThemeNode] | no | Additional themes (dark, high-contrast, etc.) |
| auth | AuthContextNode | no | Application-level auth configuration |
| i18n | I18nConfig | no | Internationalization configuration |
Minimal Example
{
"schema_version_major": 0,
"schema_version_minor": 1,
"root": {
"node_id": "root",
"document_language": "en",
"children": [
{
"value_type": "TextNode",
"value": {
"node_id": "greeting",
"content": "Hello, world",
"heading_level": 1
}
}
]
}
}
Foundation Types
These shared types appear throughout the schema.
| Type | Fields | Description |
|---|---|---|
| Color | r, g, b, a (ubyte) | RGBA color value |
| Length | value (float32), unit (LengthUnit) | Dimensional value with unit |
| Duration | ms (float32) | Time duration in milliseconds |
| Easing | easing_type, control points/spring params | Animation timing function |
| EdgeInsets | top, right, bottom, left (Length) | Four-sided spacing |
| CornerRadii | top_left, top_right, bottom_right, bottom_left (Length) | Per-corner radius |
| Shadow | offset_x, offset_y, blur, spread, color, inset | Box shadow definition |
| DataBinding | source_node_id, field_path | Runtime data reference |
LengthUnit Values
Px, Rem, Em, Percent, Vw, Vh, Dvh, Svh, Auto, FitContent, MinContent, MaxContent, Fr
EasingType Values
Linear, CubicBezier, Spring, Steps, CustomLinear
Layout Nodes
Layout nodes define the spatial composition of a Voce IR document. There are five layout node types: ViewRoot (the document root), Container (grouping and layout), Surface (visual rectangles), TextNode (styled text), and MediaNode (images, video, audio).
ViewRoot
Top-level container for a document or route. One ViewRoot per page. Defines viewport bounds, document language, and holds the flat semantic node list.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| children | [ChildNode] | no | Child nodes |
| width | Length | no | Viewport width |
| height | Length | no | Viewport height |
| background | Color | no | Document background color |
| document_language | string | no | BCP 47 language tag (e.g., “en”) |
| text_direction | TextDirection | no | Ltr (default), Rtl, or Auto |
| semantic_nodes | [SemanticNode] | no | Flat list of semantic nodes referenced by visual nodes |
| metadata | PageMetadata | no | Per-page SEO metadata |
{
"node_id": "root",
"document_language": "en",
"text_direction": "Ltr",
"background": { "r": 255, "g": 255, "b": 255, "a": 255 },
"children": []
}
Container
Groups children with a layout strategy. The primary structural node for composition.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| children | [ChildNode] | no | Child nodes |
| layout | ContainerLayout | no | Stack (default), Flex, Grid, or Absolute |
| direction | LayoutDirection | no | Row, Column (default), RowReverse, ColumnReverse |
| main_align | Alignment | no | Main axis alignment (default Start) |
| cross_align | Alignment | no | Cross axis alignment (default Start) |
| gap | Length | no | Gap between children |
| padding | EdgeInsets | no | Inner padding |
| wrap | bool | no | Enable flex wrapping (default false) |
| grid_columns | [Length] | no | Column track sizes for Grid layout |
| grid_rows | [Length] | no | Row track sizes for Grid layout |
| width | Length | no | Explicit width |
| height | Length | no | Explicit height |
| min_width | Length | no | Minimum width constraint |
| max_width | Length | no | Maximum width constraint |
| min_height | Length | no | Minimum height constraint |
| max_height | Length | no | Maximum height constraint |
| overflow_x | Overflow | no | Horizontal overflow (default Visible) |
| overflow_y | Overflow | no | Vertical overflow (default Visible) |
| clip | bool | no | Clip overflowing content (default false) |
| position | Position | no | Relative (default), Absolute, Fixed, Sticky |
| top | Length | no | Top offset (for positioned elements) |
| right | Length | no | Right offset |
| bottom | Length | no | Bottom offset |
| left | Length | no | Left offset |
| z_index | int32 | no | Stacking order (default 0) |
| opacity | float32 | no | Opacity 0.0-1.0 (default 1.0) |
| background | Color | no | Background color |
| border | BorderSides | no | Per-side border configuration |
| corner_radius | CornerRadii | no | Per-corner border radius |
| shadow | [Shadow] | no | Box shadows |
| semantic_node_id | string | no | Reference to a SemanticNode |
{
"node_id": "hero-row",
"layout": "Flex",
"direction": "Row",
"gap": { "value": 16, "unit": "Px" },
"padding": {
"top": { "value": 24, "unit": "Px" },
"right": { "value": 24, "unit": "Px" },
"bottom": { "value": 24, "unit": "Px" },
"left": { "value": 24, "unit": "Px" }
},
"main_align": "Center",
"cross_align": "Center",
"children": []
}
Surface
A visible rectangular region used for cards, backgrounds, dividers, and decorative elements.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| children | [ChildNode] | no | Child nodes |
| fill | Color | no | Fill color |
| stroke | Color | no | Stroke/border color |
| stroke_width | Length | no | Stroke thickness |
| corner_radius | CornerRadii | no | Per-corner border radius |
| shadow | [Shadow] | no | Box shadows |
| opacity | float32 | no | Opacity 0.0-1.0 (default 1.0) |
| border | BorderSides | no | Per-side border configuration |
| width | Length | no | Explicit width |
| height | Length | no | Explicit height |
| min_width | Length | no | Minimum width constraint |
| max_width | Length | no | Maximum width constraint |
| min_height | Length | no | Minimum height constraint |
| max_height | Length | no | Maximum height constraint |
| padding | EdgeInsets | no | Inner padding |
| decorative | bool | no | If true, no SemanticNode required (default false) |
| semantic_node_id | string | no | Reference to a SemanticNode |
{
"node_id": "card",
"fill": { "r": 248, "g": 248, "b": 248, "a": 255 },
"corner_radius": {
"top_left": { "value": 8, "unit": "Px" },
"top_right": { "value": 8, "unit": "Px" },
"bottom_right": { "value": 8, "unit": "Px" },
"bottom_left": { "value": 8, "unit": "Px" }
},
"shadow": [{
"offset_x": { "value": 0, "unit": "Px" },
"offset_y": { "value": 2, "unit": "Px" },
"blur": { "value": 8, "unit": "Px" },
"spread": { "value": 0, "unit": "Px" },
"color": { "r": 0, "g": 0, "b": 0, "a": 25 }
}],
"decorative": false,
"children": []
}
TextNode
Styled text content. All typography properties are explicit with no cascade.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| content | string | no | Static text content |
| content_binding | DataBinding | no | Dynamic content from a DataNode |
| localized_content | LocalizedString | no | i18n content (alternative to static content) |
| font_family | string | no | Font family name |
| font_size | Length | no | Font size |
| font_weight | FontWeight | no | 100-900 weight (default Regular/400) |
| line_height | float32 | no | Line height multiplier (default 1.5) |
| letter_spacing | Length | no | Letter spacing |
| text_align | TextAlign | no | Start (default), Center, End, Justify |
| text_overflow | TextOverflow | no | Clip (default), Ellipsis, Fade |
| text_decoration | TextDecoration | no | None (default), Underline, LineThrough |
| max_lines | int32 | no | Maximum number of visible lines |
| color | Color | no | Text color |
| opacity | float32 | no | Opacity 0.0-1.0 (default 1.0) |
| heading_level | int8 | no | 0 = not a heading, 1-6 = h1-h6 (default 0) |
| semantic_node_id | string | no | Reference to a SemanticNode |
{
"node_id": "page-title",
"content": "Welcome to Voce",
"heading_level": 1,
"font_size": { "value": 3, "unit": "Rem" },
"font_weight": "Bold",
"color": { "r": 17, "g": 24, "b": 39, "a": 255 }
}
MediaNode
Image, video, audio, or SVG with explicit dimensions, loading strategy, and format negotiation.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| media_type | MediaType | no | Image (default), Video, Audio, Svg |
| src | string | yes | Source URL |
| alt | string | no | Alt text (required for non-decorative images) |
| width | Length | no | Explicit width (recommended for CLS prevention) |
| height | Length | no | Explicit height |
| aspect_ratio | float32 | no | Aspect ratio (width/height) |
| object_fit | ObjectFit | no | Cover (default), Contain, Fill, ScaleDown, None |
| loading | LoadingStrategy | no | Eager or Lazy (default) |
| corner_radius | CornerRadii | no | Per-corner border radius |
| opacity | float32 | no | Opacity 0.0-1.0 (default 1.0) |
| srcset_widths | [int32] | no | Widths for responsive srcset generation |
| sizes | string | no | Sizes attribute for responsive images |
| decorative | bool | no | If true, no alt text required (default false) |
| above_fold | bool | no | If true, compiler uses eager loading (default false) |
| semantic_node_id | string | no | Reference to a SemanticNode |
{
"node_id": "hero-image",
"media_type": "Image",
"src": "/images/hero.webp",
"alt": "Product dashboard showing analytics overview",
"width": { "value": 100, "unit": "Percent" },
"aspect_ratio": 1.778,
"above_fold": true,
"loading": "Eager"
}
State & Logic Nodes
All state in Voce IR is modeled as explicit, typed finite state machines. There are no implicit closures, dependency arrays, or hook ordering. The compiler can statically analyze every possible state transition.
StateMachine
A named finite state machine with typed states, transitions, guards, and effects. Every component’s behavior is a state machine. The validator checks reachability (no dead states) and deadlock freedom.
State
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | yes | State name |
| initial | bool | no | Whether this is the initial state (default false) |
| terminal | bool | no | Whether this is a final state (default false) |
Transition
| Field | Type | Required | Description |
|---|---|---|---|
| event | string | yes | Event name that triggers this transition |
| from | string | yes | Source state name |
| to | string | yes | Target state name |
| guard | string | no | Reference to a ComputeNode that returns bool |
| effect | string | no | Reference to an EffectNode to execute on transition |
StateMachine
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| name | string | no | Human-readable name (e.g., “auth-flow”) |
| states | [State] | yes | List of states |
| transitions | [Transition] | yes | List of transitions |
{
"node_id": "btn-state",
"name": "button-state",
"states": [
{ "name": "idle", "initial": true },
{ "name": "hovered" },
{ "name": "pressed" },
{ "name": "disabled", "terminal": true }
],
"transitions": [
{ "event": "hover", "from": "idle", "to": "hovered" },
{ "event": "unhover", "from": "hovered", "to": "idle" },
{ "event": "press", "from": "hovered", "to": "pressed" },
{ "event": "release", "from": "pressed", "to": "idle", "effect": "submit-effect" }
]
}
DataNode
Declares an external data dependency. The compiler emits fetch code with caching, error handling, and loading states.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| name | string | no | Human-readable name |
| source | DataSource | yes | Endpoint configuration |
| cache_strategy | CacheStrategy | no | None, StaleWhileRevalidate (default), CacheUntilInvalidated, Static |
| stale_time | uint32 | no | Freshness duration in ms (default 30000) |
| cache_time | uint32 | no | Cache retention in ms (default 300000) |
| cache_tags | [string] | no | Tags for cache invalidation |
| auth_required | bool | no | Whether authentication is needed (default false) |
| loading_state_machine | string | no | StateMachine tracking loading/error/success |
| query_params | [KeyValue] | no | Query parameters for filtering, sorting, pagination |
DataSource
| Field | Type | Required | Description |
|---|---|---|---|
| provider | DataSourceProvider | no | Rest (default), GraphQL, Supabase, Firebase, Convex, Custom |
| endpoint | string | no | API endpoint URL |
| resource | string | no | Resource path or query |
| method | HttpMethod | no | GET (default), POST, PUT, PATCH, DELETE |
| headers | [KeyValue] | no | Custom request headers |
{
"node_id": "user-data",
"name": "current-user",
"source": {
"provider": "Rest",
"endpoint": "/api/users/me",
"method": "GET"
},
"cache_strategy": "StaleWhileRevalidate",
"stale_time": 60000,
"auth_required": true
}
ComputeNode
A pure function from inputs to output. Referentially transparent, so the compiler can memoize or pre-compute at build time.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| name | string | no | Human-readable name |
| inputs | [ComputeInput] | yes | Input bindings from other nodes |
| expression | string | yes | Expression to evaluate |
| output_type | string | no | Output type hint for the compiler |
ComputeInput
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | yes | Parameter name in the expression |
| source_node_id | string | yes | Source DataNode, ContextNode, or ComputeNode |
| field_path | string | no | Dot-path into the source data |
{
"node_id": "total-compute",
"name": "order-total",
"inputs": [
{ "name": "price", "source_node_id": "product-data", "field_path": "price" },
{ "name": "qty", "source_node_id": "cart-ctx", "field_path": "quantity" }
],
"expression": "price * qty",
"output_type": "number"
}
EffectNode
A side effect triggered by a state transition. Effects attach to transitions, never to states, eliminating mount/unmount ambiguity.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| name | string | no | Human-readable name |
| effect_type | EffectType | no | Analytics, ApiCall, Storage, Haptic, Log, Navigate, Custom |
| config | [KeyValue] | no | Configuration payload |
| api_source | DataSource | no | Endpoint configuration for ApiCall effects |
| idempotent | bool | no | Whether safe to retry (default false) |
{
"node_id": "track-click",
"name": "analytics-click",
"effect_type": "Analytics",
"config": [
{ "key": "event", "value": "button_click" },
{ "key": "category", "value": "cta" }
]
}
ContextNode
Shared state scoped to a subtree. Replaces React Context, Redux, and prop drilling. Typed with explicit read/write boundaries.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| name | string | yes | Context name |
| initial_value | string | no | Initial value as JSON string |
| writers | [string] | no | Node IDs allowed to write (empty = any descendant) |
| global | bool | no | If true, not scoped to subtree (default false) |
{
"node_id": "cart-ctx",
"name": "shopping-cart",
"initial_value": "{ \"items\": [], \"quantity\": 0 }",
"global": true
}
Motion & Interaction Nodes
Animation is a first-class IR concern in Voce IR. Every motion declaration includes a reduced-motion fallback, which the validator enforces as required. The compiler uses a tiered output strategy: CSS transitions, Web Animations API, then minimal requestAnimationFrame JavaScript.
AnimationTransition
Animates property changes between state machine states. The compiler chooses the optimal output technique based on the trigger type and interruptibility requirements.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| name | string | no | Human-readable name |
| target_node_id | string | yes | Node to animate |
| trigger_state_machine | string | no | StateMachine that triggers this animation |
| trigger_event | string | no | Event on the state machine that starts animation |
| properties | [AnimatedProperty] | yes | Properties to animate |
| duration | Duration | no | Animation duration in ms |
| delay | Duration | no | Delay before animation starts |
| easing | Easing | no | Timing function (supports Spring) |
| reduced_motion | ReducedMotion | no | Required alternative for prefers-reduced-motion |
AnimatedProperty
| Field | Type | Required | Description |
|---|---|---|---|
| property | string | yes | CSS-like property path (e.g., “opacity”, “transform.translateY”) |
| from | string | yes | Value in the starting state |
| to | string | yes | Value in the ending state |
Easing
The Easing table supports multiple timing function types.
| Field | Type | Required | Description |
|---|---|---|---|
| easing_type | EasingType | no | Linear (default), CubicBezier, Spring, Steps, CustomLinear |
| x1, y1, x2, y2 | float32 | no | Control points for CubicBezier |
| stiffness | float32 | no | Spring stiffness (default 200) |
| damping | float32 | no | Spring damping (default 20) |
| mass | float32 | no | Spring mass (default 1) |
| steps | int32 | no | Step count for Steps easing |
| points | [float32] | no | Pre-computed points for CustomLinear (CSS linear()) |
{
"node_id": "fade-in",
"target_node_id": "hero-section",
"trigger_state_machine": "page-state",
"trigger_event": "enter",
"properties": [
{ "property": "opacity", "from": "0", "to": "1" },
{ "property": "transform.translateY", "from": "20px", "to": "0px" }
],
"duration": { "ms": 300 },
"easing": { "easing_type": "Spring", "stiffness": 300, "damping": 25, "mass": 1 },
"reduced_motion": { "strategy": "Remove" }
}
Sequence
Choreographed animation timeline. Multiple AnimationTransitions played in sequence or parallel with stagger offsets.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| name | string | no | Human-readable name |
| steps | [SequenceStep] | yes | Ordered list of animation steps |
| stagger | Duration | no | Delay between sequential elements |
| iterations | int32 | no | Repeat count, 0 = infinite (default 1) |
| alternate | bool | no | Reverse on alternate iterations (default false) |
| reduced_motion | ReducedMotion | no | Alternative for the entire sequence |
SequenceStep
| Field | Type | Required | Description |
|---|---|---|---|
| transition_id | string | yes | Reference to an AnimationTransition node |
| delay | Duration | no | Delay before this step starts |
| parallel | bool | no | If true, runs concurrently with previous step (default false) |
{
"node_id": "entrance-seq",
"steps": [
{ "transition_id": "fade-in-title" },
{ "transition_id": "fade-in-subtitle", "delay": { "ms": 100 } },
{ "transition_id": "fade-in-cta", "delay": { "ms": 100 } }
],
"stagger": { "ms": 50 },
"reduced_motion": { "strategy": "Remove" }
}
GestureHandler
Maps touch, mouse, and keyboard input to state transitions or continuous property updates. The validator requires a keyboard_key for accessibility.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| target_node_id | string | yes | Node that receives the gesture |
| gesture_type | GestureType | no | Tap, DoubleTap, LongPress, Drag, Swipe, Pinch, Hover, Focus |
| trigger_event | string | no | State machine event to fire on gesture |
| trigger_state_machine | string | no | Target state machine |
| continuous_property | string | no | Property to update for drag/continuous gestures |
| continuous_axis | string | no | Axis for continuous gestures |
| keyboard_key | string | no | Keyboard equivalent (required by validator) |
| keyboard_modifier | string | no | Modifier key (Shift, Ctrl, Alt, Meta) |
| threshold_px | float32 | no | Gesture distance threshold in pixels |
| velocity_threshold | float32 | no | Gesture velocity threshold |
{
"node_id": "card-tap",
"target_node_id": "card-surface",
"gesture_type": "Tap",
"trigger_event": "select",
"trigger_state_machine": "card-state",
"keyboard_key": "Enter"
}
ScrollBinding
Binds node properties to scroll position. Compiled to CSS scroll-driven animations where supported, with IntersectionObserver fallback.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| target_node_id | string | yes | Node whose properties are scroll-linked |
| scroll_trigger | ScrollTrigger | no | ViewProgress (default) or ScrollProgress |
| scroll_axis | ScrollAxis | no | Vertical (default) or Horizontal |
| range_start | float32 | no | Start of scroll range, 0.0-1.0 (default 0.0) |
| range_end | float32 | no | End of scroll range, 0.0-1.0 (default 1.0) |
| properties | [AnimatedProperty] | yes | Properties to animate over the scroll range |
| scroll_container_id | string | no | Scroll container (default: nearest ancestor) |
| reduced_motion | ReducedMotion | no | Alternative for prefers-reduced-motion |
{
"node_id": "parallax-bg",
"target_node_id": "bg-image",
"scroll_trigger": "ViewProgress",
"properties": [
{ "property": "transform.translateY", "from": "0px", "to": "-50px" }
],
"reduced_motion": { "strategy": "Remove" }
}
PhysicsBody
Attaches physics simulation to a node for spring animations, momentum scrolling, and procedural motion. Non-interruptible springs are compiled to CSS linear() at build time.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| target_node_id | string | yes | Node to apply physics to |
| stiffness | float32 | no | Spring stiffness (default 300) |
| damping | float32 | no | Spring damping (default 25, must be > 0) |
| mass | float32 | no | Spring mass (default 1) |
| friction | float32 | no | Momentum friction, 0-1 (default 0.05) |
| restitution | float32 | no | Bounciness, 0-1 (default 0) |
| interruptible | bool | no | If true, uses rAF instead of CSS (default false) |
{
"node_id": "spring-body",
"target_node_id": "draggable-card",
"stiffness": 400,
"damping": 30,
"mass": 1,
"interruptible": true
}
ReducedMotion
Every animation must reference a ReducedMotion alternative. The validator rejects IR where any AnimationTransition, Sequence, or ScrollBinding lacks one.
| Field | Type | Required | Description |
|---|---|---|---|
| strategy | ReducedMotionStrategy | no | Remove (default), Simplify, ReduceDuration, Functional |
| simplified_properties | [AnimatedProperty] | no | Replacement properties for Simplify strategy |
| reduced_duration | Duration | no | Shortened duration for ReduceDuration strategy |
Strategy values:
- Remove – snap to final state, no animation
- Simplify – replace with a simpler animation (e.g., fade instead of slide)
- ReduceDuration – keep the animation but make it near-instant
- Functional – animation serves a functional purpose (spinner, progress); simplify but do not remove
Navigation & Routing Nodes
Routing in Voce IR is modeled as a state machine where states are views and transitions are navigation events. The schema supports nested routes, guards for auth checks, data preloading, and sitemap generation.
RouteMap
Top-level routing configuration for a multi-route application. Referenced from VoceDocument.routes.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| routes | [RouteEntry] | yes | List of route definitions |
| not_found_route | string | no | Route name or path for 404 pages |
| default_transition | RouteTransitionConfig | no | Default transition animation between routes |
{
"node_id": "app-routes",
"routes": [
{ "path": "/", "name": "home", "view_root_id": "home-root" },
{ "path": "/about", "name": "about", "view_root_id": "about-root" }
],
"not_found_route": "/404"
}
RouteEntry
Defines a single route mapping a URL path to a ViewRoot.
| Field | Type | Required | Description |
|---|---|---|---|
| path | string | yes | URL path pattern (e.g., “/products/:id”) |
| name | string | no | Route name for programmatic navigation |
| view_root_id | string | yes | ViewRoot to render for this route |
| guard | RouteGuard | no | Access control configuration |
| preload_data | [string] | no | DataNode IDs to prefetch on navigation |
| transition | RouteTransitionConfig | no | Transition animation for this route |
| sitemap_priority | float32 | no | Sitemap priority 0.0-1.0 (default 0.5) |
| sitemap_change_freq | ChangeFrequency | no | Always, Hourly, Daily, Weekly, Monthly, Yearly, Never |
| sitemap_last_modified | string | no | Last modification date (ISO 8601) |
| exclude_from_sitemap | bool | no | Exclude from generated sitemap (default false) |
| children | [RouteEntry] | no | Nested child routes |
{
"path": "/dashboard",
"name": "dashboard",
"view_root_id": "dashboard-root",
"guard": {
"requires_auth": true,
"required_roles": ["user"],
"redirect_on_fail": "/login"
},
"preload_data": ["user-data", "dashboard-stats"],
"sitemap_priority": 0.3,
"exclude_from_sitemap": true
}
RouteGuard
Access control for a route. The compiler emits authentication and authorization checks before rendering.
| Field | Type | Required | Description |
|---|---|---|---|
| requires_auth | bool | no | Whether authentication is required (default false) |
| required_roles | [string] | no | Roles the user must have to access the route |
| redirect_on_fail | string | no | Path to redirect to if guard fails |
| custom_guard | string | no | Reference to a ComputeNode for custom logic |
{
"requires_auth": true,
"required_roles": ["admin"],
"redirect_on_fail": "/login"
}
RouteTransitionConfig
Configures the animation played when navigating between routes.
| Field | Type | Required | Description |
|---|---|---|---|
| transition_type | RouteTransitionType | no | None, Crossfade, Slide, SharedElement, Custom |
| duration | Duration | no | Transition duration in ms |
| easing | Easing | no | Timing function |
| slide_direction | SlideDirection | no | Left, Right, Up, Down (for Slide type) |
| shared_elements | [SharedElementPair] | no | Paired elements for SharedElement transitions |
| custom_sequence_id | string | no | Reference to a Sequence node (for Custom type) |
| reduced_motion | ReducedMotion | no | Alternative for prefers-reduced-motion |
SharedElementPair
| Field | Type | Required | Description |
|---|---|---|---|
| transition_name | string | yes | Identifier for the shared transition |
| source_node_id | string | no | Node in the source route |
| target_node_id | string | no | Node in the target route |
{
"transition_type": "Crossfade",
"duration": { "ms": 200 },
"easing": { "easing_type": "CubicBezier", "x1": 0.4, "y1": 0, "x2": 0.2, "y2": 1 },
"reduced_motion": { "strategy": "Remove" }
}
Accessibility & Semantics Nodes
Accessibility in Voce IR is structurally required, not opt-in. Every interactive visual node must reference a SemanticNode. The validator rejects IR where interactive elements lack semantic annotations. Explicit opt-outs (decorative: true, presentation: true) are supported for valid exceptions.
SemanticNode
Parallel semantic tree entry for a visual node. Carries ARIA role, label, relationships, and keyboard focus configuration. Visual nodes reference SemanticNodes via the semantic_node_id field. All SemanticNodes live in a flat list on ViewRoot.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| role | string | yes | ARIA role (e.g., “button”, “heading”, “navigation”, “main”) |
| label | string | no | Accessible label announced by screen readers |
| labelled_by | string | no | Node ID whose content labels this node (aria-labelledby) |
| described_by | string | no | Node ID providing extended description (aria-describedby) |
| controls | string | no | Node ID this element controls (aria-controls) |
| owned_by | string | no | Node ID that owns this element (aria-owns) |
| heading_level | int8 | no | Heading level 1-6, only valid when role=“heading” (default 0) |
| tab_index | int32 | no | Keyboard focus order (-2=unset, -1=programmatic, 0=natural, >0=explicit) |
| hidden | bool | no | Hidden from the accessibility tree (default false) |
| aria_expanded | int8 | no | -1=unset, 0=false, 1=true |
| aria_selected | int8 | no | -1=unset, 0=false, 1=true |
| aria_checked | int8 | no | -1=unset, 0=false, 1=true, 2=mixed |
| aria_disabled | bool | no | Whether element is disabled (default false) |
| aria_required | bool | no | Whether element is required (default false) |
| aria_invalid | bool | no | Whether element has invalid input (default false) |
| aria_value_min | float32 | no | Minimum value for range widgets |
| aria_value_max | float32 | no | Maximum value for range widgets |
| aria_value_now | float32 | no | Current value for range widgets |
| aria_value_text | string | no | Human-readable value for range widgets |
| custom_aria | [KeyValue] | no | Custom ARIA attributes (escape hatch) |
The validator enforces that interactive roles (“button”, “link”, “textbox”, etc.) have either label or labelled_by set.
{
"node_id": "sem-cta-btn",
"role": "button",
"label": "Get started with Voce IR",
"tab_index": 0
}
LiveRegion
Declares a region whose content changes are announced by screen readers. Used for toast notifications, form errors, cart updates, and status messages.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| target_node_id | string | yes | Visual node this live region is attached to |
| politeness | LiveRegionPoliteness | no | Polite (default) or Assertive or Off |
| atomic | bool | no | Announce entire region on change (default false) |
| relevant | LiveRegionRelevant | no | Additions (default), Removals, Text, All |
| role_description | string | no | Descriptive label (e.g., “Shopping cart updates”) |
Politeness values:
- Polite – wait for the user to be idle before announcing
- Assertive – interrupt current speech to announce immediately
- Off – region is silent
{
"node_id": "toast-live",
"target_node_id": "toast-container",
"politeness": "Assertive",
"atomic": true,
"role_description": "Notification"
}
FocusTrap
Constrains keyboard focus to a subtree. Used for modals, drawers, and dialogs. The compiler emits focus management JavaScript.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| container_node_id | string | yes | Container node whose subtree traps focus |
| initial_focus_node_id | string | no | Node to focus on activation (default: first focusable) |
| escape_behavior | FocusTrapEscape | no | CloseOnEscape (default), NoEscape, FireEvent |
| escape_state_machine | string | no | StateMachine for FireEvent escape behavior |
| escape_event | string | no | Event to fire on Escape for FireEvent |
| restore_focus | bool | no | Restore previous focus on deactivation (default true) |
{
"node_id": "modal-trap",
"container_node_id": "modal-container",
"initial_focus_node_id": "modal-close-btn",
"escape_behavior": "CloseOnEscape",
"restore_focus": true
}
ReducedMotion
Every animation in the IR must reference a ReducedMotion alternative. The validator rejects any AnimationTransition, Sequence, or ScrollBinding that lacks one. See the Motion chapter for the full ReducedMotion reference.
Strategy values:
| Strategy | Description |
|---|---|
| Remove | Remove animation entirely, snap to final state |
| Simplify | Replace with a simpler animation (e.g., opacity fade) |
| ReduceDuration | Keep animation but reduce to near-instant duration |
| Functional | Animation is functional (spinner, progress bar); simplify only |
Theming & Personalization Nodes
Design tokens, multi-theme support, responsive breakpoints, and personalization slots. Theme switching is modeled as a state machine transition (e.g., light to dark).
ThemeNode
A named set of design tokens. Multiple themes can coexist (light, dark, high-contrast). Referenced from VoceDocument.theme and VoceDocument.alternate_themes.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| name | string | yes | Theme name (e.g., “light”, “dark”) |
| colors | ColorPalette | no | Color token definitions |
| typography | TypographyScale | no | Typography token definitions |
| spacing | SpacingScale | no | Spacing token definitions |
| shadows | ShadowScale | no | Shadow scale definitions |
| radii | RadiusScale | no | Border radius scale definitions |
| transition_duration | Duration | no | Animation duration when switching to theme |
| transition_easing | Easing | no | Easing function for theme transition |
ColorPalette
Semantic color tokens. Each token is a Color struct (r, g, b, a).
| Field | Type | Description |
|---|---|---|
| primary | Color | Primary brand color |
| primary_foreground | Color | Text on primary |
| secondary | Color | Secondary brand color |
| secondary_foreground | Color | Text on secondary |
| accent | Color | Accent color |
| accent_foreground | Color | Text on accent |
| background | Color | Page background |
| foreground | Color | Default text color |
| surface | Color | Card/surface background |
| surface_foreground | Color | Text on surface |
| muted | Color | Muted/subtle background |
| muted_foreground | Color | Text on muted |
| border_color | Color | Default border color |
| error | Color | Error state color |
| error_foreground | Color | Text on error |
| success | Color | Success state color |
| success_foreground | Color | Text on success |
| warning | Color | Warning state color |
| warning_foreground | Color | Text on warning |
| info | Color | Info state color |
| info_foreground | Color | Text on info |
TypographyScale
| Field | Type | Description |
|---|---|---|
| font_body | FontDefinition | Body text font |
| font_display | FontDefinition | Display/heading font |
| font_mono | FontDefinition | Monospace font |
| size_scale | [Length] | Size steps (xs through 4xl) |
| line_height_scale | [float32] | Line heights matching size_scale indexes |
| letter_spacing | Length | Default letter spacing |
SpacingScale
| Field | Type | Description |
|---|---|---|
| base | Length | Base spacing unit (e.g., 4px) |
| multipliers | [float32] | Multiplier scale (actual spacing = base * multiplier) |
{
"node_id": "theme-light",
"name": "light",
"colors": {
"primary": { "r": 59, "g": 130, "b": 246, "a": 255 },
"primary_foreground": { "r": 255, "g": 255, "b": 255, "a": 255 },
"background": { "r": 255, "g": 255, "b": 255, "a": 255 },
"foreground": { "r": 17, "g": 24, "b": 39, "a": 255 }
},
"spacing": {
"base": { "value": 4, "unit": "Px" },
"multipliers": [0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32]
}
}
PersonalizationSlot
A point in the IR that adapts based on user context: locale, device type, color scheme preference, A/B test cohort, or custom conditions.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| name | string | no | Human-readable name |
| variants | [PersonalizationVariant] | yes | List of conditional variants |
| default_variant_index | int32 | no | Fallback variant index (default 0) |
PersonalizationVariant
| Field | Type | Required | Description |
|---|---|---|---|
| conditions | [PersonalizationCondition] | yes | All conditions must be true to activate |
| show_nodes | [string] | no | Node IDs to show when active |
| hide_nodes | [string] | no | Node IDs to hide when active |
| overrides | [PropertyOverride] | no | Property overrides to apply |
PersonalizationCondition
| Field | Type | Description |
|---|---|---|
| condition_type | PersonalizationConditionType | Locale, DeviceType, ColorScheme, ReducedMotion, HighContrast, Viewport, Custom |
| operator | string | Comparison: “eq”, “neq”, “gt”, “lt”, “gte”, “lte”, “contains” |
| value | string | Value to compare against |
{
"node_id": "mobile-variant",
"name": "mobile-layout",
"variants": [
{
"conditions": [
{ "condition_type": "DeviceType", "operator": "eq", "value": "mobile" }
],
"hide_nodes": ["desktop-sidebar"],
"show_nodes": ["mobile-nav"]
}
],
"default_variant_index": 0
}
ResponsiveRule
Adapts layout based on viewport dimensions using explicit breakpoints with property overrides. Unlike CSS media queries, there is no cascading.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| breakpoints | [Breakpoint] | yes | Breakpoint definitions |
| responsive_overrides | [ResponsiveOverride] | yes | Per-breakpoint property overrides |
Breakpoint
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | yes | Breakpoint name (e.g., “sm”, “md”, “lg”) |
| min_width | Length | yes | Minimum viewport width for this breakpoint |
ResponsiveOverride
| Field | Type | Required | Description |
|---|---|---|---|
| breakpoint_name | string | yes | Which breakpoint this applies at |
| overrides | [PropertyOverride] | yes | Property overrides at this breakpoint |
PropertyOverride
| Field | Type | Required | Description |
|---|---|---|---|
| target_node_id | string | yes | Node to override |
| property | string | yes | Property name to override |
| value | string | yes | New value at this breakpoint |
{
"node_id": "responsive-grid",
"breakpoints": [
{ "name": "sm", "min_width": { "value": 640, "unit": "Px" } },
{ "name": "lg", "min_width": { "value": 1024, "unit": "Px" } }
],
"responsive_overrides": [
{
"breakpoint_name": "sm",
"overrides": [
{ "target_node_id": "content-grid", "property": "grid_columns", "value": "1" }
]
},
{
"breakpoint_name": "lg",
"overrides": [
{ "target_node_id": "content-grid", "property": "grid_columns", "value": "3" }
]
}
]
}
Data & Backend Nodes
The data layer covers mutations (ActionNode), real-time subscriptions (SubscriptionNode), authentication (AuthContextNode), CMS content (ContentSlot), and structured rich text (RichTextNode). Data reads are handled by DataNode in the State chapter.
ActionNode
Declares a server mutation with optimistic update strategy, cache invalidation, and error handling. The compiler emits mutation calls with rollback support.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| name | string | no | Human-readable name |
| source | DataSource | yes | Endpoint configuration |
| method | HttpMethod | no | POST (default), GET, PUT, PATCH, DELETE |
| input_type | string | no | Input type hint for the compiler |
| output_type | string | no | Output type hint for the compiler |
| optimistic | OptimisticConfig | no | Optimistic update behavior |
| invalidates | [string] | no | DataNode IDs to refetch after success |
| invalidate_tags | [string] | no | Cache tags to invalidate |
| error_handling | ErrorHandling | no | Error and retry configuration |
| auth_required | bool | no | Whether authentication is needed (default false) |
| required_roles | [string] | no | Roles required to execute this action |
| csrf_protected | bool | no | CSRF protection enabled (default true) |
| idempotent | bool | no | Whether safe to retry (default false) |
OptimisticConfig
| Field | Type | Required | Description |
|---|---|---|---|
| strategy | OptimisticStrategy | no | None (default), MirrorInput, CustomTransform |
| target_data_node_id | string | no | DataNode to optimistically update |
| rollback | RollbackStrategy | no | Revert (default) or ShowErrorKeepData |
ErrorHandling
| Field | Type | Required | Description |
|---|---|---|---|
| retry | RetryConfig | no | Retry configuration |
| fallback | ErrorFallback | no | ShowToast (default), ShowInlineError, Redirect |
| redirect_path | string | no | Path for Redirect fallback |
{
"node_id": "add-to-cart",
"name": "add-item",
"source": {
"provider": "Rest",
"endpoint": "/api/cart/items"
},
"method": "POST",
"optimistic": {
"strategy": "MirrorInput",
"target_data_node_id": "cart-data",
"rollback": "Revert"
},
"invalidates": ["cart-data"],
"csrf_protected": true
}
SubscriptionNode
Real-time data via WebSocket, Server-Sent Events, or polling. Keeps a DataNode updated with live changes.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| name | string | no | Human-readable name |
| source | DataSource | yes | Endpoint configuration |
| transport | SubscriptionTransport | no | WebSocket (default), ServerSentEvents, Polling |
| channel | string | no | Channel, topic, or table to subscribe to |
| filter | string | no | Filter expression for the subscription |
| update_strategy | UpdateStrategy | no | Replace (default), Merge, Append |
| target_data_node_id | string | yes | DataNode to keep updated |
| connection | ConnectionConfig | no | Reconnection and heartbeat settings |
ConnectionConfig
| Field | Type | Required | Description |
|---|---|---|---|
| reconnect | bool | no | Auto-reconnect on disconnect (default true) |
| reconnect_interval_ms | uint32 | no | Reconnect interval in ms (default 3000) |
| max_retries | int32 | no | Maximum reconnect attempts (default 10) |
| heartbeat_interval_ms | uint32 | no | Heartbeat interval in ms (default 30000) |
{
"node_id": "chat-sub",
"name": "chat-messages",
"source": {
"provider": "Supabase",
"endpoint": "wss://project.supabase.co/realtime/v1"
},
"transport": "WebSocket",
"channel": "messages",
"update_strategy": "Append",
"target_data_node_id": "messages-data"
}
AuthContextNode
Application-level auth configuration. Provider-agnostic; the compiler adapts output to the chosen provider.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| name | string | no | Human-readable name |
| provider | AuthProvider | no | Auth0, Clerk, Supabase, Firebase, NextAuth, Custom |
| session_strategy | SessionStrategy | no | JwtCookie (default), JwtHeader, SessionCookie |
| user_type | string | no | Type hint for the user object shape |
| role_field | string | no | Field path for roles in the user object |
| login_action_id | string | no | ActionNode reference for login |
| logout_action_id | string | no | ActionNode reference for logout |
| refresh_action_id | string | no | ActionNode reference for token refresh |
{
"node_id": "app-auth",
"provider": "Supabase",
"session_strategy": "JwtCookie",
"role_field": "user_metadata.role",
"login_action_id": "login-action",
"logout_action_id": "logout-action"
}
ContentSlot
Declares a CMS content dependency. The cache strategy determines compiler behavior: static content is baked in at build time, ISR adds revalidation, and dynamic content is fetched at runtime.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| content_key | string | yes | Content entry ID or path in the CMS |
| source | ContentSource | no | CMS provider and endpoint configuration |
| fallback | string | no | Fallback content if CMS fetch fails |
| cache_strategy | ContentCacheStrategy | no | Static (default), Isr, Dynamic |
| content_type | ContentType | no | Text (default), RichText, Media, Structured |
| locale | string | no | Locale for this content slot |
{
"node_id": "hero-content",
"content_key": "homepage-hero",
"source": {
"provider": "sanity",
"endpoint": "https://project.api.sanity.io/v2023-01-01"
},
"cache_strategy": "Isr",
"content_type": "RichText"
}
RichTextNode
Structured rich text with paragraphs, headings, lists, images, code blocks, and more. Maps directly from Sanity Portable Text, Contentful Rich Text JSON, and Payload Lexical JSON.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| blocks | [RichTextBlock] | yes | Ordered list of content blocks |
| content_slot | ContentSlot | no | CMS source for the content |
RichTextBlock
| Field | Type | Required | Description |
|---|---|---|---|
| block_type | RichTextBlockType | no | Paragraph, Heading, UnorderedList, OrderedList, ListItem, Image, CodeBlock, Blockquote, Divider, Table, TableRow, TableCell |
| level | int8 | no | Heading level (1-6) or list depth |
| children | [RichTextSpan] | no | Inline text content |
| media_src | string | no | Image source URL (for Image blocks) |
| media_alt | string | no | Image alt text (for Image blocks) |
| code_language | string | no | Language hint (for CodeBlock blocks) |
| rows | [RichTextBlock] | no | Nested rows (for Table blocks) |
RichTextSpan
| Field | Type | Required | Description |
|---|---|---|---|
| text | string | yes | Text content |
| marks | [RichTextMark] | no | Bold, Italic, Underline, Strikethrough, Code, Subscript, Superscript |
| link_url | string | no | URL if this span is a link |
{
"node_id": "article-body",
"blocks": [
{
"block_type": "Heading",
"level": 2,
"children": [{ "text": "Getting Started" }]
},
{
"block_type": "Paragraph",
"children": [
{ "text": "Voce IR uses " },
{ "text": "FlatBuffers", "marks": ["Bold"] },
{ "text": " for zero-copy deserialization." }
]
}
]
}
Forms Nodes
Declarative forms with compiler-generated validation, progressive enhancement, and accessible markup. The compiler emits native <form> elements that work without JavaScript, then layers on client-side validation and enhanced submission handling.
FormNode
Top-level form declaration containing fields, validation, and submission configuration.
| Field | Type | Required | Description |
|---|---|---|---|
| node_id | string | yes | Unique identifier |
| name | string | no | Human-readable name |
| fields | [FormField] | yes | List of form fields |
| field_groups | [FormFieldGroup] | no | Logical groupings of fields (rendered as fieldset) |
| cross_validations | [CrossFieldValidation] | no | Validations spanning multiple fields |
| validation_mode | ValidationMode | no | OnSubmit, OnBlur, OnChange, OnBlurThenChange (default) |
| submission | FormSubmission | yes | Submission handling configuration |
| initial_values_node_id | string | no | DataNode providing initial values (edit forms) |
| autosave | AutosaveConfig | no | Draft persistence configuration |
| semantic_node_id | string | no | Reference to a SemanticNode |
ValidationMode Values
| Value | Description |
|---|---|
| OnSubmit | Validate all fields on submit only |
| OnBlur | Validate each field on blur |
| OnChange | Validate on every keystroke |
| OnBlurThenChange | Validate on blur, then on change after first error (default) |
{
"node_id": "contact-form",
"name": "contact",
"fields": [
{
"name": "email",
"field_type": "Email",
"label": "Email address",
"autocomplete": "Email",
"validations": [
{ "rule_type": "Required", "message": "Email is required" },
{ "rule_type": "Email", "message": "Enter a valid email" }
]
},
{
"name": "message",
"field_type": "Textarea",
"label": "Message",
"validations": [
{ "rule_type": "Required", "message": "Message is required" },
{ "rule_type": "MinLength", "value": "10", "message": "At least 10 characters" }
]
}
],
"validation_mode": "OnBlurThenChange",
"submission": {
"action_node_id": "submit-contact",
"encoding": "Json",
"progressive": true,
"success_redirect": "/thank-you"
}
}
FormField
Individual form field with type, label, validation, and accessibility attributes.
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | yes | Field identifier (form submission key) |
| field_type | FormFieldType | no | Text (default), Email, Password, Number, Tel, Url, Search, Select, MultiSelect, Checkbox, Radio, Textarea, File, Date, Time, DateTimeLocal, Hidden, Color, Range |
| label | string | yes | Visible label (required for accessibility) |
| placeholder | string | no | Placeholder text |
| description | string | no | Help text below the field (aria-describedby) |
| initial_value | string | no | Default value |
| validations | [ValidationRule] | no | Client-side validation rules |
| async_validations | [AsyncValidation] | no | Server-side async validations |
| options | [SelectOption] | no | Options for Select and Radio fields |
| visible_when | string | no | Conditional visibility expression |
| disabled_when | string | no | Conditional disable expression |
| autocomplete | AutocompleteHint | no | Browser autocomplete hint (default Off) |
| accept | string | no | Accepted MIME types for File fields |
| max_file_size | uint32 | no | Max file size in bytes for File fields |
| multiple | bool | no | Allow multiple files (default false) |
| step | float32 | no | Step value for Number and Range fields |
| semantic_node_id | string | no | Reference to a SemanticNode |
AutocompleteHint Values
Off, On, Name, GivenName, FamilyName, Email, Username, NewPassword, CurrentPassword, Tel, StreetAddress, City, State, PostalCode, Country, CreditCardNumber, CreditCardExp, CreditCardCsc
{
"name": "password",
"field_type": "Password",
"label": "Password",
"autocomplete": "NewPassword",
"validations": [
{ "rule_type": "Required", "message": "Password is required" },
{ "rule_type": "MinLength", "value": "8", "message": "At least 8 characters" },
{ "rule_type": "Pattern", "value": "[A-Z]", "message": "Must contain an uppercase letter" }
]
}
ValidationRule
A single client-side validation constraint applied to a FormField.
| Field | Type | Required | Description |
|---|---|---|---|
| rule_type | ValidationType | no | Required, MinLength, MaxLength, Pattern, Min, Max, Email, Url, Custom |
| value | string | no | Parameter for the rule (e.g., “8” for MinLength) |
| message | string | yes | Error message displayed on validation failure |
{ "rule_type": "MaxLength", "value": "500", "message": "Maximum 500 characters" }
FormSubmission
Configures how the form is submitted and what happens on success or failure.
| Field | Type | Required | Description |
|---|---|---|---|
| action_node_id | string | yes | ActionNode that handles submission |
| encoding | FormEncoding | no | UrlEncoded, Multipart, Json (default) |
| progressive | bool | no | Works without JS via native form action (default true) |
| success_event | string | no | StateMachine event to fire on success |
| success_state_machine | string | no | Target StateMachine for success event |
| success_redirect | string | no | Route to navigate to on success |
| error_display | string | no | “field” (map to fields) or “summary” (error list) |
FormFieldGroup
Logical grouping of fields, rendered as <fieldset> with <legend>.
| Field | Type | Required | Description |
|---|---|---|---|
| label | string | yes | Group label (rendered as legend) |
| field_names | [string] | yes | Field names belonging to this group |
| description | string | no | Group description |
CrossFieldValidation
Validation spanning multiple fields (e.g., password confirmation).
| Field | Type | Required | Description |
|---|---|---|---|
| field_names | [string] | yes | Fields involved in this validation |
| expression | string | yes | Expression that must evaluate to true |
| message | string | yes | Error message on failure |
| target_field | string | no | Which field displays the error |
{
"field_names": ["password", "confirm_password"],
"expression": "password == confirm_password",
"message": "Passwords must match",
"target_field": "confirm_password"
}
SEO Nodes
Search engine optimization metadata for Voce IR documents. The compiler emits <head> meta tags, JSON-LD structured data, and generates sitemap.xml and robots.txt. SEO metadata is attached per-page via the metadata field on ViewRoot.
PageMetadata
Per-page SEO configuration. One per ViewRoot. The validator warns if title exceeds 60 characters or description exceeds 160 characters.
| Field | Type | Required | Description |
|---|---|---|---|
| title | string | yes | Page title (validator warns if > 60 chars) |
| title_template | string | no | Title template (e.g., “%s |
| description | string | no | Meta description (validator warns if > 160 chars) |
| canonical_url | string | no | Canonical URL for this page |
| robots | RobotsDirective | no | Robots meta directives |
| open_graph | OpenGraphData | no | Open Graph metadata |
| twitter_card | TwitterCardData | no | Twitter Card metadata |
| alternates | [AlternateLink] | no | Hreflang alternate links for i18n |
| structured_data | [StructuredData] | no | JSON-LD structured data blocks |
| custom_meta | [MetaTag] | no | Custom meta tags (escape hatch) |
{
"title": "Voce IR Documentation",
"title_template": "%s | Voce IR",
"description": "Schema reference and guide for the Voce IR intermediate representation.",
"canonical_url": "https://voce-ir.xyz/docs",
"robots": { "index": true, "follow": true },
"open_graph": {
"title": "Voce IR Documentation",
"description": "Schema reference and guide for the Voce IR intermediate representation.",
"og_type": "Website",
"site_name": "Voce IR"
}
}
OpenGraphData
Open Graph protocol metadata for social sharing previews.
| Field | Type | Required | Description |
|---|---|---|---|
| title | string | no | OG title (falls back to PageMetadata.title) |
| description | string | no | OG description |
| image | string | no | Image URL or MediaNode reference |
| image_alt | string | no | Alt text for the OG image |
| image_width | int32 | no | Image width in pixels |
| image_height | int32 | no | Image height in pixels |
| og_type | OGType | no | Website (default), Article, Product, Profile |
| url | string | no | Canonical page URL |
| site_name | string | no | Site name |
| locale | string | no | Content locale (e.g., “en_US”) |
{
"title": "Introducing Voce IR",
"description": "An AI-native UI intermediate representation.",
"image": "https://voce-ir.xyz/og-image.png",
"image_alt": "Voce IR logo and tagline",
"image_width": 1200,
"image_height": 630,
"og_type": "Website",
"site_name": "Voce IR"
}
StructuredData
JSON-LD structured data block for search engine rich results. The validator checks basic conformance to the declared Schema.org type.
| Field | Type | Required | Description |
|---|---|---|---|
| schema_type | string | yes | Schema.org type (e.g., “Article”, “Product”, “FAQ”, “BreadcrumbList”) |
| properties_json | string | yes | JSON-LD properties as a JSON string |
{
"schema_type": "Article",
"properties_json": "{\"headline\":\"Getting Started with Voce IR\",\"author\":{\"@type\":\"Person\",\"name\":\"Voce Team\"},\"datePublished\":\"2025-01-15\"}"
}
TwitterCardData
Twitter (X) Card metadata for link previews on the platform.
| Field | Type | Required | Description |
|---|---|---|---|
| card_type | TwitterCardType | no | Summary, SummaryLargeImage (default), App, Player |
| title | string | no | Card title |
| description | string | no | Card description |
| image | string | no | Image URL |
| image_alt | string | no | Alt text for the image |
| site | string | no | @username of the site |
| creator | string | no | @username of the content creator |
RobotsDirective
Controls search engine crawling and indexing behavior for the page.
| Field | Type | Required | Description |
|---|---|---|---|
| index | bool | no | Allow indexing (default true) |
| follow | bool | no | Follow links on page (default true) |
| max_snippet | int32 | no | Max snippet length, -1 = unlimited (default) |
| max_image_preview | ImagePreviewSize | no | None, Standard, Large (default) |
| max_video_preview | int32 | no | Max video preview seconds, -1 = unlimited |
| no_archive | bool | no | Prevent cached page (default false) |
| no_translate | bool | no | Prevent translation (default false) |
AlternateLink
Hreflang link for international SEO, connecting pages across locales.
| Field | Type | Required | Description |
|---|---|---|---|
| hreflang | string | yes | BCP 47 language tag (e.g., “en-US”, “x-default”) |
| href | string | yes | URL of the alternate page |
[
{ "hreflang": "en-US", "href": "https://voce-ir.xyz/en/docs" },
{ "hreflang": "fr-FR", "href": "https://voce-ir.xyz/fr/docs" },
{ "hreflang": "x-default", "href": "https://voce-ir.xyz/docs" }
]
MetaTag
Custom meta tag for cases not covered by the structured fields above.
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | yes | Meta tag name or property attribute |
| content | string | yes | Meta tag content value |
| is_property | bool | no | Use property= instead of name= (default false) |
Internationalization Nodes
Voce IR supports two i18n compilation modes. In static mode (default), the compiler resolves all LocalizedStrings against the target locale’s MessageCatalog and emits fully resolved HTML per locale with zero runtime i18n code. In runtime mode, a single output is emitted with locale switching and on-demand translation loading (~1KB runtime).
LocalizedString
A reference to a translated message, used in TextNode content, FormField labels, PageMetadata, and anywhere user-visible text appears. The validator checks that every message_key exists in all declared locale catalogs.
| Field | Type | Required | Description |
|---|---|---|---|
| message_key | string | yes | Message key (e.g., “hero.title”, “form.email.label”) |
| default_value | string | no | Fallback value in the primary language |
| parameters | [MessageParameter] | no | Typed parameters for interpolation |
| description | string | no | Context for translators |
MessageParameter
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | yes | Parameter name (e.g., “count”, “name”) |
| param_type | MessageParamType | no | StringParam (default), NumberParam, DateParam, CurrencyParam, PluralParam, SelectParam |
| format_options | FormatOptions | no | Formatting configuration |
{
"message_key": "cart.item_count",
"default_value": "{count, plural, one {# item} other {# items}} in your cart",
"parameters": [
{ "name": "count", "param_type": "PluralParam" }
],
"description": "Shopping cart item count displayed in the header"
}
MessageCatalog
A collection of translated messages for a single locale. Stored as a companion file alongside the IR, not embedded in the main binary.
| Field | Type | Required | Description |
|---|---|---|---|
| locale | string | yes | BCP 47 locale tag (e.g., “en-US”, “fr-FR”) |
| messages | [Message] | yes | All translated messages for this locale |
| fallback_locale | string | no | Fallback locale (e.g., “en” for “en-US”) |
Message
| Field | Type | Required | Description |
|---|---|---|---|
| key | string | yes | Message key matching LocalizedString.message_key |
| value | string | yes | ICU MessageFormat translated string |
ICU MessageFormat supports plurals, select, and nested expressions:
- Simple:
"Hello {name}" - Plural:
"{count, plural, one {# item} other {# items}}" - Select:
"{gender, select, female {She} male {He} other {They}} commented"
{
"locale": "fr-FR",
"messages": [
{ "key": "hero.title", "value": "Bienvenue sur Voce" },
{ "key": "hero.subtitle", "value": "Representation intermediaire pour l'IA" },
{ "key": "cart.item_count", "value": "{count, plural, one {# article} other {# articles}} dans votre panier" }
],
"fallback_locale": "en"
}
FormatOptions
Locale-aware formatting configuration for number, date, and currency parameters.
| Field | Type | Required | Description |
|---|---|---|---|
| number_style | NumberStyle | no | Decimal (default), Currency, Percent, Unit |
| currency_code | string | no | ISO 4217 code (e.g., “USD”, “EUR”) for Currency style |
| date_style | DateStyle | no | Short, Medium (default), Long, Full, Custom |
| custom_date_pattern | string | no | ICU date pattern for Custom style (e.g., “yyyy-MM-dd”) |
| min_fraction_digits | int32 | no | Minimum fractional digits for numbers |
| max_fraction_digits | int32 | no | Maximum fractional digits for numbers |
{
"number_style": "Currency",
"currency_code": "EUR",
"min_fraction_digits": 2,
"max_fraction_digits": 2
}
I18nConfig
Application-level internationalization configuration. Added to VoceDocument.i18n.
| Field | Type | Required | Description |
|---|---|---|---|
| default_locale | string | yes | Default/primary locale (e.g., “en-US”) |
| supported_locales | [string] | yes | All supported locale tags |
| mode | string | no | “static” (default) or “runtime” |
Compilation modes:
- static – one output per locale. All LocalizedStrings resolved at compile time. Zero runtime cost. Best for SEO and performance.
- runtime – single output with locale switching. Translations loaded on demand. Adds ~1KB of i18n runtime. Best for SPAs with in-app language switching.
{
"default_locale": "en-US",
"supported_locales": ["en-US", "fr-FR", "de-DE", "ja-JP"],
"mode": "static"
}
Pipeline Overview
Voce IR follows a SPIR-V-inspired pipeline: a binary intermediate representation flows through validation and compilation stages before reaching the end user. No human-readable source code exists in the pipeline. The AI generates IR directly, the validator enforces correctness, and the compiler emits optimized output for each target platform.
Pipeline Stages
Natural Language
|
v
+-----------+
| AI Bridge | LLM generates JSON IR from conversation
+-----------+
|
v
+-----------+
| JSON IR | Machine-readable text (.voce.json)
+-----------+
|
v
+-----------+
| Validator | 9 passes, 46 rules — errors block compilation
+-----------+
|
v
+-----------+
| Compiler | 7 targets — DOM, WebGPU, WASM, Hybrid, iOS, Android, Email
+-----------+
|
v
+-----------+
| Deployer | 4 adapters — Static, Cloudflare, Netlify, Vercel
+-----------+
|
v
End User
Stage 1: AI Bridge
The AI bridge (packages/ai-bridge/) is a TypeScript layer that sits between
the LLM and the rest of the pipeline. It manages the conversation, applies
style packs, and produces valid JSON IR. The bridge uses structured output
to ensure the LLM emits well-formed IR conforming to the FlatBuffers schema.
Key responsibilities:
- Conversation management (anti-vibe-coding: the AI asks questions, pushes back)
- Style pack selection and token injection
- Schema-aware JSON generation
- Intent-IR pair matching via RAG
Stage 2: JSON IR
The JSON representation is the canonical text form of the binary IR. It is not
source code – it is a machine-readable serialization used for AI generation,
debugging, and version control diffing. Files use the .voce.json extension.
The voce json2bin command converts JSON to the binary FlatBuffers format
(.voce), and voce bin2json reverses the process.
Stage 3: Validator
The validator (packages/validator/) runs 9 ordered passes over the IR,
checking 46 rules across structural integrity, reference resolution, state
machines, accessibility, security, SEO, forms, internationalization, and
motion safety. Validation errors block compilation entirely – there is no
“build with warnings” mode for critical rules.
Passes execute in dependency order:
- Structural (STR) – document shape, required fields, node nesting
- References (REF) – all ID references resolve to existing nodes
- State Machine (STA) – valid transitions, initial states, no orphans
- Accessibility (A11Y) – keyboard equivalents, heading hierarchy, alt text
- Security (SEC) – CSRF on mutations, auth redirects, HTTPS enforcement
- SEO – title, description, h1 count, Open Graph completeness
- Forms (FRM) – field labels, unique names, validation rules
- Internationalization (I18N) – localized key presence, default values
- Motion (MOT) – ReducedMotion required, physics constraints, duration limits
Stage 4: Compiler
Seven compile targets live in separate crates under packages/. Each compiler
reads validated IR and emits platform-specific output with zero runtime
dependencies:
| Target | Crate | Output |
|---|---|---|
| DOM | compiler-dom | Single-file HTML |
| WebGPU | compiler-webgpu | WGSL shaders + JS harness |
| WASM | compiler-wasm | WAT/WASM modules |
| Hybrid | compiler-hybrid | Per-component target analysis |
| iOS | compiler-ios | SwiftUI views |
| Android | compiler-android | Jetpack Compose functions |
compiler-email | Table-based HTML |
Stage 5: Deploy
Four deploy adapters handle the last mile, packaging compiler output for specific hosting environments:
| Adapter | Crate | Description |
|---|---|---|
| Static | adapter-static | Plain files, any static host |
| Cloudflare | adapter-cloudflare | Cloudflare Workers / Pages |
| Netlify | adapter-netlify | Netlify Functions + deploy config |
| Vercel | adapter-vercel | Vercel serverless + edge config |
Design Principles
- No human-readable code in the pipeline. The IR is the source of truth, not a stepping stone to hand-editable files.
- Accessibility is a compile error. Missing semantic information blocks the build, not just produces a warning.
- Zero runtime dependencies. Compiled output has no npm packages, no CDN links, no framework bundles. This eliminates the supply chain attack surface.
- Binary IR is not human-readable by design. JSON exists for AI generation and debugging, not for human authorship.
IR Format
Voce IR uses FlatBuffers as its binary wire format. FlatBuffers provide zero-copy deserialization, schema evolution, and compact binary encoding – properties borrowed from GPU shader pipelines (SPIR-V) rather than traditional web frameworks.
File Extensions
| Extension | Format | Purpose |
|---|---|---|
.voce | Binary | FlatBuffers binary, used at compile time |
.voce.json | JSON | Canonical text representation for AI, debug |
The two formats are interchangeable. The CLI provides round-trip conversion:
voce json2bin input.voce.json -o output.voce
voce bin2json input.voce -o output.voce.json
Internally, both commands delegate to flatc (the FlatBuffers compiler) with
the Voce schema. The JSON form is what AI models emit; the binary form is what
the validator and compilers consume.
Schema Organization
FlatBuffers schemas live in packages/schema/schemas/. Each .fbs file covers
one domain:
| File | Domain |
|---|---|
types.fbs | Primitive types (RGB, Edge, Dimension, etc.) |
layout.fbs | ViewRoot, Container, Surface, TextNode, MediaNode |
state.fbs | StateMachine, DataNode, ComputeNode, EffectNode, ContextNode |
motion.fbs | AnimationTransition, Sequence, GestureHandler, ScrollBinding, PhysicsBody |
navigation.fbs | RouteMap, RouteEntry, RouteTransition, RouteGuard |
a11y.fbs | SemanticNode, LiveRegion, FocusTrap |
theming.fbs | ThemeNode, ColorPalette, TypographyScale, SpacingScale, PersonalizationSlot, ResponsiveRule |
data.fbs | ActionNode, SubscriptionNode, AuthContextNode, ContentSlot, RichTextNode |
forms.fbs | FormNode, FormField, ValidationRule, FormSubmission |
seo.fbs | PageMetadata, OpenGraphData, StructuredData |
i18n.fbs | LocalizedString, MessageCatalog, FormatOptions, I18nConfig |
voce.fbs | Master file – ChildUnion, ChildNode, VoceDocument |
The master file voce.fbs includes all domain files and defines the document
root. This is the single compilation target for flatc.
The ChildUnion Wrapper Pattern
FlatBuffers unions allow heterogeneous node types in a single tree. However, the Rust codegen does not support vectors of unions directly. Voce solves this with a wrapper table:
union ChildUnion {
Container,
Surface,
TextNode,
MediaNode,
StateMachine,
// ... 27 total node types
FormNode
}
table ChildNode {
value: ChildUnion;
}
Parent nodes store children as [ChildNode] – a vector of wrapper tables,
each containing one union variant. This pattern adds one level of indirection
but preserves type safety and allows the full 27-type union to appear anywhere
in the tree.
VoceDocument Root
Every IR file has a VoceDocument at its root:
table VoceDocument {
schema_version_major: int32 = 0;
schema_version_minor: int32 = 1;
root: ViewRoot (required);
routes: RouteMap;
theme: ThemeNode;
alternate_themes: [ThemeNode];
auth: AuthContextNode;
i18n: I18nConfig;
}
The root field is the visual tree entry point (always a ViewRoot).
Top-level configuration – routing, theming, authentication, and
internationalization – lives alongside the root rather than nested inside it.
The binary file uses the FlatBuffers file identifier "VOCE" (4 bytes at
offset 4), enabling quick format detection without parsing.
Schema Versioning
The schema_version_major and schema_version_minor fields follow semver
conventions:
- Minor bump: New optional fields, new union members. Old validators and compilers can still read the IR (FlatBuffers forward-compatibility).
- Major bump: Removed fields, changed semantics, breaking structural changes. Requires matching validator/compiler versions.
FlatBuffers’ wire format naturally supports forward compatibility – unknown fields are silently ignored by older readers. This means minor version bumps require no coordination between the AI bridge and the compiler.
JSON Canonical Form
The JSON representation mirrors the FlatBuffers schema exactly. Field names
match the schema, enums use string names, and nested tables become nested
objects. This is not a separate format – it is the standard FlatBuffers JSON
encoding produced by flatc --json.
A minimal valid document in JSON:
{
"schema_version_major": 0,
"schema_version_minor": 1,
"root": {
"id": "root",
"children": []
}
}
The JSON form exists for three reasons: AI generation (LLMs produce text, not binary), debugging (humans can inspect the IR during development), and version control (text diffs are meaningful, binary diffs are not). It is never the primary runtime format.
Compiler Architecture
Voce IR supports seven compile targets. Each compiler lives in its own Rust
crate under packages/, reads validated IR, and emits platform-specific output
with zero runtime dependencies. The compiler selection happens at build time –
the same IR can be compiled to any supported target without modification.
Compiler Crates
| Crate | Target | Output Format |
|---|---|---|
compiler-dom | DOM | Single-file HTML + CSS + JS |
compiler-webgpu | WebGPU | WGSL shaders + JS harness |
compiler-wasm | WASM | WAT text format / WASM binary |
compiler-hybrid | Hybrid | Mixed targets per component |
compiler-ios | iOS | SwiftUI view files |
compiler-android | Android | Jetpack Compose Kotlin |
compiler-email | Table-based HTML |
DOM Compiler
The DOM compiler (packages/compiler-dom/) is the primary compile target and
the most mature. It emits a single self-contained HTML file with inlined CSS
and JavaScript. No framework, no bundler, no CDN dependencies.
Internal pipeline stages:
- Lower – Transform IR nodes into a compiler-internal representation
(
compiler_ir.rs) optimized for code generation - Animation – Process motion nodes into CSS keyframes and JS animation code
- Assets – Resolve and inline media references
- Emit – Generate the final HTML string with embedded styles and scripts
The output follows patterns from SolidJS and Svelte compiled output: surgical
DOM mutations rather than virtual DOM diffing. State changes produce direct
element.textContent = value assignments, not tree reconciliation.
WebGPU Compiler
The WebGPU compiler targets GPU-accelerated rendering using the WebGPU API. It produces WGSL (WebGPU Shading Language) shader programs alongside a JavaScript harness that manages the render pipeline.
Key capabilities:
- PBR (Physically Based Rendering) material support
- Scene3D, MeshNode, and ShaderNode compilation
- Particle system emission as compute shaders
- Automatic fallback annotations for non-WebGPU browsers
WASM Compiler
The WASM compiler translates StateMachine and ComputeNode logic into WebAssembly
Text Format (WAT), which can then be assembled into .wasm binaries. This
target is used when state logic needs to run at near-native speed in the browser.
The compiler maps Voce state machines to WASM function tables: each state becomes a function, transitions become conditional branches, and data bindings become memory load/store operations.
Hybrid Compiler
The hybrid compiler performs per-component target analysis. Rather than compiling the entire document to one target, it examines each subtree and selects the optimal compiler:
- Static content with no interactivity routes to DOM (minimal output)
- Heavy animation or 3D content routes to WebGPU
- Complex state logic routes to WASM
- The final output stitches the pieces together with a thin coordination layer
This allows a single page to mix GPU-rendered hero sections with lightweight DOM content sections, optimizing both performance and payload size.
iOS Compiler
The iOS compiler emits SwiftUI view code. IR layout nodes map to SwiftUI’s
VStack, HStack, ZStack, and LazyVGrid. Theming tokens become SwiftUI
Color and Font definitions. Navigation maps to NavigationStack and
NavigationLink.
Accessibility semantics translate directly – Voce’s SemanticNode maps to
SwiftUI’s .accessibilityLabel, .accessibilityHint, and role modifiers.
Android Compiler
The Android compiler targets Jetpack Compose, emitting Kotlin composable
functions. IR containers become Column, Row, and Box composables.
Theming maps to Material 3 MaterialTheme with custom color schemes generated
from the IR’s ThemeNode.
State machines compile to Compose State holders with LaunchedEffect for
side effects, matching the IR’s reactive model.
Email Compiler
The email compiler produces HTML that renders correctly across email clients – a notoriously constrained environment. It uses table-based layouts (not flexbox or grid), inline styles (not CSS classes), and conservative markup that passes Litmus and Email on Acid testing.
Key constraints the compiler handles:
- All layout via nested
<table>elements - All styles inlined on each element
- No JavaScript (email clients strip it)
- Image references as absolute URLs (no inlining)
- MSO conditional comments for Outlook compatibility
Shared Architecture
All seven compilers share common patterns:
- Input: Validated
VoceIr(the serde model, not raw FlatBuffers) - Output: A string or file bundle representing the compiled artifact
- No runtime dependencies: Every compiler produces self-contained output
- Snapshot testing: Compiler output is tested with
instasnapshots to catch regressions - Accessibility preservation: Semantic information from the IR must appear in the compiled output – compilers cannot silently drop it
The compiler selection is exposed through the CLI:
voce compile input.voce.json --target dom -o output.html
voce compile input.voce.json --target ios -o OutputView.swift
voce compile input.voce.json --target email -o newsletter.html
Validation Passes
The Voce validator runs 9 ordered passes over the IR, enforcing 46 rules that span structural correctness, accessibility, security, and more. Validation errors block compilation – there is no way to skip or suppress critical failures. This is by design: accessibility and security are compile errors, not warnings.
The ValidationPass Trait
Every pass implements the ValidationPass trait defined in
packages/validator/src/passes/mod.rs:
#![allow(unused)]
fn main() {
pub trait ValidationPass {
fn name(&self) -> &'static str;
fn run(&self, ir: &VoceIr, index: &NodeIndex, result: &mut ValidationResult);
}
}
The VoceIr is a serde-based IR model, separate from the FlatBuffers generated
types. This decoupling is intentional – the validator works with JSON-
deserialized data, not raw FlatBuffers buffers. The NodeIndex provides
pre-built lookup tables (ID-to-node maps, parent chains) so passes can resolve
references without redundant traversals.
Passes execute in dependency order. Structural checks run first because later passes assume the document shape is valid. Reference resolution runs second because domain passes may follow reference chains.
Error Code Taxonomy
Each rule has a unique error code with a prefix identifying its pass:
STR – Structural (5 rules)
| Code | Rule |
|---|---|
| STR001 | Document must have a root ViewRoot |
| STR002 | All nodes must have a non-empty id field |
| STR003 | Node IDs must be unique within the document |
| STR004 | Children must be valid for their parent node type |
| STR005 | Required fields must be present (per schema) |
REF – References (9 rules)
| Code | Rule |
|---|---|
| REF001 | target_id references must resolve to existing nodes |
| REF002 | State machine initial_state must name a valid state |
| REF003 | Transition targets must name valid states |
| REF004 | Animation target_id must resolve |
| REF005 | Route guard redirect must name a valid route |
| REF006 | Context provider_id must resolve |
| REF007 | Subscription source_id must resolve |
| REF008 | Form field form_id must resolve to a FormNode |
| REF009 | Scroll binding source_id must resolve |
STA – State Machine (4 rules)
| Code | Rule |
|---|---|
| STA001 | State machine must have at least one state |
| STA002 | Initial state must exist in the state list |
| STA003 | All transition targets must be reachable states |
| STA004 | No orphan states (every state reachable from initial) |
A11Y – Accessibility (5 rules)
| Code | Rule |
|---|---|
| A11Y001 | Interactive elements must have keyboard equivalents |
| A11Y002 | Heading levels must not skip (h1 -> h3 without h2) |
| A11Y003 | Images must have alt text (or decorative: true) |
| A11Y004 | Form fields must have associated labels |
| A11Y005 | Focus traps must have an escape mechanism |
SEC – Security (4 rules)
| Code | Rule |
|---|---|
| SEC001 | Mutation actions must include CSRF protection |
| SEC002 | Auth-guarded routes must specify a redirect |
| SEC003 | External URLs must use HTTPS |
| SEC004 | Password fields must have autocomplete attribute |
SEO (7 rules)
| Code | Rule |
|---|---|
| SEO001 | Page must have a <title> (via PageMetadata) |
| SEO002 | Title length must be 10-60 characters |
| SEO003 | Meta description must be present |
| SEO004 | Description length must be 50-160 characters |
| SEO005 | Exactly one h1 heading per page |
| SEO006 | Open Graph data must include title, description, image |
| SEO007 | Structured data must have @type field |
FRM – Forms (4 rules)
| Code | Rule |
|---|---|
| FRM001 | Form must have at least one field |
| FRM002 | Every field must have a label |
| FRM003 | Field names must be unique within their form |
| FRM004 | Email fields must have email validation |
I18N – Internationalization (3 rules)
| Code | Rule |
|---|---|
| I18N001 | Localized string keys must be non-empty |
| I18N002 | Every localized string must have a default value |
| I18N003 | All locales must have consistent key sets |
MOT – Motion (5 rules)
| Code | Rule |
|---|---|
| MOT001 | Animations must specify a ReducedMotion alternative |
| MOT002 | Physics bodies must have damping > 0 |
| MOT003 | Animation duration should not exceed 10 seconds |
| MOT004 | Sequences must have at least one step |
| MOT005 | Gesture handlers must specify a recognized gesture |
Output Formats
The validator reports diagnostics in two formats, controlled by CLI flags:
- Colored terminal output (default) – human-readable with error codes, node paths, and descriptions
- JSON output (
--format json) – machine-readable array of diagnostics for integration with CI pipelines and editor tooling
voce validate my-page.voce.json # colored terminal output
voce validate my-page.voce.json --format json # JSON diagnostics
Serde IR Model
The validator does not read FlatBuffers binary directly. Instead, it
deserializes JSON into a parallel serde-based IR model defined in
packages/validator/src/ir.rs. This model mirrors the FlatBuffers schema
but uses standard Rust types (String, Vec, Option) rather than
FlatBuffers accessors. The separation keeps validation logic clean and
testable without requiring binary serialization in test fixtures.
Style Packs
Style packs are design presets for AI-generated output – think of them as LoRA adapters for UI. Instead of describing colors, fonts, and spacing from scratch in every conversation, you select a style pack and the AI bridge applies its design tokens to the generated IR.
What a Style Pack Contains
Each pack defines a complete visual identity:
- Color palette – background, foreground, primary, surface, muted, and optional accent colors (as RGB values)
- Typography – heading and body font families, sizes, weights, and line height
- Spacing – base unit and a scale array for consistent rhythm
- Border radii – small, medium, and large values for component rounding
- Component patterns – example IR files showing how the pack’s tokens apply to common UI patterns (hero sections, pricing cards, forms)
The type definitions live in packages/ai-bridge/src/packs/types.ts:
export interface StylePack {
id: string;
name: string;
description: string;
tags: string[];
tokens: DesignTokens;
examples: PackExample[];
}
Built-in Packs
Voce ships with three built-in style packs:
minimal-saas
A clean, utilitarian design for SaaS dashboards and landing pages. High contrast, generous whitespace, and a neutral palette with a single accent color. Typography uses a system font stack for fast loading.
Tags: saas, landing, clean, dashboard
editorial
A content-first design for blogs, documentation, and long-form reading. Serif headings, generous line height, narrow content column, and muted colors that keep attention on the text.
Tags: blog, editorial, content, documentation
ecommerce
A conversion-oriented design for product pages and storefronts. Bold primary colors, tight spacing for product grids, prominent call-to-action buttons, and image-heavy layouts.
Tags: ecommerce, product, store, conversion
How the AI Bridge Uses Packs
When a user starts a conversation, the AI bridge can select a style pack based on the user’s description (or the user can request one explicitly). The bridge then:
- Loads the pack’s design tokens
- Injects token values into the IR’s
ThemeNodeduring generation - Uses the pack’s example IR files for RAG (retrieval-augmented generation) matching – if the user asks for a “pricing section,” the bridge retrieves the pack’s pricing example as context for the LLM
The pack’s tags field enables automatic matching. When a user says “build me
a SaaS landing page,” the bridge scores packs by tag overlap and selects the
best fit.
Pack Examples and RAG
Each pack includes PackExample entries:
export interface PackExample {
filename: string;
description: string;
tags: string[];
irJson?: string;
}
The description and tags fields are indexed for similarity search. When
the AI bridge receives a user intent, it retrieves the most relevant examples
from the active pack and includes them as few-shot context for the LLM. This
grounds generation in concrete, validated IR rather than relying solely on
schema knowledge.
Creating a Custom Pack
To add a new style pack:
- Create a new directory under
packages/ai-bridge/src/packs/with your pack ID as the name - Define a
StylePackobject with your design tokens - Add example IR files that demonstrate your pack’s visual language
- Register the pack in the loader (
packages/ai-bridge/src/packs/loader.ts)
The pack’s tokens must use RGB values (0-255 per channel). The spacing scale
is an array of multipliers applied to the base unit – for example, a base of
8 with scale [0.5, 1, 2, 3, 4, 6, 8] produces 4, 8, 16, 24, 32, 48, 64
pixel values.
Packs vs. Themes
Style packs and IR themes (ThemeNode) serve different roles:
- Style packs are an AI bridge concept. They guide generation by providing design tokens and examples. They exist at authoring time.
- Themes are an IR concept. They are embedded in the generated document and survive compilation. They exist at runtime.
The AI bridge translates pack tokens into theme nodes during generation. A
single pack can produce multiple theme variants (light/dark) stored as
alternate_themes in the VoceDocument.
Contributing
This guide covers development setup, code conventions, the working pattern for new features, and the process for adding a new compile target.
Development Setup
Prerequisites
- Rust 1.85+ (edition 2024 support required)
- FlatBuffers compiler (
flatc) for schema regeneration - Node.js 18+ and npm (for the TypeScript AI bridge)
Clone and Build
git clone https://github.com/fireburnsup/voce-ir.git
cd voce-ir
# Build the entire workspace
cargo build --workspace
# Run all tests
cargo test --workspace
# Lint (must pass with zero warnings)
cargo clippy --workspace -- -D warnings
# Format check
cargo fmt --check
If all four commands pass, your environment is ready.
Regenerating FlatBuffers Bindings
After editing any .fbs file in packages/schema/schemas/:
# Rust bindings
flatc --rust -o packages/schema/src/generated/ packages/schema/schemas/voce.fbs
# TypeScript bindings (for AI bridge)
flatc --ts -o packages/ai-bridge/src/generated/ packages/schema/schemas/voce.fbs
Code Conventions
Rust
- Edition 2024 (latest stable). Fall back to edition 2021 per-crate only if a dependency requires it (e.g., FlatBuffers codegen).
- Error handling:
thiserrorfor library error types,anyhowfor CLI entry points. Nounwrap()in library code – propagate errors with?. - CLI arguments:
clapderive API. - Naming:
snake_casefor files and functions,PascalCasefor types and enums. - Documentation: Every public function and type has a
///doc comment. - Formatting:
cargo fmtwith default settings. Run before every commit. - Linting:
cargo clippy -- -D warningsenforces a zero-warnings policy. CI will reject PRs with clippy warnings.
Testing
- Unit tests live in-file under
#[cfg(test)]modules. - Integration tests live in
tests/. - Every new node type needs valid and invalid test IR in
tests/schema/. - Compiler output uses
instasnapshot testing to catch regressions.
Working Pattern
When implementing a new feature, follow this sequence:
-
Schema – If new IR types are needed, add or modify
.fbsfiles inpackages/schema/schemas/. Update theChildUnioninvoce.fbsif adding a new node type. -
Bindings – Regenerate Rust and TypeScript bindings with
flatc. -
Validation – Add a validation pass (or extend an existing one) in
packages/validator/src/passes/. Implement theValidationPasstrait and register the pass inall_passes(). -
Compiler – Add codegen support in the relevant compiler crate(s). At minimum, the DOM compiler (
packages/compiler-dom/) should handle the new node type. -
Tests – Write tests for each layer: schema validity, validator acceptance/rejection, and compiler snapshot output.
-
Verify – Run the full suite before submitting:
cargo test --workspace && cargo clippy --workspace -- -D warnings -
Commit – Use conventional commit messages:
feat(validator): add FRM005 rule for phone field validation fix(compiler-dom): correct heading level output for nested sections
Adding a New Compile Target
To add a new compiler (e.g., Flutter, React Native):
-
Create a new crate:
cargo new --lib packages/compiler-flutter -
Add it to the workspace
Cargo.tomlmembers list. -
Depend on the schema crate for IR types and the validator’s serde IR model for input deserialization.
-
Implement a
compilefunction that accepts validatedVoceIrand returns the target output (string, file bundle, or byte stream). -
Follow the shared compiler patterns:
- Zero runtime dependencies in output
- Accessibility semantics must be preserved (never silently drop them)
- Use
instafor snapshot testing
-
Register the new target in the CLI’s
--targetenum (packages/cli/or the relevant binary crate). -
Add integration tests that compile the reference landing page IR and verify the output.
Pull Request Process
- Fork the repository and create a feature branch.
- Follow the working pattern above.
- Ensure
cargo test --workspaceandcargo clippy --workspace -- -D warningsboth pass. - Write a clear PR description explaining what changed and why.
- Link any related issues.
PRs that introduce new validation rules should include both positive tests (valid IR that passes) and negative tests (invalid IR that triggers the expected error code).
Project Structure Reference
Key directories: packages/schema/ (FlatBuffers schema + bindings),
packages/validator/ (9-pass validator), packages/compiler-*/ (7 compile
targets), packages/ai-bridge/ (TypeScript AI layer), packages/adapter-*/
(4 deploy adapters), tests/ (integration tests), examples/ (reference IR).