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

> Discover how OpenMAIC DSL manages versioning and migrations. Learn about its automatic document upgrades from stored dslVersion to the current DSL_VERSION using a pure migration runner and ordered transforms.

- Repository: [MAIC/OpenMAIC](https://github.com/THU-MAIC/OpenMAIC)
- Tags: how-to-guide
- Published: 2026-09-10

---

**@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`](https://github.com/THU-MAIC/OpenMAIC/blob/main/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`](https://github.com/THU-MAIC/OpenMAIC/blob/main/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`](https://github.com/THU-MAIC/OpenMAIC/blob/main/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`](https://github.com/THU-MAIC/OpenMAIC/blob/main/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`](https://github.com/THU-MAIC/OpenMAIC/blob/main/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:

```typescript
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:

```typescript
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`](https://github.com/THU-MAIC/OpenMAIC/blob/main/version.ts):

```typescript
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.