# How OpenMAIC Handles DSL Versioning and Migration: A Deep-Dive Guide

> Learn how OpenMAIC manages DSL versioning and migration with separate persistent and runtime versions. Discover its pure, side-effect-free upgrade transforms for seamless document updates.

- Repository: [MAIC/OpenMAIC](https://github.com/THU-MAIC/OpenMAIC)
- Tags: deep-dive
- Published: 2026-09-06

---

**OpenMAIC maintains two separate version lines for its DSL: the persistent document version (`DSL_VERSION`) and the in-memory runtime version (`RUNTIME_DSL_VERSION`), with pure, side-effect-free migration transforms that upgrade documents through an ordered ladder of changes.**

The Domain-Specific Language (DSL) in OpenMAIC defines the shape of serialized slide contracts stored on disk and the structure of runtime session objects. Unlike typical npm packages where a single version number suffices, OpenMAIC decouples the DSL contract version from the package version. This separation allows API refactors and internal improvements without forcing document migrations.

## Core Versioning Concepts

### Separated Version Lines

OpenMAIC tracks two distinct DSL versions:

- **`DSL_VERSION`** — The version of persisted slide documents, defined in `packages/@openmaic/dsl/src/version.ts` at [line 61](https://github.com/THU-MAIC/OpenMAIC/blob/main/packages/@openmaic/dsl/src/version.ts#L61)
- **`RUNTIME_DSL_VERSION`** — The version of in-memory runtime session objects, defined at [line 76](https://github.com/THU-MAIC/OpenMAIC/blob/main/packages/@openmaic/dsl/src/version.ts#L76)

These constants are intentionally independent of the [`package.json`](https://github.com/THU-MAIC/OpenMAIC/blob/main/package.json) version. A package bump from `2.1.0` to `2.2.0` might add utility functions without touching `DSL_VERSION`, meaning existing documents require no migration.

### Envelope Keys for Version Stamps

Documents and sessions carry their version in envelope fields:

- `DSL_VERSION_KEY` (value: `'dslVersion'`) — stamps persisted documents at [line 91](https://github.com/THU-MAIC/OpenMAIC/blob/main/packages/@openmaic/dsl/src/version.ts#L91)
- `RUNTIME_DSL_VERSION_KEY` (value: `'runtimeDslVersion'`) — stamps runtime sessions at [line 108](https://github.com/THU-MAIC/OpenMAIC/blob/main/packages/@openmaic/dsl/src/version.ts#L108)

These keys enable the runtime to inspect any object and determine its DSL lineage without parsing the full payload.

## The Migration Ladder Architecture

### Ordered Transform Arrays

Migrations are declared as pure functions in ordered arrays:

- **`DSL_MIGRATIONS`** — transforms for persisted documents, defined at [line 235](https://github.com/THU-MAIC/OpenMAIC/blob/main/packages/@openmaic/dsl/src/version.ts#L235)
- **`RUNTIME_DSL_MIGRATIONS`** — transforms for runtime sessions

Each migration step specifies `from` version, `to` version, and a `migrate` function. The ladder is append-only; new versions extend the array rather than modifying existing entries.

### Key Properties of Migration Functions

Every transform in the ladder must be:

- **Pure** — no side effects, returns a new object
- **Idempotent** — running twice produces the same result
- **Forward-compatible** — handles documents already at or beyond its target version

These constraints allow OpenMAIC to safely re-migrate documents during import, sync, or recovery operations.

## Version Inspection and Migration Helpers

### Reading Versions Safely

The `dslVersionOf` function at [line 407](https://github.com/THU-MAIC/OpenMAIC/blob/main/packages/@openmaic/dsl/src/version.ts#L407) and `runtimeDslVersionOf` at [line 423](https://github.com/THU-MAIC/OpenMAIC/blob/main/packages/@openmaic/dsl/src/version.ts#L423) enforce well-formedness during version extraction:

- Handle legacy documents lacking any version stamp (unversioned epoch)
- Reject ambiguous cross-line stamps (a document carrying `runtimeDslVersion` but not `dslVersion`)
- Return the current version constant for valid, stamped objects

Runtime sessions have **no unversioned epoch** — an unstamped runtime object triggers a no-runtime-epoch error, indicating either a mis-routed legacy document or a producer bug.

### Detecting Migration Needs

The predicate functions `needsMigration` ([line 507](https://github.com/THU-MAIC/OpenMAIC/blob/main/packages/@openmaic/dsl/src/version.ts#L507)) and `needsRuntimeMigration` ([line 532](https://github.com/THU-MAIC/OpenMAIC/blob/main/packages/@openmaic/dsl/src/version.ts#L532)) compare an object's version against the current constants. They share validation logic with the migrators, ensuring consistent error handling.

### Executing Migrations

The `migrate` function at [line 640](https://github.com/THU-MAIC/OpenMAIC/blob/main/packages/@openmaic/dsl/src/version.ts#L640) and `migrateRuntime` at [line 670](https://github.com/THU-MAIC/OpenMAIC/blob/main/packages/@openmaic/dsl/src/version.ts#L670) implement the migration runner:

1. Look up the current version in the ladder
2. Apply each sequential `migrate` function
3. Stamp the new version after each step
4. Repeat until reaching `DSL_VERSION` or `RUNTIME_DSL_VERSION`

If the document is newer than the target version, it returns unchanged. If a migration path is missing or the envelope is malformed, the runner **fails loudly** rather than silently corrupting data.

## Practical DSL Migration Examples

### Detecting and Migrating Persisted Slides

```typescript
import { dslVersionOf, needsMigration, migrate } from '@openmaic/dsl';

// `rawDoc` is the JSON object read from storage
const rawDoc: unknown = fetchFromDatabase();

console.log('Current DSL stamp:', dslVersionOf(rawDoc));

if (needsMigration(rawDoc)) {
  const migrated = migrate(rawDoc);
  console.log('Migrated to', dslVersionOf(migrated));
  // Persist `migrated` back to storage
}

```

### Working with Runtime Sessions

```typescript
import {
  runtimeDslVersionOf,
  needsRuntimeMigration,
  migrateRuntime,
} from '@openmaic/dsl';

const session: unknown = getRuntimeSession();

console.log('Runtime DSL stamp:', runtimeDslVersionOf(session));

if (needsRuntimeMigration(session)) {
  const upgraded = migrateRuntime(session);
  console.log('Session upgraded to', runtimeDslVersionOf(upgraded));
  // Continue using `upgraded` (e.g., feed it to the agent)
}

```

### Adding a New Migration Step

```typescript
// 1. Implement a pure transform function:
function addNewField(doc: unknown): unknown {
  if (typeof doc === 'object' && doc !== null) {
    return { ...doc, newField: 'defaultValue' };
  }
  return doc;
}

// 2. Append it to the ladder (in `version.ts`):
export const DSL_MIGRATIONS = [
  ...previousMigrations,
  { from: '0.3.0', to: '0.4.0', migrate: addNewField },
] as const;

// 3. Bump `DSL_VERSION` to `'0.4.0'` and increase the npm package version
//    according to the caret-escape rule (minor → major at 1.0.0 boundary).

```

## Release Discipline and Breaking Changes

### The Caret-Escape Rule

Changing `DSL_VERSION` or `RUNTIME_DSL_VERSION` **must** accompany a package version bump that breaks dependents' caret ranges. This rule, enforced by `scripts/check-package-version-bumps.mjs`, ensures downstream packages (`@openmaic/storage`, `@openmaic/renderer`, `@openmaic/importer`) cannot silently consume incompatible DSL shapes.

For example, if `DSL_VERSION` increments from `0.3.0` to `0.4.0` while the package is at `1.2.3`, the new package version becomes `2.0.0`. Caret ranges (`^1.2.3`) will not match `2.0.0`, forcing explicit upgrades.

## Summary

- **Separate version lines** — `DSL_VERSION` for persisted documents, `RUNTIME_DSL_VERSION` for sessions, both independent of npm package version
- **Migration ladders** — ordered arrays of pure, idempotent transforms in `DSL_MIGRATIONS` and `RUNTIME_DSL_MIGRATIONS`
- **Strict validation** — `dslVersionOf` and `runtimeDslVersionOf` enforce envelope well-formedness and reject cross-line contamination
- **Loud failures** — missing migration paths or malformed envelopes throw rather than corrupt
- **Caret-escape releases** — DSL version changes force breaking package bumps to protect downstream consumers

## Frequently Asked Questions

### What happens if I try to migrate a runtime session using the document migrator?

The version inspection functions enforce **cross-line safety**. If an object carries `runtimeDslVersion` but not `dslVersion`, `dslVersionOf` throws a cross-line error. This prevents accidental corruption by ensuring document and runtime migrations stay strictly separated.

### Can I skip migration steps in the ladder?

No. The `migrate` and `migrateRuntime` runners walk the ladder sequentially from the object's current version to the target. Each step must exist; if a version jump lacks an intermediate transform, the runner fails loudly. This design guarantees every schema change is explicitly handled.

### Why does the runtime version have no unversioned epoch?

Persisted documents may originate from early prototypes before versioning existed, so `dslVersionOf` handles the unversioned epoch gracefully. Runtime sessions are always produced by code that should stamp them correctly; an unstamped runtime object indicates a bug or mis-routing that should surface immediately rather than be silently patched.

### How do I verify a migration won't break existing documents?

The test suite in [`tests/document-store/migration.test.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/tests/document-store/migration.test.ts) exercises migration paths with fixtures representing each historical DSL version. Before releasing a new migration, add a fixture at the previous version and assert it upgrades correctly to the new `DSL_VERSION`.