Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,390 @@
# 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<MigrationResult>
down(options?: RunOptions & { steps?: number }): Promise<MigrationResult>
status(): Promise<MigrationStatus[]>
validate(): Promise<ValidationResult>
}
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<void>
down?: (ctx: MigrationContext) => Promise<void>
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<MigrationRecord[]>
record(record: MigrationRecord): Promise<void>
remove(id: string): Promise<void>
clear(): Promise<void>
}
class MemoryMigrationStore implements MigrationStoreAdapter
class DatabaseMigrationStore implements MigrationStoreAdapter {
constructor(config: DatabaseStoreConfig)
}
interface DatabaseStoreConfig { adapter: DatabaseAdapter; tableName?: string }
interface DatabaseAdapter { query<T>(sql: string, params?: unknown[]): Promise<T[]> }
```
### Lock adapters
```ts
interface MigrationLockAdapter {
acquire(timeout?: number): Promise<boolean>
release(): Promise<void>
isLocked(): Promise<boolean>
}
class MemoryMigrationLock implements MigrationLockAdapter
class DistributedMigrationLock implements MigrationLockAdapter {
constructor(config: DistributedLockConfig)
}
interface DistributedLockAdapter { acquire(key: string, ttl: number): Promise<boolean>; release(key: string): Promise<void> }
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<string | null>; del(key: string): Promise<number> }
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<MigrationResult>
}
```
### 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<ExtendedMigrationStatus[]>
getSummary(): Promise<MigrationSummary>
healthCheck(): Promise<MigrationHealthCheck>
getHistory(id: string): Promise<MigrationHistoryEntry[]>
}
```
### Batch migrator
```ts
class BatchMigrator {
constructor(config: BatchMigratorConfig)
run(options?: RunOptions): Promise<BatchMigrationResult>
}
interface BatchMigratorConfig { targets: Array<{ name: string; runner: MigrationRunner }>; concurrency?: number }
interface BatchMigrationResult { results: Map<string, MigrationResult>; failed: string[]; succeeded: string[] }
```
### SurrealDB adapter
```ts
class SurrealDbAdapter implements DatabaseAdapter {
constructor(config: SurrealDbAdapterConfig)
query<T>(sql: string, params?: unknown[]): Promise<T[]>
}
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<MigrationEventMap>
```
### 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<MigrationEventMap>`. 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.

View File

@@ -0,0 +1,241 @@
# Tasks: enterprise-migration
**Change ID**: enterprise-migration
**Package**: `@suaveplan/migration`
**Target path**: `universal/database/migration`
**Prerequisite packages on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/db`, `@suaveplan/logger`, `@suaveplan/events`
---
## 1. Scaffold
- [x] Create directory `universal/database/migration/` with `src/`, `tests/`, `src/__benchmarks__/`, and all feature subdirectories per the layout in `proposal.md`.
- [x] Write `package.json` at version `0.1.0` with:
- `"type": "module"`, `"sideEffects": false`
- `exports` map: top-level `"."` barrel plus subpaths for `./planner/migration-planner`, `./runner/migration-runner`, `./runner/migration-executor`, `./runner/dry-run-reporter`, `./store/memory-store`, `./store/database-store`, `./lock/memory-lock`, `./lock/distributed-lock`, `./lock/redis-lock`, `./status/migration-status`, `./batch/batch-migrator`, `./adapters/surrealdb-adapter`
- `dependencies`: `@suaveplan/error workspace:^`, `@suaveplan/types workspace:^`, `@suaveplan/db workspace:^`, `@suaveplan/logger workspace:^`, `@suaveplan/events workspace:^`, `zod catalog:forms`, `@opentelemetry/api catalog:observability`, `tslib catalog:build`
- `devDependencies`: `vitest catalog:testing`, `@vitest/coverage-istanbul catalog:testing`, `fast-check catalog:testing`, `mitata catalog:testing`, `@suaveplan/testing workspace:^`, `@suaveplan/db-sqlite workspace:^`
- [x] Write `tsconfig.json` extending `@suaveplan/typescript-config/library.json`.
- [x] Write `vitest.config.ts` using `createTestConfig()` from `@suaveplan/testing/config` (pool: `forks`).
- [x] Add `lint`, `typecheck`, `test`, `test:coverage`, `build`, `docs:check`, `bench` scripts to `package.json`.
---
## 2. Types + errors
### 2a. Migration types (`src/types/migration.types.ts` + `src/types/migration.types.md`)
- [x] Define all public TS interfaces: `Migration`, `MigrationContext`, `MigrationDirection`, `MigrationStatus`, `MigrationRecord`, `MigrationPlan`, `MigrationResult`, `ValidationError`, `ValidationResult`, `MigrationRunnerConfig`, `RunOptions`, `MigrationStoreAdapter`, `MigrationLockAdapter`, `DatabaseAdapter`, `DatabaseStoreConfig`, `DistributedLockAdapter`, `DistributedLockConfig`, `RedisLockClient`, `RedisLockConfig`, `BatchMigratorConfig`, `BatchMigrationResult`, `BatchTargetStatus`, `BatchTargetProgress`, `SurrealDbAdapterConfig`, `DryRunEntry`, `DryRunReport`, `RiskLevel`, `ExtendedMigrationStatus`, `MigrationSummary`, `MigrationHealthCheck`, `MigrationHistoryEntry`, `MigrationStatusEntry`.
- [x] Write `src/types/migration.types.md` (≥200 words).
### 2b. Error codes (`src/error-codes.ts`)
- [x] Import `CodeMetadata`, `ErrorCodes`, `SuaveplanError` from `@suaveplan/error`.
- [x] Define `MigrationErrorCode` union — eight codes: `MIGRATION_FAILED`, `MIGRATION_LOCK_TIMEOUT`, `MIGRATION_CHECKSUM_MISMATCH`, `MIGRATION_NOT_FOUND`, `MIGRATION_STORE_ERROR`, `MIGRATION_PLAN_INVALID`, `MIGRATION_ABORTED`, `MIGRATION_DRY_RUN`.
- [x] Define `MIGRATION_ERROR_METADATA` with `httpStatus`, `severity`, `retryable`, `userMessageKey`, `category`, `description` per code.
- [x] Export `registerMigrationErrorCodes()`. Idempotent; called eagerly at module scope.
- [x] Export `MigrationError` extending `SuaveplanError` carrying `MigrationErrorCode` discriminant.
### 2c. Zod schemas (`src/types/schemas.ts` + `src/types/schemas.md`)
- [x] Write `MigrationRunnerConfigSchema`, `RunOptionsSchema`, `BatchMigratorConfigSchema`, `DatabaseStoreConfigSchema`, `DistributedLockConfigSchema`, `RedisLockConfigSchema`.
- [x] Write `src/types/schemas.md` (≥200 words).
### 2d. Events (`src/migration-events.ts`)
- [x] Define `MigrationEventMap` interface: `migration:before`, `migration:after`, `migration:failed`, `migration:plan`.
- [x] Export `migrationEvents` — a lazily-initialised `EventBus<MigrationEventMap>` singleton from `@suaveplan/events`.
---
## 3. Constants (`src/constants/index.ts`)
- [x] Export `DEFAULT_MIGRATION_TABLE = 'migrations'`, `DEFAULT_LOCK_TTL_MS = 30_000`, `DEFAULT_BATCH_CONCURRENCY = 5`.
---
## 4. Planner (`src/planner/migration-planner/`)
- [x] Implement `sortMigrations(migrations)` — sort ascending by migration `id` (lexicographic).
- [x] Implement `checksumMigration(migration)` — serialise `migration.up.toString()` and produce a hex hash (use `@suaveplan/crypto-utils` if available; otherwise use Web Crypto `crypto.subtle.digest('SHA-256', ...)`).
- [x] Implement `planUp(all, applied)`:
- Sort `all` with `sortMigrations`.
- Filter to migrations whose `id` is not in `applied` (by id + checksum if `checksumValidation`).
- Return `{ toApply, toRollback: [] }`.
- [x] Implement `planDown(all, applied, steps?)`:
- Return `steps` most-recently-applied in reverse order for rollback.
- Default `steps = 1`.
- [x] Write `migration-planner.test.ts` (≤500 lines): happy path up/down, already applied skipped, checksum mismatch throws, empty sets, partial rollback with steps.
- [x] Write `migration-planner.md` (≥200 words).
---
## 5. Store adapters
### 5a. Memory store (`src/store/memory-store/`)
- [x] Implement `MemoryMigrationStore` implementing `MigrationStoreAdapter`.
- Internal `Map<string, MigrationRecord>`.
- Thread-safety: all operations are synchronous but wrapped in `Promise.resolve()` to honour the async interface.
- [x] Write `memory-store.test.ts`: record/getApplied/remove/clear.
- [x] Write `memory-store.md` (≥200 words).
### 5b. Database store (`src/store/database-store/`)
- [x] Implement `DatabaseMigrationStore(config: DatabaseStoreConfig)`:
- On construction, creates the ledger table if it does not exist (via `CREATE TABLE IF NOT EXISTS` on `config.adapter`).
- `record` inserts; `remove` deletes; `getApplied` queries all rows; `clear` truncates.
- Wraps errors as `MigrationError(MIGRATION_STORE_ERROR)`.
- [x] Write `database-store.test.ts` (integration: `@suaveplan/db-sqlite` SQLite adapter in-memory database).
- [x] Write `database-store.md` (≥200 words).
---
## 6. Lock adapters
### 6a. Memory lock (`src/lock/memory-lock/`)
- [x] Implement `MemoryMigrationLock` — in-process boolean flag; `acquire` with timeout using injectable clock.
- [x] Write `memory-lock.test.ts`: acquire, double-acquire times out, release.
- [x] Write `memory-lock.md` (≥200 words).
### 6b. Distributed lock (`src/lock/distributed-lock/`)
- [x] Implement `DistributedMigrationLock(config)` — delegates to `config.adapter.acquire/release`.
- [x] Implement `InMemoryDistributedLockAdapter` — a test double implementing `DistributedLockAdapter` using an in-process Map.
- [x] Write `distributed-lock.test.ts`.
- [x] Write `distributed-lock.md` (≥200 words).
### 6c. Redis lock (`src/lock/redis-lock/`)
- [x] Implement `RedisDistributedLock(config)` — uses `config.client.set(key, value, { NX: true, PX: ttl })` for acquire; `config.client.del(key)` for release.
- [x] Write `redis-lock.test.ts` — DI the `RedisLockClient` interface with a real in-memory stub (no vi.mock).
- [x] Write `redis-lock.md` (≥200 words).
---
## 7. Executor (`src/runner/migration-executor/`)
- [x] Implement `MigrationExecutor(options)`:
- `execute(plan, direction, runOptions?)`:
- Checks `runOptions?.signal` before each migration; throws `MigrationError(MIGRATION_ABORTED)` on abort.
- For each migration in plan: emits `migration:before`, calls `migration[direction](ctx)`, emits `migration:after` with duration, calls `store.record/remove`.
- On error: emits `migration:failed`, records to result.failed, continues to next.
- Dry-run: skips execution, records to `DryRunReporter`.
- OTel: per-migration span `migration.execute`, histogram `migration.duration_ms`.
- Logs via injected `LoggerLike`.
- [x] Write `migration-executor.test.ts` (unit: inject `MemoryMigrationStore` + `MemoryMigrationLock`).
- [x] Write `migration-executor.md` (≥200 words).
---
## 8. Runner (`src/runner/migration-runner/`)
- [x] Implement `MigrationRunner(config)`:
- `up(options?)`:
1. Acquire lock (if configured); throw `MigrationError(MIGRATION_LOCK_TIMEOUT)` on failure.
2. Validate checksums if `checksumValidation: true`.
3. Call `planUp`; emit `migration:plan`.
4. Delegate to `MigrationExecutor.execute(plan, 'up', options)`.
5. Release lock in `finally`.
6. OTel: span `migration.run` with attribute `migration.direction = 'up'`.
- `down(options?)`: analogous with `planDown`.
- `status()` — compares `config.migrations` against `store.getApplied()`, returns `MigrationStatus[]`.
- `validate()` — checks checksums for all applied migrations, returns `ValidationResult`.
- [x] Write `migration-runner.test.ts` (≤500 lines): up/down happy path, abort mid-run, checksum mismatch, lock contention, dry-run.
- [x] Write `migration-runner.md` (≥200 words).
---
## 9. Dry-run reporter (`src/runner/dry-run-reporter/`)
- [x] Implement `DryRunReporter`: `record(entry)` appends; `report()` computes `totalRisk` (highest entry risk), generates plain-text `summary`.
- [x] Write `dry-run-reporter.test.ts`.
- [x] Write `dry-run-reporter.md` (≥200 words).
---
## 10. Status dashboard (`src/status/migration-status/`)
- [x] Implement `MigrationStatusDashboard(config)`:
- `getAll()` — joins registered migrations with applied records.
- `getSummary()` — counts pending/applied/failed.
- `healthCheck()` — returns healthy if zero failed, at-risk if any pending for >24h.
- `getHistory(id)` — returns store records for a specific migration id.
- [x] Write `migration-status.test.ts`.
- [x] Write `migration-status.md` (≥200 words).
---
## 11. Batch migrator (`src/batch/batch-migrator/`)
- [x] Implement `BatchMigrator(config)` — runs `config.targets` runners concurrently up to `config.concurrency` (default 5) using a semaphore; collects results.
- [x] Write `batch-migrator.test.ts`: two targets, one fails; concurrency respected.
- [x] Write `batch-migrator.md` (≥200 words).
---
## 12. SurrealDB adapter (`src/adapters/surrealdb-adapter/`)
- [x] Implement `SurrealDbAdapter(config)` wrapping a SurrealDB `Surreal` instance (typed as `unknown` to avoid hard dep on `surrealdb.js` — callers cast):
- `query<T>(sql, params?)` — calls `db.query(sql, params)`, flattens the `QueryResult<T>[][]` to `T[]`.
- Note: avoid `db.create()` — use `INSERT INTO` query (see memory `feedback_surrealdb_create_content_drop`).
- [x] Write `surrealdb-adapter.test.ts` — DI a mock `{ query }` conforming to `SurrealDbAdapterConfig`.
- [x] Write `surrealdb-adapter.md` (≥200 words).
---
## 13. Property-based tests (`tests/property.test.ts`)
Use `fast-check`. All timeouts: `{ timeout: 30000 }`.
- [x] **`planUp` idempotency**: apply all migrations, then call `planUp` again — `toApply` is empty.
- [x] **`sortMigrations` stability**: for any permutation of N migrations, `sortMigrations` returns the same order.
- [x] **`checksumMigration` determinism**: for the same migration object, checksum called twice returns the same string.
- [x] **Partial batch failure**: BatchMigrator with K/N failing targets yields exactly K entries in `result.failed`.
---
## 14. Benchmarks (`src/__benchmarks__/migration.bench.ts`)
Use `mitata`.
- [x] `planUp_100`: plan for 100 pending migrations (pure, no IO).
- [x] `runner_up_10_memory`: run 10 no-op migrations against `MemoryMigrationStore`.
- [x] `checksumMigration_1k`: 1000 checksum computations.
- [x] Add `bench` script and placeholder benchmark table to `README.md`.
---
## 15. Docs
- [x] `src/types/migration.types.md`
- [x] `src/types/schemas.md`
- [x] `src/planner/migration-planner/migration-planner.md`
- [x] `src/runner/migration-runner/migration-runner.md`
- [x] `src/runner/migration-executor/migration-executor.md`
- [x] `src/runner/dry-run-reporter/dry-run-reporter.md`
- [x] `src/store/memory-store/memory-store.md`
- [x] `src/store/database-store/database-store.md`
- [x] `src/lock/memory-lock/memory-lock.md`
- [x] `src/lock/distributed-lock/distributed-lock.md`
- [x] `src/lock/redis-lock/redis-lock.md`
- [x] `src/status/migration-status/migration-status.md`
- [x] `src/batch/batch-migrator/batch-migrator.md`
- [x] `src/adapters/surrealdb-adapter/surrealdb-adapter.md`
- [x] Root `universal/database/migration/README.md`.
---
## 16. Verify and publish
- [x] Run `bunx turbo lint --filter=@suaveplan/migration` — zero errors.
- [x] Run `bunx turbo typecheck --filter=@suaveplan/migration` — zero errors.
- [x] Run `bunx turbo test --filter=@suaveplan/migration` — all tests pass.
- [x] Run `bunx turbo test:coverage --filter=@suaveplan/migration` — 100% on all touched modules.
- [x] Run `bun run docs:check --filter=@suaveplan/migration` — zero errors.
- [x] Confirm no `TODO`, `FIXME`, `@deprecated`, untyped `any`, `ts-ignore`, or `.skip` in `src/` or `tests/`.
- [x] Confirm `"sideEffects": false` in `package.json`.
- [x] Run `bun scripts/publish.ts --package @suaveplan/migration`.
- [x] Update `CLAUDE.md` package listing to include `@suaveplan/migration`.
- [x] Archive: move `openspec/changes/enterprise-migration/` to `openspec/archive/2026-04-24-enterprise-migration/` and fold spec delta into `openspec/specs/database/spec.md`.