import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/core")

# @json-render/core

Core types, schemas, and utilities.

## defineCatalog

Creates a type-safe catalog definition with schema validation.

```typescript
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react/schema';

function defineCatalog<T extends ZodType>(
  s: T,
  config: CatalogConfig
): Catalog

// Use the React schema for standard UI specs
const catalog = defineCatalog(schema, {
  components: {...},
  actions: {...},
});
```

### CatalogConfig

```typescript
interface CatalogConfig {
  components: Record<string, ComponentDefinition>;
  actions?: Record<string, ActionDefinition>;
  functions?: Record<string, FunctionDefinition>;
}

interface ComponentDefinition {
  props: ZodObject;         // Use .nullable() for optional props
  slots?: string[];         // Named slots (e.g., ["default"])
  description?: string;     // Help AI understand usage
}

interface ActionDefinition {
  params?: ZodObject;
  description?: string;
}

interface FunctionDefinition {
  description?: string;
}
```

### Catalog Instance

The returned catalog provides methods for AI prompt generation, validation, and schema export:

```typescript
interface Catalog {
  // Data
  readonly data: CatalogConfig;         // The catalog configuration
  readonly componentNames: string[];    // List of component names
  readonly actionNames: string[];       // List of action names

  // AI Prompt Generation
  prompt(options?: PromptOptions): string;

  // Validation
  validate(spec: unknown): SpecValidationResult;
  zodSchema(): z.ZodType;               // Get the Zod schema for specs

  // Export
  jsonSchema(): object;                 // Export as JSON Schema
}

interface PromptOptions {
  system?: string;           // Custom system message intro
  customRules?: string[];    // Additional rules to append
  mode?: "standalone" | "inline" | "generate" | "chat"; // Output mode (default: "standalone")
}

interface SpecValidationResult<T> {
  success: boolean;
  data?: T;               // Validated spec (if success)
  error?: z.ZodError;     // Validation errors (if failed)
}
```

### Catalog Methods

```typescript
// Generate AI system prompt
const systemPrompt = catalog.prompt({
  customRules: ["Always use Card as root element"],
});

// Validate a spec from AI
const result = catalog.validate(aiOutput);
if (result.success) {
  render(result.data);
} else {
  console.error(result.error);
}

// Get Zod schema for custom validation
const schema = catalog.zodSchema();
const parsed = schema.safeParse(aiOutput);

// Export as JSON Schema (for structured outputs)
const jsonSchema = catalog.jsonSchema();
```

## Schema System

json-render uses a flexible schema system that defines both the AI output format (spec) and what catalogs must provide. Each renderer package provides its own schema (e.g., @json-render/react exports `schema`).

### schema

The schema for flat UI element trees. This is exported from @json-render/react.

```typescript
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react/schema';

// schema defines:
// - Spec shape: { root: string, elements: Record<string, UIElement> }
// - Catalog shape: { components: {...}, actions: {...} }

const catalog = defineCatalog(schema, {
  components: {
    Card: {
      props: z.object({ title: z.string() }),
      slots: ["default"],
      description: "Container card",
    },
  },
  actions: {
    submit: {
      params: z.object({ formId: z.string() }),
      description: "Submit a form",
    },
  },
});
```

### SchemaOptions

When creating schemas with `defineSchema`, you can pass options:

```typescript
interface SchemaOptions {
  promptTemplate?: PromptTemplate;  // Custom AI prompt generator
  defaultRules?: string[];          // Default rules injected before custom rules in prompts
  builtInActions?: BuiltInAction[]; // Actions always available at runtime, auto-injected into prompts
}

interface BuiltInAction {
  name: string;        // Action name (e.g. "setState")
  description: string; // Human-readable description for the LLM
}
```

Built-in actions are injected into prompts as `[built-in]` and are handled by the runtime (e.g. `ActionProvider`) without requiring handlers in `defineRegistry`. The React schema declares `setState`, `pushState`, and `removeState` as built-in.

### defineSchema

Create custom schemas for different output formats (e.g., page-based, block-based).

```typescript
import { defineSchema } from '@json-render/core';

const mySchema = defineSchema((s) => ({
  // What the AI outputs (spec)
  spec: s.object({
    title: s.string(),
    blocks: s.array(s.object({
      type: s.ref("catalog.blocks"),
      content: s.any(),
    })),
  }),

  // What the catalog must provide
  catalog: s.object({
    blocks: s.map({
      props: s.zod(),
      description: s.string(),
    }),
  }),
}));
```

### Schema Builder API

The schema builder provides these methods:

```typescript
// Primitive types
s.string()           // String value
s.number()           // Number value
s.boolean()          // Boolean value
s.any()              // Any value

// Compound types
s.array(item)        // Array of items
s.object({ ... })    // Object with shape
s.record(value)      // Record/map with value type

// Catalog references (for type safety)
s.ref("catalog.components")      // Reference to catalog key (becomes enum)
s.propsOf("catalog.components")  // Props schema from catalog entry

// Catalog definitions
s.map({ props: s.zod(), ... })   // Map of named entries with shared shape
s.zod()                          // Placeholder for user-provided Zod schema

// Modifiers
s.optional()         // Mark field as optional
```

## Zod Schemas

Pre-built Zod schemas for common json-render types:

### Dynamic Value Schemas

```typescript
import {
  DynamicValueSchema,    // string | number | boolean | null | { $state: string }
  DynamicStringSchema,   // string | { $state: string }
  DynamicNumberSchema,   // number | { $state: string }
  DynamicBooleanSchema,  // boolean | { $state: string }
} from '@json-render/core';

// Dynamic values can be literals or state path references
type DynamicValue<T> = T | { $state: string };

// Example: a prop that can be a literal or bound to state
const schema = z.object({
  label: DynamicStringSchema,  // "Hello" or { $state: "/user/name" }
});
```

### Visibility Schemas

```typescript
import { VisibilityConditionSchema } from '@json-render/core';

// Use in component props that need conditional rendering
const schema = z.object({
  visible: VisibilityConditionSchema.optional(),
});
```

### Action Schemas

```typescript
import {
  ActionSchema,           // Full action definition
  ActionConfirmSchema,    // Confirmation dialog config
  ActionOnSuccessSchema,  // Success handler config
  ActionOnErrorSchema,    // Error handler config
} from '@json-render/core';
```

### Validation Schemas

```typescript
import {
  ValidationCheckSchema,   // Single validation check
  ValidationConfigSchema,  // Full validation config with checks array
} from '@json-render/core';
```

## SpecStream

SpecStream is json-render's streaming format for progressively building specs from JSONL patches.

### createSpecStreamCompiler

Create a streaming compiler that incrementally builds a spec:

```typescript
import { createSpecStreamCompiler } from '@json-render/core';

const compiler = createSpecStreamCompiler<MySpec>();

// Process streaming chunks
const { result, newPatches } = compiler.push(chunk);

// Get final result
const spec = compiler.getResult();

// Reset for reuse
compiler.reset();
```

### compileSpecStream

Compile an entire SpecStream string at once:

```typescript
import { compileSpecStream } from '@json-render/core';

const jsonl = `{"op":"add","path":"/root","value":{}}
{"op":"add","path":"/root/type","value":"Card"}`;

const spec = compileSpecStream<MySpec>(jsonl);
```

### Low-Level Utilities

```typescript
import {
  parseSpecStreamLine,
  applySpecStreamPatch,
} from '@json-render/core';

// Parse a single line
const patch = parseSpecStreamLine('{"op":"add","path":"/root","value":{}}');

// Apply patch to object (mutates in place)
const obj = {};
applySpecStreamPatch(obj, patch);
```

### applySpecPatch

Apply a single SpecStream patch to a Spec object (mutates in place, returns the spec):

```typescript
import { applySpecPatch } from '@json-render/core';

let spec: Spec = { root: "", elements: {} };
applySpecPatch(spec, { op: "add", path: "/root", value: "main" });

// For React state updates, spread to create a new reference:
setSpec({ ...applySpecPatch(spec, patch) });
```

### nestedToFlat

Convert a nested element tree (with inline children) into the flat `Spec` format:

```typescript
import { nestedToFlat } from '@json-render/core';

const flat = nestedToFlat({
  type: "Card",
  props: { title: "Hello" },
  children: [
    { type: "Text", props: { content: "World" }, children: [] }
  ],
});
// { root: "el-0", elements: { "el-0": ..., "el-1": ... } }
```

### createJsonRenderTransform

Low-level `TransformStream` that separates text from JSONL patches in a mixed AI stream. Lines that parse as JSONL patches are emitted as `data-spec` parts; everything else passes through as text.

The transform properly splits text blocks around spec data by emitting `text-end`/`text-start` pairs, ensuring the AI SDK creates separate text parts and preserving correct interleaving of prose and UI in `message.parts`.

```typescript
import { createJsonRenderTransform } from '@json-render/core';

const transform = createJsonRenderTransform();
// Use with ReadableStream.pipeThrough(transform) for custom pipelines
```

Most users should use `pipeJsonRender()` instead, which wraps this transform for the common AI SDK use case.

### createMixedStreamParser

Parse a mixed stream of text and JSONL patches (used for Inline mode):

```typescript
import { createMixedStreamParser } from '@json-render/core';

const parser = createMixedStreamParser({
  onText: (text) => appendToMessage(text),
  onPatch: (patch) => applySpecPatch(spec, patch),
});

// As chunks arrive from the stream:
for await (const chunk of stream) {
  parser.push(chunk);
}
parser.flush();
```

### pipeJsonRender

Pipe an AI SDK `UIMessageStream` through the json-render transform. Lines that parse as JSONL patches are emitted as `data-spec` parts; everything else passes through as text. Used in Inline mode API routes.

```typescript
import { pipeJsonRender } from '@json-render/core';
import { createUIMessageStream, createUIMessageStreamResponse } from 'ai';

const stream = createUIMessageStream({
  execute: async ({ writer }) => {
    writer.merge(pipeJsonRender(result.toUIMessageStream()));
  },
});
return createUIMessageStreamResponse({ stream });
```

See [Generation Modes](/docs/generation-modes) for full Inline mode setup.

### SpecStream Types

Fully compliant with [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902):

```typescript
interface SpecStreamLine {
  op: 'add' | 'remove' | 'replace' | 'move' | 'copy' | 'test';
  path: string;
  value?: unknown;  // Required for add, replace, test
  from?: string;    // Required for move, copy
}

interface SpecStreamCompiler<T> {
  push(chunk: string): { result: T; newPatches: SpecStreamLine[] };
  getResult(): T;
  getPatches(): SpecStreamLine[];
  reset(): void;
}

interface MixedStreamCallbacks {
  onText: (text: string) => void;
  onPatch: (patch: SpecStreamLine) => void;
}

interface MixedStreamParser {
  push(chunk: string): void;
  flush(): void;
}
```

## Utility Functions

### Path Utilities

```typescript
import { getByPath, setByPath } from '@json-render/core';

// Get value by JSON Pointer path
const value = getByPath(state, '/user/name');  // "Alice"

// Set value by path (mutates object)
setByPath(state, '/user/email', 'alice@example.com');
```

### resolveDynamicValue

```typescript
import { resolveDynamicValue } from '@json-render/core';

// Resolve a dynamic value against state
const name = resolveDynamicValue("Hello", state);        // "Hello"
const name2 = resolveDynamicValue({ $state: "/user/name" }, state);  // "Alice"
```

### findFormValue

```typescript
import { findFormValue } from '@json-render/core';

// Find form values regardless of path format
// Checks: params.name, params["form.name"], state["form.name"], state.form.name
const value = findFormValue("name", params, state);
```

## buildUserPrompt

Build structured user prompts for AI generation, with support for refinement and state context.

```typescript
import { buildUserPrompt } from '@json-render/core';

function buildUserPrompt(options: UserPromptOptions): string

interface UserPromptOptions {
  prompt: string;                        // The user's text prompt
  currentSpec?: Spec | null;             // Existing spec to refine (triggers patch-only mode)
  state?: Record<string, unknown> | null; // Runtime state context to include
  maxPromptLength?: number;              // Max length for user text (truncates before wrapping)
}
```

### Fresh generation

```typescript
const userPrompt = buildUserPrompt({ prompt: "create a todo app" });
```

### Refinement (patch-only mode)

When `currentSpec` is provided, the prompt instructs the AI to output only the patches needed for the change, not recreate the entire spec:

```typescript
const userPrompt = buildUserPrompt({
  prompt: "add a dark mode toggle",
  currentSpec: existingSpec,
});
```

### With state context

Include runtime state so the AI knows what data is available:

```typescript
const userPrompt = buildUserPrompt({
  prompt: "show my data",
  state: { todos: [{ text: "Buy milk" }] },
});
```

## evaluateVisibility

Evaluates a visibility condition against the state model.

```typescript
function evaluateVisibility(
  condition: VisibilityCondition | undefined,
  ctx: VisibilityContext
): boolean

interface VisibilityContext {
  stateModel: StateModel;
  repeatItem?: unknown;    // Current repeat item (inside repeat scope)
  repeatIndex?: number;    // Current repeat array index (inside repeat scope)
}

type VisibilityCondition =
  | { $state: string }                                    // truthiness
  | { $state: string; not: true }                         // falsy
  | { $state: string; eq: unknown }                       // equality
  | { $state: string; neq: unknown }                      // inequality
  | { $state: string; gt: number }                        // greater than
  | { $state: string; gte: number }                       // gte
  | { $state: string; lt: number }                        // lt
  | { $state: string; lte: number }                       // lte
  | { $item: string }                                     // item field (repeat scope)
  | { $item: string; eq: unknown }                        // item field equality
  | { $index: true }                                      // index truthiness (repeat scope)
  | { $index: true; gt: number }                          // index comparison
  | VisibilityCondition[]                                 // implicit AND
  | { $and: VisibilityCondition[] }                       // explicit AND
  | { $or: VisibilityCondition[] }                        // OR
  | boolean;                                              // always / never
```

## Types

### UIElement

```typescript
interface UIElement {
  type: string;
  props: Record<string, unknown>;
  children?: string[];          // Keys of child elements
  visible?: VisibilityCondition;
  on?: Record<string, ActionBinding | ActionBinding[]>;  // Event bindings
  repeat?: { statePath: string; key?: string };           // Repeat for arrays
}
```

Elements are stored in the `elements` map keyed by string IDs. The key comes from the map, not from the element itself.

### Spec (Element Tree)

```typescript
interface Spec {
  root: string | null;                        // Key of root element
  elements: Record<string, UIElement>;        // Flat element map
  state?: Record<string, unknown>;            // Initial state model
}
```

Elements are stored as a flat map with string keys. The tree structure is built by following the `children` arrays.

### ActionBinding

```typescript
interface ActionBinding {
  action: string;
  params?: Record<string, DynamicValue>;
  confirm?: {
    title: string;
    message: string;
    variant?: 'default' | 'danger';
  };
  onSuccess?: { set: Record<string, unknown> };
  onError?: { set: Record<string, unknown> };
  preventDefault?: boolean;  // Prevent default browser behavior (e.g. navigation on links)
}
```

### ValidationSchema

```typescript
interface ValidationSchema {
  checks: ValidationCheck[];
  validateOn?: 'change' | 'blur' | 'submit';
}

interface ValidationCheck {
  type: string;
  args?: Record<string, unknown>;
  message: string;
}
```
