Skip to content

Validation and errors

This content is for v0.4. Switch to the latest version for up-to-date documentation.

A project is checked in layers, from its JSON shape to what a frame needs when it draws. This page explains each layer, where to run it, and what its messages look like. After reading it you can catch a bad document before it reaches a render, and read the error when one does.

The JSON Schema (schema/project.v1.schema.json) describes shape only: JSON types, required fields, enumerated and tagged values, and the rejection of unknown fields. A document it rejects is never a valid project. One it accepts may still be invalid.

Use it in your editor or your own tooling. The engine does not need it: it checks shape itself when it parses a document.

A well-shaped document must also make sense. The engine’s model checks ranges, limits, references between items, keyframe order and transition coverage, without opening any media. The same checks run when a document loads (loadProject, Project::from_json) and when you ask for them:

import { validateProject, validateProjectJson } from "alpha2/project";
const problems = validateProject(project); // [] when valid; never throws
if (problems.length > 0) console.error(problems.join("\n"));

validateProject mirrors the engine’s checks in TypeScript, so an editor can reject a document before it loads one. validateProjectJson(text) also reports what only the text shows: size over the limit, repeated fields, integers spelled with a fraction or exponent (1920.0), and nesting deeper than 127 levels. ProjectBuilder.build() from alpha2/builder runs validateProject and throws on a problem.

Messages name the item and, for shape problems, the path to it. A few examples, from validateProject:

Problem Message
A field the schema doesn’t have project.timeline.tracks[1].clips[0] has unknown field "duration"
A wrong type project.settings.width must be integer
A range that ends before it starts project.timeline.tracks[0].clips[0].timeline_range.end must not precede its start
A clip that names a missing source clip forest-clip refers to unknown source beach
Clips that overlap without a transition clips coast-clip and forest-clip overlap without a transition
Keyframes out of order clip coast-clip has invalid visual properties

The engine’s own messages, from loadProject, alpha2-host and Python, say the same things in its words, with the position in the JSON text for a shape problem:

invalid project JSON: unknown field `duration`, expected one of `id`, `source_id`, `timeline_range`, ... at line 1 column 1276
invalid project: clips coast-clip and forest-clip overlap without a covering transition on track photos

Some things can only be checked by the renderer that draws them, so loading a project does not catch them:

  • Effects. The document doesn’t know which effects exist; the renderer’s registry does. A frame that draws an unregistered effect, or an effect parameter outside its definition’s range, fails with EFFECT_ERROR. On the CPU renderer, a runtime WGSL effect fails the same way.
  • Fonts. A project that shows text or captions with no fonts registered fails with MISSING_INPUT: “the project shows text, but no fonts were registered”.
  • Media. renderAt reports the sources it had no frame for (missingSources, missingFrames) instead of failing; an export asks you for them with a waiting ticket. Natively, alpha2-host conform and client.conform check a project’s media without rendering, and an export whose media isn’t ready fails with Alpha2ConformError in Node.js.

A valid project can still hold likely mistakes. The linter reports them without blocking anything:

import init, * as wasm from "alpha2/wasm";
import { lintProject } from "alpha2/project";
await init();
const issues = lintProject(wasm, project); // throws, as loadProject does, if the project is invalid

In Python, alpha2.lint_project(project). Each issue has a kind:

kind Means
unreached_keyframe A keyframe no time its owner is shown at depends on, usually one in the wrong time base.
hidden_clip A clip covered for its whole time by an opaque full-frame clip above it.
short_transition A transition no frame shows: shorter than a frame, or one frame long and off the grid.
off_frame_boundary An edge between frames, whose change shows only at the next frame.
unused_source A source no clip or reachable media node reads.
hdr_source An HDR source in a project whose color pipeline refuses HDR.
empty_subpath, paint_without_geometry Shapes that draw nothing.

For example, a shapes generator keyed past the end of the clip that shows it reports:

{ "kind": "unreached_keyframe", "owner": { "type": "source", "source": "dot" }, "property": "items[0].center", "time": 480000 }

The engine enforces fixed limits on every document: 256 tracks, 4,096 clips per track, 100,000 clips in all, a 64 MiB JSON document, a canvas of at most 16,384 pixels a side and 67,108,864 pixels in all, and more. A document over a limit is refused when it loads, with an error that names the limit. The numbers ship as TypeScript constants in alpha2/project and as Python constants, so you can check a document before you send it. Limits lists every one.