OverviewVersioning

Versioning

How PanelWave manifests declare their format version, what the version numbers mean, and how to upgrade older manifests with the CLI.

Every manifest declares which format version it was written for in the required top-level panelwave header. Consumers use it to decide how to interpret the document and whether an upgrade is needed.

The panelwave header (PanelwaveHeader)

PropertyTypeRequiredConstraintsDescription
versionstringYesPattern ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-.]+)?$Semantic format version the manifest targets, e.g. "1.1.0". Pre-release suffixes (1.2.0-beta.1) are allowed
schemastring (Uri)YesAbsolute URIURL of the schema the manifest validates against
generatorstring——Free-form tool identifier that produced the manifest, e.g. "panelwave-cms/2.3"
{
  "panelwave": {
    "version": "1.7.0",
    "schema": "https://panelwave.org/schema/1.0/panelwave.schema.json",
    "generator": "panelwave-cms/1.0"
  }
}

Version semantics

The format follows semantic versioning:

SegmentExampleMeaning
Patch (1.0.x)1.0.1Clarifications and non-functional changes — no structural impact
Minor (1.x.0)1.1.0Backward-compatible additions — new optional fields and enum values
Major (x.0.0)2.0.0Breaking changes — requires migration

One schema file per major version

The schema file lives in a directory named after the major version. Minor versions are additive and ship in place within that directory:

  • Current format version: 1.7.0
  • Schema location: 1.0/panelwave.schema.json in the panelwave/schema repository
  • Schema $id: https://panelwave.org/schema/1.0/panelwave.schema.json (unchanged by minor releases; an identifier — the URL is not publicly served yet)

This is why a 1.7.0 manifest still points its schema URI at the 1.0/ path — the 1.1/1.2/1.3/1.4/1.5/1.6/1.7 additions were merged into the same schema file, and every valid 1.0 manifest remains valid against it.

Current releases

The format and the open-source packages that read and write it, as of 2026-10-04:

ReleaseCurrent versionFormatNotes
Format / schema (panelwave/schema)1.7.0—1.0/panelwave.schema.json, CC BY 4.0
@panelwave/cli1.2.0bundles 1.7.0Validates offline and upgrades to 1.7.0. CLI 1.1.0 bundled 1.6.0, CLI 1.0.0 1.5.0. See CLI
@panelwave/types1.2.01.7.0TypeScript interfaces for the manifest, including the 1.7.0 fields
@panelwave/player1.2.0up to 1.6, plus Edge.labelChapters without edges, x-locked panels, Hotspot.display and textColor arrive with the release after 1.2.0 (already live on read.panelwave.org and in the CMS preview). See Player
@panelwave/mcp0.1.0—Local bridge to the PanelWave MCP server for desktop AI assistants. See Local bridge

All packages are MIT-licensed and published from github.com/panelwave with npm provenance. The CMS exports and publishes 1.7.0 and imports formats 1.0–1.7.

Compatibility rules as encoded in the schema

Backward compatibility (old manifests, new schema). Minor releases only add optional properties and new enum values. A manifest written for 1.0.0 validates unchanged against the current schema. Example: schema 1.1 added playMode/startMode to VideoLayer, but the legacy 1.0 fields autoplay and loop remain valid and are not deprecated — consumers map them at read time (loop: true → playMode: "loop" and autoplay → startMode only when the new field is absent; a present new field always wins). See Video for the full mapping.

Forward compatibility (new manifests, old consumers). The schema is strict about unknown properties (additionalProperties: false / unevaluatedProperties: false on most objects), so a 1.0-era validator will reject fields introduced later. Custom data that must survive round-trips belongs in x- prefixed extension fields instead.

Deprecation policy. Deprecated fields are marked with "deprecated": true in the schema and remain supported for at least one major version.

What changed in 1.7.0

Relaxing and backward-compatible with 1.6.0 — existing manifests remain valid unchanged.

  • Graph.edges may be empty. The minItems: 1 constraint is gone; the property itself is still required ("edges": []). A chapter without edges — typically a single-panel chapter — follows its reading order. Graph.
  • x- extension fields on panels and extras blocks. Panel and ExtraBlock (and with it ExtraCharacterSheet) accept ^x- properties with any value, in addition to the manifest root. Motivating use: server-side paywall views mark stripped panels "x-locked": true. Extensions.

Four additive fields were folded into 1.7.0 on 2026-10-04, before its tooling release — all optional, so existing manifests stay valid:

  • Edge.label (LocalizedString): the reader-facing choice text of a path, shown on the branch chooser's buttons. Graph.
  • Hotspot.display (auto | button | area, default auto): whether readers see the hotspot's label as a button or an invisible click area over the artwork. auto shows a button when the panel's goTo hotspots lead to two or more panels (a choice). Hotspots.
  • BalloonConfig.textColor (hex color, default #000000; also on BalloonConfigOverride): the lettering color, e.g. amber on-screen text on a dark fill. Speech Bubbles.
  • Variable ids may use underscores (trust_jonas): the pattern is now ^[a-zA-Z0-9][a-zA-Z0-9_]*(?:\.[a-zA-Z0-9_-]+)*$. Variables.

@panelwave/cli 1.2.0 and @panelwave/types 1.2.0 (released 2026-10-04) bundle and type 1.7.0. A validator with an older schema copy (CLI 1.1.0 or earlier) rejects empty edges, x- fields on panels and the four fields above — update with npm install -g @panelwave/cli@latest.

What changed in 1.6.0

Additive; existing manifests remain valid unchanged. A purchase paywall rule can list every product that unlocks it, and the manifest can describe those products for readers:

  • PaywallRule.requiredProductIds (Identifier[], unique, at least one item): owning any one of the listed products satisfies the rule, and players offer one Buy option per product. Only purchase rules use it.
  • Without it, nothing changes: requireEntitlement stays the only product hint, and a reader that cannot resolve it accepts any purchase. Exporters that also target older players keep requireEntitlement set to the first listed product.
  • paywall.products (PaywallProduct[]: required id, optional name / description as LocalizedString, price { amount, currency }, type purchase | subscription): display info for the products and tiers the rules reference by id. Players label each Buy / Subscribe option with the entry's localized name, description and price, falling back to the rule's name / price, then the id. Informational only — the rules decide what unlocks what.

Details: Paywall → Product lists, Paywall → Products.

Three more additions were merged into 1.6.0 on 2026-09-30 — the version number did not change, and every manifest that was valid before is still valid:

  • Layer keyframe animations. PanelAnimations gains keyframes (AnimationKeyframe[]: required layerId, property, timeMs, value; optional id, easing), loop and name. Nine animatable properties; offsets are fractions of the panel size. The viewport-move fields are unchanged. Animations.
  • Several alternate covers. extras.alt_cover accepts one ExtraBlock (as before) or a non-empty array. Extras.
  • Ensemble character sheets. ExtraCharacterSheet accepts characterIds next to characterId; at least one of the two is required. Extras.

A validator that bundles an earlier copy of the 1.6.0 schema rejects manifests using these fields — use @panelwave/cli 1.2.0 or later.

What changed in 1.5.0

All additive; existing manifests remain valid unchanged. 1.5 adds authoring metadata blocks so a work can round-trip between authoring systems (they power the CMS's work archive transfer) — rendering consumers may ignore all of them:

  • assets.folders (AssetFolder[]): the asset library's folder tree — id, name, optional parentId (nesting) and order. Assets reference folders through the new folderIds array on the asset entry (n:m — one asset can be filed in several folders).
  • Top-level localization (Localization): translation workflow state. locales[] lists the work's locales with isDefault/isActive; entries[] carries one row per translatable string — key, defaultText, optional category/context, an isStale flag, and per-locale values (text plus a machine flag marking machine translations).

Rendered text in the manifest stays in LocalizedString objects as before — the localization block does not replace them; it preserves the workflow state (stale markers, machine-translation flags) that authoring tools need and players do not.

Details: Assets, Localization concepts.

What changed in 1.4.0

All additive; existing manifests remain valid unchanged. Introduces the infinite canvas — a chapter's panels on one continuous world-space plane, read by a camera that follows the story graph:

  • Chapter.canvas (CanvasLayout): world-unit placements (map of panel id → CanvasPlacement with x/y/w/h, z, r, origin, panel-relative enterFraming, revealMode), plane background, camera policy (fitMode, overview, freeRoam, bounds), and presentational decorations. World units: 1 unit = 1 CSS px at zoom 1.
  • Edge.cameraMove (CameraMove): per-edge camera travel — path (direct | arc | waypoints + WorldPoint[]), zoomProfile (hold | pull-back | dive), durationMs, easing, reducedMotionFallback. Inherits like transition: edge → settings.outputPresets[format].defaultCameraMove → built-in direct move.
  • FormatPreset.canvasView (default false) and FormatPreset.defaultCameraMove: canvas view renders only when the active format enables it and the chapter has a canvas — otherwise players fall back to panel view. Print formats ignore canvas.
  • Semantic validation rules (placement keys must be chapter panels, etc.) are enforced by the CLI and CMS preflight — JSON Schema cannot express them structurally.

Details: Infinite Canvas, Graph, Settings.

What changed in 1.3.0

All additive; existing manifests remain valid unchanged:

  • Reusable style presets: settings.typography.textStyles (name → TextStyle) and settings.typography.balloonPresets (name → BalloonConfigOverride), referenced by the new styleRef property on TextLayer and SpeechBubble. Text resolution: work defaults → preset → inline style; balloon cascade: work balloon_config → character → preset → inline balloonConfig. Unknown styleRefs are ignored.
  • TextLayer.style is now the shared $defs/TextStyle definition (same shape as before; also used by the preset map).
  • The speech toggle is implicit: every speech bubble is inherently subject to the reader's speech toggle (initial state: settings.ui.speechDefault), ANDed on top of visibleIf. Per-bubble visibleIf: {"var": "prefs.speech"} boilerplate is obsolete; visibleIf is reserved for story logic.

Details: Settings, Speech Bubbles, Layers.

What changed in 1.2.0

Relaxing, backward-compatible — lets exporters write leaner manifests:

  • mime became optional on ImageVariant, AudioVariant, VideoVariant, SubtitleVariant, and VectorVariant; consumers derive it from the src file extension. Still required when the extension is missing or misleading.
  • Edge.transition inheritance clarified: an edge without a transition inherits the active format's settings.outputPresets[format].defaultTransition, falling back to a plain cut.
  • Documented export discipline: omit values equal to schema defaults (e.g. shareable: true, placement z: 0 / r: 0).

Details: Assets, Graph, Settings.

What changed in 1.1.0

All additive; existing manifests remain valid unchanged:

  • VideoLayer: playMode (once | loop | pingpong | loop-from), loopFromMs, startMode (on-view | on-hover | on-click), controls
  • VideoVariant: direction (forward | reverse) for pre-rendered reverse encodes (smooth ping-pong)
  • AssetCatalogItemVideo: optional poster (VideoPoster)
  • settings.ui: work-level defaults videoPlayModeDefault, videoStartModeDefault, videoMutedDefault
  • tracking.eventWhitelist: new events videoPlay, videoPause, videoEnded, videoLoop

Details: Video, Assets, Settings, Tracking.

Upgrading manifests with the CLI

@panelwave/cli ships an upgrade command that normalizes a manifest to the latest supported format:

# Preview the changes without writing anything
panelwave upgrade ./my-comic/panelwave.json --dry-run

# Upgrade in place
panelwave upgrade ./my-comic/panelwave.json

# Write to a different file
panelwave upgrade ./my-comic/panelwave.json --output ./my-comic/panelwave.upgraded.json

The upgrade currently performs these normalizations:

  • Adds a missing panelwave header
  • Updates panelwave.version and panelwave.schema to the CLI's target version
  • Derives a missing meta.locales array from meta.default_locale
  • Renames the pre-1.0 draft layer field type to kind
  • Adds a missing graph.edges array

The CLI's upgrade target is always the version declared by the schema bundled in that CLI release — 1.7.0 in CLI 1.2.0 (1.6.0 in CLI 1.1.0, 1.5.0 in CLI 1.0.0); the SDK build fails if the two diverge (see CLI → Bundled schema). Update with npm install -g @panelwave/cli@latest when a new format version ships. Since 1.1–1.7 are purely additive or relaxing, older manifests need no migration to be read by current consumers — upgrading normalizes the header and legacy fields. Run --dry-run first and review the reported steps before overwriting files.

After upgrading, always re-validate:

panelwave validate ./my-comic/panelwave.json

See Validation for validation options and CLI for the full command reference.