# Hedy Deck 2.0 specification

Hedy Deck 2.0 is a portable, editable presentation project for AI agents and people. The source is structured JSON with stable identifiers. The distributable `.hedydeck` file is a ZIP containing that source and binary assets. [Studio](/studio) accepts a complete JSON document as well as the ZIP file.

This document describes the implemented static profile. It is not a roadmap. Slides may contain text, images, shapes, charts, tables, sanitized SVG and HTML, groups, and reusable components. Named reveal steps produce separate static playback frames. Timed animation, audio, video, interaction, responsive reflow, arbitrary scripts, and external resources are not supported.

## Contents

1. [Authoring workflow](#authoring-workflow)
2. [Document model](#document-model)
3. [Slide and node model](#slide-and-node-model)
4. [Node types](#node-types)
5. [Data bindings](#data-bindings)
6. [Steps and playback](#steps-and-playback)
7. [Assets and packaging](#assets-and-packaging)
8. [Revisions and editing](#revisions-and-editing)
9. [Validation and rendering](#validation-and-rendering)
10. [Capabilities and boundaries](#capabilities-and-boundaries)

## Authoring workflow

Validate source shape against the [JSON Schema](/schemas/hedydeck-v2.schema.json). The schema covers structure; cross-references, component cycles, asset hashes, and browser layout require the application validator.

Start from [the complete minimal JSON example](/examples/hello-v2.json), Studio's starter, or its example project. Read the [agent tools](/docs/agent-tools.md) for exact operation inputs. Create or import the source, read its IDs and revision, apply changes, check the result, inspect previews, export the project, and render it for presentation.

The source project is the editable document. The presentation is generated output with its own identity. Opening a presentation bundle does not reconstruct the source's component structure or editable charts.

Studio also [imports PowerPoint, PDF, and images](/docs/import-files.md) into image-based V2 projects. Imported artwork stays flat, while slide order, titles, notes, and added elements are editable. Export produces the same `.hedydeck` package.

The authoring interface is local to the browser. WebMCP, page JavaScript, and Studio's ordinary text inputs operate on the same stored projects. A separate chat or browser session does not automatically share them. Optional [temporary handoff](/docs/temporary-handoff.md) transfers an explicit source snapshot through server storage; it does not change this file format or synchronize editing sessions.

## Document model

A complete source document is a JSON object with these fields:

| Field | Type | Meaning |
| --- | --- | --- |
| `format` | string | Exactly `hedydeck`. |
| `version` | string | Exactly `2.0`; this is the format version, not an editing revision. |
| `id` | string | Stable document identity. Preserve it through edits; create another ID for a copy. |
| `revision` | integer | Nonnegative safe integer used for compare-and-swap editing. Source may begin at 0; the local service saves new projects at revision 1 or higher. |
| `title` | string | Nonempty deck title. |
| `language` | string | Document language, such as `en` or `en-US`. |
| `stage` | object | Exactly `{ "width": 1920, "height": 1080 }`. All coordinates use CSS pixels. |
| `theme` | object | Background, foreground, accent, muted, body font, and heading font. |
| `slides` | array | Slides in playback order; each has a stable ID. |
| `components` | object | Maps component IDs to arrays of reusable nodes. Use `{}` when empty. |
| `data` | object | Maps dataset IDs to JSON values. Use `{}` when empty. |
| `assets` | object | Maps asset IDs to asset metadata. Use `{}` when empty. |
| `metadata` | object, optional | Author, description, audience, objective, and optional source references. |
| `requires` | string array, optional | Required capabilities. Unknown requirements fail validation. Supported names are `core`, `static`, `text`, `shapes`, `images`, `svg`, `html`, `charts`, `tables`, `data-bindings`, `components`, `reveal-steps`, and `fonts`. |
| `extensions` | object, optional | Additional JSON metadata; storing a value does not enable a renderer feature. |

`theme` requires `background`, `foreground`, `accent`, `muted`, `fontFamily`, and `headingFont`. Colors are CSS color values accepted by the static renderer. Font fields are CSS family lists. For example:

```json
{
  "background": "#0b0d12",
  "foreground": "#faf9f6",
  "accent": "#6c8cff",
  "muted": "#a7afbf",
  "fontFamily": "Arial, sans-serif",
  "headingFont": "Georgia, serif"
}
```

System fonts are convenient but their availability and glyph metrics can differ between computers. Embedded supported fonts improve reproducibility; generated playback images preserve the approved pixels independently of receiver font availability.

Optional `metadata` fields are strings: `author`, `description`, `audience`, and `objective`. `metadata.sources` is an array of `{id,title,url?}`. These references are author-supplied annotations, not verified claims. Metadata URLs are citations, not resources loaded to render the deck.

## Slide and node model

A slide has `id`, `title`, and `nodes`. Optional fields are `notes`, `purpose`, `background`, and `steps`. Notes are plain text. A background overrides the deck background. The title provides the slide's name in navigation; put a visible title node on the slide when the audience should see it.

`nodes` is an array in painting order: later nodes appear above earlier nodes. Each node has:

| Field | Meaning |
| --- | --- |
| `id` | Stable identity for precise edits and diagnostics. |
| `type` | One of the supported node types below. |
| `x`, `y`, `w`, `h` | Position and dimensions on the logical canvas. |
| `name` | Optional descriptive name. |
| `role` | Optional `title`, `body`, `caption`, or `decoration`. |
| `style` | Optional supported visual properties. |
| `visibleIn` | Optional list of this slide's step IDs in which the node is visible. |

IDs must match `^[A-Za-z][A-Za-z0-9_-]{0,95}$`: 1–96 ASCII characters, starting with a letter. Reserved keys `__proto__`, `prototype`, and `constructor` are forbidden throughout the source. Slide IDs are unique within the deck; node IDs are unique across each slide and its nested groups; IDs inside each component definition are unique within that definition. IDs should be concise, descriptive, and stable, such as `checkout`, `headline`, and `comparison-chart`. Reordering slides or nodes is not a reason to rename them. Do not use array indices as persistent identifiers.

Composition is explicit. There is no automatic slide layout or automatic font shrinking. Give each element sufficient space, render, inspect the diagnostic and visual results, and revise the source. Use HTML/CSS within a custom block when a static flex or grid arrangement is useful.

`style` supports:

| Property | Meaning |
| --- | --- |
| `color` | Text foreground color. |
| `fill` | Node fill/background where supported. |
| `stroke`, `strokeWidth` | Outline color and width. |
| `radius` | Rounded corners in CSS pixels. |
| `fontFamily`, `fontSize`, `fontWeight` | Typography; font size is in CSS pixels. |
| `lineHeight` | Unitless line-height multiplier. |
| `align` | `left`, `center`, or `right`. |
| `opacity` | Opacity between 0 and 1. |
| `padding` | Interior padding in CSS pixels. |

The stage and node dimensions include their box. Marking artwork as `decoration` exempts intentional internal content overflow during measurement; it does not permit the node box itself to leave its parent. Coordinates must be nonnegative, width and height must be positive, and every box must fit its parent. Text defaults to 36px, weight 400, and line-height 1.2; title-role nodes default to 88px and weight 600. Titles use the theme heading font, other nodes the body font. Keep ordinary content inside the slide. A useful starting point is 128px safe margins, 64–96px headings, 40px body text, and 28px captions. These are design suggestions, not a forced template.

## Node types

### Text

A `text` node uses `text` or `binding`. Text is plain text; it is escaped, not interpreted as Markdown or HTML. Use newline characters for deliberate line breaks. Custom formatted text can be represented as multiple text nodes or a sanitized HTML block.

```json
{
  "id": "headline",
  "type": "text",
  "role": "title",
  "x": 128, "y": 120, "w": 1664, "h": 180,
  "text": "A faster checkout",
  "style": { "fontSize": 84, "fontWeight": 600 }
}
```

### Image

An `image` node names an `asset` ID and supplies `alt` text. `fit` is `contain` or `cover`. Supply an empty `alt` only for a decoration. Image metadata and bytes must exist and agree. External URLs are not image assets.

### Shape

A `shape` node uses `shape: "rect"`, `"ellipse"`, or `"line"`. Use style fill and stroke properties. Use SVG for detailed paths, arrows, or diagrams that are not naturally represented by these primitives.

### Chart

A `chart` node has a `chart` object:

```json
{
  "type": "bar",
  "labels": ["Before", "After"],
  "series": [
    { "name": "Seconds", "values": [120, 45], "color": "#6c8cff" }
  ]
}
```

Supported chart types are `bar`, `line`, and `donut`. Rendering supports 1–200 labels and 1–12 series. Donut charts require exactly one nonnegative series with a positive total. Bar and line scales include zero. Labels identify categories. Each series has a name, numeric values, and an optional color. Supply finite numeric values and matching label/value counts. The static chart implementation is intentionally bounded; it is not an arbitrary charting library configuration.

To bind a chart to a shared array of records, set `chart.data` to its dataset ID, `chart.labelKey` to the label field, and `chart.valueKey` to the numeric field. Supply `labels` and `series` arrays even for a data-bound chart; empty arrays are valid placeholders when `data` is used. Both `labelKey` and `valueKey` are required for data binding and select direct record fields. The dataset supplies one series, replacing the inline labels and values. Its series name/color can be supplied by the first inline series entry. Keep source data numeric rather than preformatted strings. The renderer resolves the data when rendering.

### Table

A `table` node has `table: { "columns": ["Name", "Value"], "rows": [["Before", 120], ["After", 45]] }`. Rows contain JSON values. Use scalar values for readable cells. Choose dimensions that accommodate the header and every row.

For shared data, `table.data` names an array-of-records dataset and `table.keys` specifies the fields in column order. `columns` supplies the visible column labels, and `keys` is required with one existing key per column. Keep `rows` present, using an empty array when records provide the content. There are at most 50 columns and 1,000 rows; available visual space usually requires far fewer. Tables are static views; they do not provide scrolling or editing inside a slide.

### SVG

An `svg` node has an `svg` string containing static SVG artwork. Use a viewBox appropriate to the artwork and the node's dimensions. Active SVG, event handlers, scripts, foreign objects, and external resources are rejected. The same safety rules apply even when SVG is nested in HTML.

### HTML

An `html` node has an `html` string and optional `css` string. It provides a static composition escape hatch for layouts or typography that need HTML/CSS. The block occupies its node box and must fit it. Ordinary CSS selector rules are scoped to that block. CSS at-rules, fixed/sticky viewport positioning, and animations are unsupported. There are no asset-ID placeholders in HTML: use native image nodes for package assets or permitted inline image data URLs inside custom markup.

The source is checked before it is rendered in an isolated, scriptless frame. No script execution, forms, frames, embedded documents, active SVG, network resources, CSS imports, or authored animation are allowed. The block cannot access the application or register tools. Prefer native nodes when an agent needs precise IDs for each editable item.

### Group and component

A `group` has a `children` array of nodes. Children use coordinates relative to the parent box. Grouping does not automatically resize or scale children. A `component` has a `component` ID referencing one array in the document's `components` map. Component children also use coordinates relative to the instance box, without implicit scaling; size the instance to fit them. Components provide reusable compositions; changing the shared definition changes every instance on the next render. Component references must exist and must not form a cycle.

A component is not a runtime plugin. It cannot execute code, define an external resource, or add a new renderer capability. Use distinct instance IDs so diagnostics and edits can identify a use of the shared artwork.

## Data bindings

A text binding points to a shared dataset and a path:

```json
{
  "data": "metrics",
  "path": "checkout.seconds",
  "format": "number",
  "decimals": 0,
  "prefix": "Checkout: ",
  "suffix": " seconds"
}
```

Supported binding fields are `data`, `path`, `format`, `decimals`, `currency`, `prefix`, and `suffix`. Formats are `text`, `number`, `percent`, and `currency`. Paths use dot-separated object keys or array indices (`rows.0.value`); an empty path selects the whole dataset. Numeric formats require finite numeric values. `decimals` is an integer from 0 to 20; when omitted, formatting uses up to two fractional digits. Percent input is a fraction, so `0.625` displays as `62.5%`. Currency requires an uppercase three-letter code such as `USD`. Number formatting uses the document language. Use a dataset with concrete JSON values. Missing datasets or paths are errors rather than permission to invent a value.

Data bindings and computed display formatting are supported. A formula expression language and dependency-based arithmetic calculations are not implemented. Calculate derived measurements explicitly and store them in the shared dataset; keep their provenance in metadata or notes when useful. Never put executable expressions in a binding.

## Steps and playback

Without `steps`, a slide produces one frame. When `steps` is present, it is an ordered array of `{id,title,notes?}`. Each slide supports 1–100 steps. Slide notes and step notes are joined for that frame. The frame title combines the slide and step titles. It produces exactly one frame per declared step, with no implicit initial frame.

Nodes without `visibleIn` appear in every step. A node with `visibleIn` appears only in the listed steps; referenced IDs must belong to that slide. This describes complete visibility states, not accumulated show/hide commands. Include a node in each step where it should remain visible.

```json
{
  "steps": [
    { "id": "question", "title": "The question" },
    { "id": "answer", "title": "The evidence" }
  ]
}
```

A title can remain visible throughout, while a chart uses `"visibleIn": ["answer"]`. Playback advances between the resulting images using the existing presenter controls. It does not animate the chart into place. The receiver's frame index restores the selected static state on reconnection.

## Assets and packaging

The `.hedydeck` extension identifies the V2 ZIP package. JSON entries use UTF-8. Keep a complete monolithic source document as a `.json` file when exchanging text rather than an archive.

Asset IDs are descriptive keys in `document.assets`; nodes reference the ID, not an arbitrary file path. Asset metadata contains `path`, `mime`, `sha256`, and optional `alt`. Supported binary media types are `image/png`, `image/jpeg`, `image/webp`, and `font/woff2`. The SHA-256 digest is 64 lowercase hexadecimal characters over the asset bytes. A custom WOFF2 asset is registered using its asset ID as its font family; set `fontFamily` or `headingFont` to that ID. The renderer also knows the bundled families `Manrope`, `Fraunces`, and `JetBrains Mono`; it loads their files from the application while authoring. For a package whose original custom typography can be reconstructed without that application resource, include WOFF2 assets explicitly.

The asset bytes must be present when a referenced asset is used. Editing metadata cannot create image or font bytes. Use the asset tool or file import/export machinery to keep bytes and metadata together. Do not place credentials, upload capabilities, room codes, or private connection state in source or asset metadata.

The canonical archive has these entries:

| Entry | Contents |
| --- | --- |
| `deck.json` | Root metadata and references described below. |
| `slides/<slide-id>.json` | One complete slide object per ordered slide. |
| `theme.json` | The complete theme object. |
| `components.json` | The components map, including `{}` when unused. |
| `data.json` | The data map, including `{}` when unused. |
| `assets/...` | Binary bytes at the paths declared in root asset metadata. |

`deck.json` contains the document fields except `slides`, `theme`, `components`, and `data`. In their place it contains:

```json
{
  "slideFiles": ["slides/welcome.json"],
  "themeFile": "theme.json",
  "componentsFile": "components.json",
  "dataFile": "data.json"
}
```

That fragment is not a complete root file: retain `format`, `version`, `id`, `revision`, `title`, `language`, `stage`, `assets`, and any optional document metadata beside those reference fields. `slideFiles` defines playback order. Do not place source `slides` beside `slideFiles` in the canonical package.

The package contains editable source and assets. It does not export trusted playback previews or local undo history. Reopening recompiles source; imported previews are not used as proof of validation.

Import validates asset hashes, package paths, and resource budgets. Archive names are relative portable paths, never absolute paths or `..` traversals. Unknown format versions and unsupported media are rejected. ZIP packaging is not permission to load executable content. Encryption, multi-disk archives, and ZIP64 are unsupported.

### Resource limits

| Resource | Limit |
| --- | --- |
| Source slides | 1–500 |
| Expanded playback frames | At most 500, including every named step |
| Steps per slide | 1–100 when present |
| Source nodes | At most 30,000 |
| JSON nesting | At most 40 levels |
| JSON values | At most 200,000 |
| Assets | At most 1,000 |
| Image dimensions | At most 16,384 pixels per side and 40 million pixels total |
| Expanded WOFF2 font | At most 32 MiB |
| ZIP entries | At most 2,000 |
| One archive entry | At most 32 MiB uncompressed |
| ZIP input and aggregate expanded contents | At most 128 MiB each |
| Operations in a batch | At most 1,000 |
| Compiled HTML per frame | At most 20 MiB |
| Compiled HTML across all frames | At most 100 MiB |
| Expanded rendered nodes across frames | At most 30,000 |

Large assets repeated across reveal frames may hit compiled-output limits before the ZIP limit. Source node nesting and component expansion are bounded further; keep structures shallow. The renderer limits expanded group/component nesting to 16 levels. Limits are upper bounds, not a guarantee that a phone can comfortably render the largest possible deck.

## Revisions and editing

Source projects have a stable document ID and increasing local revisions. A newly saved local project has revision 1 or higher. The source fingerprint uses canonical JSON and verified asset hashes, excluding the local revision number; a revision-only change does not alter visible-content identity. Generated playback bundles have a content-derived ID. Do not use a playback `deckId` as the Studio source `id`.

Mutations supply the current `expectedRevision`. A mismatch is a conflict: read the current project, reconcile the requested changes, and submit against the new revision. Never retry against an invented revision. A batch is applied to a copy, checked, and committed atomically, so an invalid node or broken reference cannot leave half of the changes applied.

Supported editing operations are:

| Operation | Fields | Purpose |
| --- | --- | --- |
| `setDeck` | `fields` | Set deck title, language, theme, or metadata. |
| `putSlide` | `slide`, optional `after` | Insert or replace a complete slide by stable ID. |
| `removeSlide` | `slideId` | Remove a slide. |
| `moveSlide` | `slideId`, `after` | Move an existing slide. |
| `putNode` | `slideId`, `node`, optional `parentId` | Insert or replace a complete node. |
| `removeNode` | `slideId`, `nodeId` | Remove a node. |
| `setData` | `id`, `value` | Set a shared JSON dataset. |
| `setComponent` | `id`, `nodes` | Set a reusable composition. |

For slide insertion, `after: null` means first; an omitted `after` appends a new slide or keeps an existing slide in its position. A string names the slide to follow. `moveSlide` requires `after`. A slide cannot follow itself. `putNode` replaces an existing matching node in place, or appends a new node to the slide or named group. `parentId` must name a group. Component definition children are changed through `setComponent`, not by treating expanded instance paths as source IDs.

Replacement operations take a complete slide or node, not a partial field patch. Read it first, preserve the fields you intend to keep, and submit the revised object. An edit cannot smuggle arbitrary property paths into application objects.

An optional `requestId` makes a mutation retry idempotent within retained request history: repeat the same request ID with exactly the same input. Reusing that ID for a different mutation is an error. Undo creates a new current revision from the most recent retained local snapshot. Up to 30 prior edit snapshots and 64 keyed mutation results are retained; there is no exposed redo operation. Exported files do not carry the browser's undo history.

## Validation and rendering

Validation reports diagnostics with `severity`, `code`, `message`, and `fix`, plus `slideId`, `nodeId`, or `path` where relevant. Structural validation checks source, IDs, references, capabilities, and asset metadata. Browser checks also measure rendered content and decode fonts and images. An error must be corrected before a valid presentation build can be produced. Warnings require review but are not proof of failure.

Render a preview and inspect it. Automated checks cannot establish that a claim is accurate or that a composition communicates well. Deliberate overlaps and visual hierarchy need judgment; inspect charts, line breaks, contrast, crops, and each named step.

Rendering generates a source fingerprint and a separate image presentation bundle. A source edit makes an older build stale; render again before presenting the revision. The generated presentation uses the existing HPD1 transport and receiver controls. Its pixels are static and do not require an LLM at playback time.

The implementation uses browser text and image rendering. Source re-rendering can vary across browser/font environments; exporting an image playback build freezes the rendered result. Do not claim byte-identical re-rendering across arbitrary browsers.

## Capabilities and boundaries

- The fixed stage is 1920×1080. Alternate phone/handout layouts are not part of this implementation.
- Timed animation, audio/video, interactive widgets, executable plugins, and arbitrary formulas are unsupported.
- A deck may store descriptive metadata and extension values; neither can enable unsupported execution.
- V2 source is saved locally in a separate database from existing presentation bundles. Export source for durable backup. The optional temporary handoff uploads an editable snapshot, including assets and notes, for an expiring cross-browser link; its 25 MiB transfer cap does not reduce the 128 MiB local package limit.
- Speaker notes are included in the current playback bundle sent to receivers. They are not a private channel. V2 export is an editable project, not a special notes-stripping audience format.
- PDF/PPTX input remains available through existing imports. Editable V2-to-PPTX export and a dedicated PDF export are not supplied by the V2 tools.
- The old HTML validator, temporary dropbox, and fragment-link workflows do not accept V2 ZIP source. Use Studio or the V2 tools.
- There is no V2 remote REST editing API, separate MCP server, or cross-device paired Studio session. WebMCP and the JavaScript/form alternatives edit the current browser's source. The public temporary handoff HTTP API transfers complete files, not edit commands.

See [agent tools](/docs/agent-tools.md) for exact inputs and [handoff](/docs/agent-handoff.md) for supported ways to move a project between an agent, a browser, and a person.
