Files
suaveplan/openspec/archive/2026-04-24-enterprise-migration/tasks.md
2026-05-27 23:29:53 +01:00

13 KiB

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

  • Create directory universal/database/migration/ with src/, tests/, src/__benchmarks__/, and all feature subdirectories per the layout in proposal.md.
  • 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:^
  • Write tsconfig.json extending @suaveplan/typescript-config/library.json.
  • Write vitest.config.ts using createTestConfig() from @suaveplan/testing/config (pool: forks).
  • 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)

  • 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.
  • Write src/types/migration.types.md (≥200 words).

2b. Error codes (src/error-codes.ts)

  • Import CodeMetadata, ErrorCodes, SuaveplanError from @suaveplan/error.
  • 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.
  • Define MIGRATION_ERROR_METADATA with httpStatus, severity, retryable, userMessageKey, category, description per code.
  • Export registerMigrationErrorCodes(). Idempotent; called eagerly at module scope.
  • Export MigrationError extending SuaveplanError carrying MigrationErrorCode discriminant.

2c. Zod schemas (src/types/schemas.ts + src/types/schemas.md)

  • Write MigrationRunnerConfigSchema, RunOptionsSchema, BatchMigratorConfigSchema, DatabaseStoreConfigSchema, DistributedLockConfigSchema, RedisLockConfigSchema.
  • Write src/types/schemas.md (≥200 words).

2d. Events (src/migration-events.ts)

  • Define MigrationEventMap interface: migration:before, migration:after, migration:failed, migration:plan.
  • Export migrationEvents — a lazily-initialised EventBus<MigrationEventMap> singleton from @suaveplan/events.

3. Constants (src/constants/index.ts)

  • Export DEFAULT_MIGRATION_TABLE = 'migrations', DEFAULT_LOCK_TTL_MS = 30_000, DEFAULT_BATCH_CONCURRENCY = 5.

4. Planner (src/planner/migration-planner/)

  • Implement sortMigrations(migrations) — sort ascending by migration id (lexicographic).
  • 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', ...)).
  • 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: [] }.
  • Implement planDown(all, applied, steps?):
    • Return steps most-recently-applied in reverse order for rollback.
    • Default steps = 1.
  • Write migration-planner.test.ts (≤500 lines): happy path up/down, already applied skipped, checksum mismatch throws, empty sets, partial rollback with steps.
  • Write migration-planner.md (≥200 words).

5. Store adapters

5a. Memory store (src/store/memory-store/)

  • Implement MemoryMigrationStore implementing MigrationStoreAdapter.
    • Internal Map<string, MigrationRecord>.
    • Thread-safety: all operations are synchronous but wrapped in Promise.resolve() to honour the async interface.
  • Write memory-store.test.ts: record/getApplied/remove/clear.
  • Write memory-store.md (≥200 words).

5b. Database store (src/store/database-store/)

  • 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).
  • Write database-store.test.ts (integration: @suaveplan/db-sqlite SQLite adapter in-memory database).
  • Write database-store.md (≥200 words).

6. Lock adapters

6a. Memory lock (src/lock/memory-lock/)

  • Implement MemoryMigrationLock — in-process boolean flag; acquire with timeout using injectable clock.
  • Write memory-lock.test.ts: acquire, double-acquire times out, release.
  • Write memory-lock.md (≥200 words).

6b. Distributed lock (src/lock/distributed-lock/)

  • Implement DistributedMigrationLock(config) — delegates to config.adapter.acquire/release.
  • Implement InMemoryDistributedLockAdapter — a test double implementing DistributedLockAdapter using an in-process Map.
  • Write distributed-lock.test.ts.
  • Write distributed-lock.md (≥200 words).

6c. Redis lock (src/lock/redis-lock/)

  • Implement RedisDistributedLock(config) — uses config.client.set(key, value, { NX: true, PX: ttl }) for acquire; config.client.del(key) for release.
  • Write redis-lock.test.ts — DI the RedisLockClient interface with a real in-memory stub (no vi.mock).
  • Write redis-lock.md (≥200 words).

7. Executor (src/runner/migration-executor/)

  • 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.
  • Write migration-executor.test.ts (unit: inject MemoryMigrationStore + MemoryMigrationLock).
  • Write migration-executor.md (≥200 words).

8. Runner (src/runner/migration-runner/)

  • 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.
  • Write migration-runner.test.ts (≤500 lines): up/down happy path, abort mid-run, checksum mismatch, lock contention, dry-run.
  • Write migration-runner.md (≥200 words).

9. Dry-run reporter (src/runner/dry-run-reporter/)

  • Implement DryRunReporter: record(entry) appends; report() computes totalRisk (highest entry risk), generates plain-text summary.
  • Write dry-run-reporter.test.ts.
  • Write dry-run-reporter.md (≥200 words).

10. Status dashboard (src/status/migration-status/)

  • 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.
  • Write migration-status.test.ts.
  • Write migration-status.md (≥200 words).

11. Batch migrator (src/batch/batch-migrator/)

  • Implement BatchMigrator(config) — runs config.targets runners concurrently up to config.concurrency (default 5) using a semaphore; collects results.
  • Write batch-migrator.test.ts: two targets, one fails; concurrency respected.
  • Write batch-migrator.md (≥200 words).

12. SurrealDB adapter (src/adapters/surrealdb-adapter/)

  • 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).
  • Write surrealdb-adapter.test.ts — DI a mock { query } conforming to SurrealDbAdapterConfig.
  • Write surrealdb-adapter.md (≥200 words).

13. Property-based tests (tests/property.test.ts)

Use fast-check. All timeouts: { timeout: 30000 }.

  • planUp idempotency: apply all migrations, then call planUp again — toApply is empty.
  • sortMigrations stability: for any permutation of N migrations, sortMigrations returns the same order.
  • checksumMigration determinism: for the same migration object, checksum called twice returns the same string.
  • 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.

  • planUp_100: plan for 100 pending migrations (pure, no IO).
  • runner_up_10_memory: run 10 no-op migrations against MemoryMigrationStore.
  • checksumMigration_1k: 1000 checksum computations.
  • Add bench script and placeholder benchmark table to README.md.

15. Docs

  • src/types/migration.types.md
  • src/types/schemas.md
  • src/planner/migration-planner/migration-planner.md
  • src/runner/migration-runner/migration-runner.md
  • src/runner/migration-executor/migration-executor.md
  • src/runner/dry-run-reporter/dry-run-reporter.md
  • src/store/memory-store/memory-store.md
  • src/store/database-store/database-store.md
  • src/lock/memory-lock/memory-lock.md
  • src/lock/distributed-lock/distributed-lock.md
  • src/lock/redis-lock/redis-lock.md
  • src/status/migration-status/migration-status.md
  • src/batch/batch-migrator/batch-migrator.md
  • src/adapters/surrealdb-adapter/surrealdb-adapter.md
  • Root universal/database/migration/README.md.

16. Verify and publish

  • Run bunx turbo lint --filter=@suaveplan/migration — zero errors.
  • Run bunx turbo typecheck --filter=@suaveplan/migration — zero errors.
  • Run bunx turbo test --filter=@suaveplan/migration — all tests pass.
  • Run bunx turbo test:coverage --filter=@suaveplan/migration — 100% on all touched modules.
  • Run bun run docs:check --filter=@suaveplan/migration — zero errors.
  • Confirm no TODO, FIXME, @deprecated, untyped any, ts-ignore, or .skip in src/ or tests/.
  • Confirm "sideEffects": false in package.json.
  • Run bun scripts/publish.ts --package @suaveplan/migration.
  • Update CLAUDE.md package listing to include @suaveplan/migration.
  • Archive: move openspec/changes/enterprise-migration/ to openspec/archive/2026-04-24-enterprise-migration/ and fold spec delta into openspec/specs/database/spec.md.