# OpenSpec Change: enterprise-migration - **Change ID**: enterprise-migration - **Package**: `@suaveplan/migration` - **Category**: universal - **Target path**: `universal/database/migration` - **Status**: PROPOSED - **Date**: 2026-04-24 - **Reference spec**: `/mnt/s/web/suaveplan/enterprise/migration.md` --- ## Summary Deliver `@suaveplan/migration` on the genesis branch from scratch at enterprise quality. The package provides a database-agnostic schema migration runner: a `MigrationRunner` that orchestrates up/down execution, a pluggable `MigrationStoreAdapter` for ledger persistence, a `MigrationLockAdapter` interface (memory + distributed), a dry-run reporter, a status dashboard, an optional batch migrator for multi-target rollouts, and a SurrealDB adapter. Genesis adds structured errors via `@suaveplan/error`, Zod schemas for all public types, OTel instrumentation, `AbortSignal` cancellation, logger injection via `@suaveplan/logger`, event bus integration via `@suaveplan/events`, property-based tests (fast-check), and a mitata benchmark suite. The migration runner previously bundled inside `@suaveplan/surrealdb` is now a standalone package consuming `@suaveplan/db`'s `DatabaseAdapter` interface. --- ## Current-state audit (genesis) The package does not exist on genesis yet — this is a fresh build. No prior source, no test results, no coverage data. --- ## Available workspace deps on genesis | Package | Role | |---|---| | `@suaveplan/error` (`workspace:^`) | Base class for `MigrationError`; `defineCodes` for `MIGRATION.*` codes | | `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock`, `DatabaseLike` | | `@suaveplan/db` (`workspace:^`) | `DatabaseAdapter` interface consumed by `DatabaseMigrationStore` and `SurrealDbAdapter` | | `@suaveplan/logger` (`workspace:^`) | `LoggerLike` structured logging throughout the runner | | `@suaveplan/events` (`workspace:^`) | `EventBus` for migration lifecycle events (before/after each step) | | `zod` (`catalog:forms`) | Zod schemas for all public option types | | `@opentelemetry/api` (`catalog:observability`) | Spans + counters on runner, planner, executor | | `tslib` (`catalog:build`) | Runtime helpers | Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`), `@suaveplan/testing` (`workspace:^`), `@suaveplan/db-sqlite workspace:^` (integration test store backend). --- ## Preserved public API surface (from main branch) ### Runner ```ts class MigrationRunner { constructor(config: MigrationRunnerConfig) up(options?: RunOptions): Promise down(options?: RunOptions & { steps?: number }): Promise status(): Promise validate(): Promise } interface MigrationRunnerConfig { migrations: Migration[] store: MigrationStoreAdapter lock?: MigrationLockAdapter logger?: LoggerLike dryRun?: boolean checksumValidation?: boolean } interface RunOptions { signal?: AbortSignal; target?: string } ``` ### Types ```ts interface Migration { id: string name: string up: (ctx: MigrationContext) => Promise down?: (ctx: MigrationContext) => Promise checksum?: string } interface MigrationContext { db: DatabaseAdapter; logger: LoggerLike } type MigrationDirection = 'up' | 'down' type MigrationStatus = 'pending' | 'applied' | 'failed' | 'rolled-back' interface MigrationRecord { id: string; name: string; appliedAt: number; checksum?: string; direction: MigrationDirection } interface MigrationPlan { toApply: Migration[]; toRollback: Migration[] } interface MigrationResult { applied: string[]; skipped: string[]; failed: string[]; direction: MigrationDirection; dryRun: boolean } interface ValidationError { migrationId: string; message: string } interface ValidationResult { valid: boolean; errors: ValidationError[] } ``` ### Store adapters ```ts interface MigrationStoreAdapter { getApplied(): Promise record(record: MigrationRecord): Promise remove(id: string): Promise clear(): Promise } class MemoryMigrationStore implements MigrationStoreAdapter class DatabaseMigrationStore implements MigrationStoreAdapter { constructor(config: DatabaseStoreConfig) } interface DatabaseStoreConfig { adapter: DatabaseAdapter; tableName?: string } interface DatabaseAdapter { query(sql: string, params?: unknown[]): Promise } ``` ### Lock adapters ```ts interface MigrationLockAdapter { acquire(timeout?: number): Promise release(): Promise isLocked(): Promise } class MemoryMigrationLock implements MigrationLockAdapter class DistributedMigrationLock implements MigrationLockAdapter { constructor(config: DistributedLockConfig) } interface DistributedLockAdapter { acquire(key: string, ttl: number): Promise; release(key: string): Promise } interface DistributedLockConfig { adapter: DistributedLockAdapter; key?: string; ttl?: number } class InMemoryDistributedLockAdapter implements DistributedLockAdapter class RedisDistributedLock implements MigrationLockAdapter { constructor(config: RedisLockConfig) } interface RedisLockClient { set(key: string, value: string, options: object): Promise; del(key: string): Promise } interface RedisLockConfig { client: RedisLockClient; key?: string; ttl?: number } ``` ### Planner ```ts function planUp(all: Migration[], applied: MigrationRecord[]): MigrationPlan function planDown(all: Migration[], applied: MigrationRecord[], steps?: number): MigrationPlan function sortMigrations(migrations: Migration[]): Migration[] function checksumMigration(migration: Migration): string ``` ### Executor ```ts class MigrationExecutor { constructor(options: ExecutorOptions) execute(plan: MigrationPlan, direction: MigrationDirection, options?: RunOptions): Promise } ``` ### Dry-run reporter ```ts class DryRunReporter { constructor() record(entry: DryRunEntry): void report(): DryRunReport } interface DryRunEntry { migrationId: string; direction: MigrationDirection; riskLevel: RiskLevel; reason?: string } interface DryRunReport { entries: DryRunEntry[]; totalRisk: RiskLevel; summary: string } type RiskLevel = 'low' | 'medium' | 'high' ``` ### Status dashboard ```ts class MigrationStatusDashboard { constructor(config: StatusConfig) getAll(): Promise getSummary(): Promise healthCheck(): Promise getHistory(id: string): Promise } ``` ### Batch migrator ```ts class BatchMigrator { constructor(config: BatchMigratorConfig) run(options?: RunOptions): Promise } interface BatchMigratorConfig { targets: Array<{ name: string; runner: MigrationRunner }>; concurrency?: number } interface BatchMigrationResult { results: Map; failed: string[]; succeeded: string[] } ``` ### SurrealDB adapter ```ts class SurrealDbAdapter implements DatabaseAdapter { constructor(config: SurrealDbAdapterConfig) query(sql: string, params?: unknown[]): Promise } interface SurrealDbAdapterConfig { db: unknown /* Surreal instance */ } ``` ### Events ```ts interface MigrationEventMap { 'migration:before': { migration: Migration; direction: MigrationDirection } 'migration:after': { migration: Migration; direction: MigrationDirection; durationMs: number } 'migration:failed': { migration: Migration; direction: MigrationDirection; error: Error } 'migration:plan': { plan: MigrationPlan; direction: MigrationDirection } } const migrationEvents: EventBus ``` ### Error codes ```ts type MigrationErrorCode = | 'MIGRATION_FAILED' | 'MIGRATION_LOCK_TIMEOUT' | 'MIGRATION_CHECKSUM_MISMATCH' | 'MIGRATION_NOT_FOUND' | 'MIGRATION_STORE_ERROR' | 'MIGRATION_PLAN_INVALID' | 'MIGRATION_ABORTED' | 'MIGRATION_DRY_RUN' ``` --- ## New additions for genesis ### 1. Structured `MigrationError` `MigrationError` extends `SuaveplanError` from `@suaveplan/error` carrying a `MigrationErrorCode` discriminant. Internal throw sites use named factory helpers. ### 2. Zod schemas - `MigrationRunnerConfigSchema` - `RunOptionsSchema` - `BatchMigratorConfigSchema` - `DatabaseStoreConfigSchema` - `DistributedLockConfigSchema` - `RedisLockConfigSchema` ### 3. Logger injection `MigrationRunnerConfig.logger` is now typed as `LoggerLike` (from `@suaveplan/types`). All runner, planner, and executor operations emit structured log entries at `debug` / `info` / `error` levels with migration ID, direction, and duration. ### 4. Event bus integration `MigrationRunnerConfig` gains an optional `events?: EventBus`. When provided the runner emits `migration:before`, `migration:after`, `migration:failed`, `migration:plan` events. The exported `migrationEvents` singleton is available for packages that do not control runner construction. ### 5. OTel instrumentation | Operation | Signal | |---|---| | `runner.up` / `runner.down` | span `migration.run`, attribute `migration.direction`, `migration.count` | | per-migration execution | span `migration.execute`, attribute `migration.id`, histogram `migration.duration_ms` | | `runner.status` | counter `migration.status_query_total` | | `batchMigrator.run` | span `migration.batch`, attribute `migration.target_count` | ### 6. Checksum validation `checksumMigration` hashes the serialised `up` function body. When `checksumValidation: true` (default on `MigrationRunner`), mismatched checksums between the registered migration and the store record throw `MigrationError(MIGRATION_CHECKSUM_MISMATCH)` before applying. ### 7. Property-based tests (fast-check) - `planUp` idempotency: applying `planUp` to an already fully-applied set returns an empty `toApply` list. - `sortMigrations` stability: for any permutation of N migrations, `sortMigrations` returns the same order. - `checksumMigration` determinism: calling checksum on the same migration object twice returns the same string. - `BatchMigrator` partial failure: if K of N targets fail, the result has exactly K failed entries. ### 8. Benchmark suite - `planUp_100`: plan for 100 pending migrations. - `runner_up_10_memory`: run 10 no-op migrations against `MemoryMigrationStore`. - `checksumMigration_1k`: 1000 checksum computations. --- ## File layout ```text universal/database/migration/ package.json tsconfig.json vitest.config.ts src/ index.ts # Public barrel error-codes.ts # MIGRATION.* codes migration-events.ts # migrationEvents singleton + MigrationEventMap constants/ index.ts # DEFAULT_TABLE_NAME, DEFAULT_LOCK_TTL types/ migration.types.ts # All public TS interfaces migration.types.md schemas.ts # Zod schemas schemas.md planner/ migration-planner/ migration-planner.ts # planUp, planDown, sortMigrations, checksumMigration migration-planner.md migration-planner.test.ts index.ts runner/ migration-runner/ migration-runner.ts # MigrationRunner migration-runner.md migration-runner.types.ts migration-runner.test.ts index.ts migration-executor/ migration-executor.ts # MigrationExecutor migration-executor.md migration-executor.test.ts index.ts dry-run-reporter/ dry-run-reporter.ts dry-run-reporter.md dry-run-reporter.test.ts index.ts store/ memory-store/ memory-store.ts # MemoryMigrationStore memory-store.md memory-store.test.ts index.ts database-store/ database-store.ts # DatabaseMigrationStore database-store.md database-store.types.ts database-store.test.ts index.ts lock/ memory-lock/ memory-lock.ts memory-lock.md memory-lock.test.ts index.ts distributed-lock/ distributed-lock.ts distributed-lock.md distributed-lock.types.ts distributed-lock.test.ts index.ts redis-lock/ redis-lock.ts redis-lock.md redis-lock.types.ts redis-lock.test.ts index.ts status/ migration-status/ migration-status.ts # MigrationStatusDashboard migration-status.md migration-status.types.ts migration-status.test.ts index.ts batch/ batch-migrator/ batch-migrator.ts batch-migrator.md batch-migrator.types.ts batch-migrator.test.ts index.ts adapters/ surrealdb-adapter/ surrealdb-adapter.ts surrealdb-adapter.md surrealdb-adapter.types.ts surrealdb-adapter.test.ts index.ts __benchmarks__/ migration.bench.ts tests/ integration.test.ts # Full up/down cycle with SQLite store (db-sqlite) property.test.ts # fast-check property tests ``` --- ## Impact **Downstream packages**: any package that manages database schema evolution. `@suaveplan/surrealdb` provides a `SurrealAdapter` that implements `DatabaseAdapter`; `@suaveplan/migration`'s `SurrealDbAdapter` wraps that for the migration store. **Breaking changes from main-branch API**: none. New optional fields (`events`, `signal`) are additive. **Workspace dep prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/db`, `@suaveplan/logger`, `@suaveplan/events` must be published to Verdaccio before this package can be built or tested.