How @openmaic/dsl Handles Versioning and Migrations: A Complete Guide to the DSL Migration System

@openmaic/dsl isolates the serialized slide contract version from the npm package version using a pure migration runner that automatically upgrades documents from their stored dslVersion to the current DSL_VERSION via an ordered ladder of side-effect-free transforms.

The @openmaic/dsl package in the THU-MAIC/OpenMAIC repository manages schema evolution for slide documents through a strict separation between the DSL contract version and the npm package semantic version. This architecture allows the underlying data structure to evolve without breaking existing installations, while ensuring that all persisted documents can be automatically migrated to the latest schema.

DSL Version Constants and Envelope Storage

All versioning metadata is centralized in packages/@openmaic/dsl/src/version.ts. The system uses five core constants to manage document identity:

  • DSL_VERSION – The current contract version ('0.3.0'), representing the canonical schema for serialized slides.
  • UNVERSIONED_DSL_VERSION – A legacy sentinel ('0.0.0') assigned to documents created before versioning was implemented.
  • INITIAL_DSL_VERSION – The first shipped contract version ('0.1.0'), marking the boundary of legacy support.
  • DSL_VERSION_KEY – The envelope property name ('dslVersion') where persisted documents store their schema version.
  • RUNTIME_DSL_VERSION_KEY – The envelope property ('runtimeDslVersion') for active runtime sessions requiring different migration tracking.

When a document is serialized, the current DSL_VERSION is stamped into the envelope using DSL_VERSION_KEY. This enables the migration system to identify the schema vintage of any document retrieved from storage.

The Migration Ladder Architecture

The package defines an ordered array DSL_MIGRATIONS in version.ts that functions as a migration ladder. Each entry is a plain object implementing the DslMigration interface:

  • from – The source version string (e.g., '0.2.0').
  • to – The target version string (e.g., '0.3.0').
  • migrate – A pure, side-effect-free transform function that receives a document and returns the migrated copy.

The ladder is sequential; the migration runner walks a document from its stored version up to DSL_VERSION, applying every migration in order. For example, the ladder includes entries for 0.0.0 → 0.1.0 (handling unversioned legacy documents), 0.1.0 → 0.2.0 (a pure version stamp), and 0.2.0 → 0.3.0 (which strips stray geometric fields).

The Pure Migration Runner

The exported migrate function in version.ts orchestrates the upgrade process. Because the runner contains no runtime dependencies, migrations are safe to execute in any environment—whether during server-side storage operations, client-side importing, or rendering pipelines.

The runner operates synchronously through three steps:

  1. Version detection – Reads the document's dslVersion property, defaulting to UNVERSIONED_DSL_VERSION if missing.
  2. Slice selection – Locates the appropriate contiguous slice of DSL_MIGRATIONS spanning from the detected version to DSL_VERSION.
  3. Transform application – Iteratively applies each migrate function, passing the output of one as input to the next.
  4. Version stamping – Assigns the new DSL_VERSION to the document envelope before returning.

This design guarantees that migrations are pure functions—they never mutate the input object and produce no side effects, making them deterministic and testable.

Runtime vs Persisted Versions

@openmaic/dsl maintains parallel versioning tracks for different document lifecycles. While persisted slides use DSL_VERSION and DSL_VERSION_KEY, active runtime sessions utilize runtimeDslVersion tracked via RUNTIME_DSL_VERSION_KEY.

The migrateRuntime function (provided in packages/@openmaic/dsl/src/migrate.ts) handles upgrades for session documents separately from storage documents. This separation allows runtime-specific schema optimizations without affecting the persistent storage format, ensuring that temporary session data can evolve independently from archived slides.

Package Version Enforcement and Breaking Changes

A critical release rule enforced by scripts/check-package-version-bumps.mjs ensures that changes to DSL_VERSION force a package version bump that caret ranges cannot satisfy. When the contract shape changes, the script detects the modification and mandates a minor or major version increase that breaks the ^ range, forcing downstream consumers to explicitly upgrade.

This enforcement guarantees that any application pulling a caret-range version of @openmaic/dsl will never receive an unexpected contract shape change. The npm version effectively acts as a gatekeeper for schema compatibility, while DSL_VERSION manages the internal data transformation.

Legacy Handling and Schema Evolution

The migration system gracefully handles documents predating the versioning system. The first migration entry lifts UNVERSIONED_DSL_VERSION (0.0.0) documents by assigning them INITIAL_DSL_VERSION (0.1.0) before applying subsequent transforms.

Specific historical migrations include:

  • 0.1.0 → 0.2.0 – A pure version stamp with no field modifications, establishing the versioning convention.
  • 0.2.0 → 0.3.0 – Eliminates deprecated geometric fields using stripLegacyLineGeometry from packages/@openmaic/dsl/src/legacy-line-geometry.ts, removing stray rotate and height properties left by older runtimes.

The design deliberately avoids dropping live payloads (such as audioUrl) within migration functions. Instead, application-side converters remove such fields after the document is read, keeping the migration layer focused solely on structural schema changes.

Migrating Documents in Practice

To upgrade a persisted slide to the current schema, import the migration runner and version constants:

import {
  DSL_VERSION,
  DSL_VERSION_KEY,
  UNVERSIONED_DSL_VERSION,
  migrate,
} from '@openmaic/dsl';

// Simulated retrieved document (may lack version key)
const persistedSlide = {
  content: 'Slide data...',
  // Older documents omit dslVersion entirely
};

// Execute migration pipeline
const migratedSlide = migrate(persistedSlide);

// Verify upgrade
console.assert(migratedSlide[DSL_VERSION_KEY] === DSL_VERSION);
// Document is now safe to render or store

Defining Custom Migrations

To add support for a new schema version, define a migration object and append it to the ladder:

import { DslMigration } from '@openmaic/dsl';

export const migration_0_3_0_to_0_4_0: DslMigration = {
  from: '0.3.0',
  to: '0.4.0',
  migrate(doc) {
    // Pure transform: create new object, never mutate input
    const copy = { ...doc };
    
    if ('oldField' in copy) {
      copy['newField'] = copy['oldField'];
      delete copy['oldField'];
    }
    return copy;
  },
};

Then register the migration in version.ts:

export const DSL_MIGRATIONS: DslMigration[] = [
  // ... existing migrations ...
  migration_0_3_0_to_0_4_0,
];

Summary

  • @openmaic/dsl uses DSL_VERSION constants stored in document envelopes to track schema versions independently from npm package versions.
  • The DSL_MIGRATIONS ladder provides an ordered sequence of pure, side-effect-free transforms that upgrade documents from any historical version to the current schema.
  • The migrate function in packages/@openmaic/dsl/src/version.ts runs synchronously without runtime dependencies, making it safe for storage, import, and rendering contexts.
  • Parallel versioning via runtimeDslVersion separates runtime session schemas from persisted document schemas.
  • The scripts/check-package-version-bumps.mjs enforcement ensures that contract changes trigger breaking package version bumps, preventing accidental schema incompatibilities in caret-range updates.

Frequently Asked Questions

How does @openmaic/dsl handle documents created before versioning existed?

Documents lacking a dslVersion field are treated as UNVERSIONED_DSL_VERSION (0.0.0). The migration runner automatically applies the initial migration entry to lift them to INITIAL_DSL_VERSION (0.1.0) before proceeding through subsequent upgrades, ensuring backward compatibility with legacy data.

What makes the migration runner "pure" and why does it matter?

The migrate function and all migration transforms are pure functions: they do not mutate input objects, produce no side effects, and have no external dependencies. This matters because it makes migrations deterministic, testable (as verified in packages/@openmaic/dsl/test/version.test.ts), and safe to run in any JavaScript environment without risking state corruption.

Why does the DSL version differ from the npm package version?

@openmaic/dsl deliberately decouples the serialized contract version (DSL_VERSION) from the package semantic version to allow schema evolution without forcing consumer upgrades. The npm version changes only when the package API or contract shape changes (enforced by check-package-version-bumps.mjs), while DSL_VERSION increments with every schema modification, enabling granular data migration control.

Can I run migrations on runtime session documents separately from stored slides?

Yes. The package provides separate versioning tracks: use migrate for persisted documents (using dslVersion) and migrateRuntime for active sessions (using runtimeDslVersion). This separation allows runtime-specific optimizations without affecting archived slide data.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →