Files
suaveplan/openspec/changes/migration-adapters-completion/proposal.md
2026-05-27 23:29:53 +01:00

11 KiB

Migration adapters completion

Package: @suaveplan/migration Status: PROPOSED Type: spec-conformance + feature

Why

The genesis port of @suaveplan/migration ships concrete MemoryMigrationLock, MemoryMigrationStore, DistributedMigrationLock, and RedisDistributedLock, but the surface around them is incomplete relative to the upstream package at ../suaveplan/packages/universal/migration. The runner already emits migration:before, migration:after, migration:failed, and migration:plan through an EventBus<MigrationEventMap> injected via config — but there is no shared migrationEvents instance that downstream tooling (status dashboards, observability sinks) can subscribe to, no canonical re-export of the lock / store interfaces from dedicated migration-lock / migration-store modules, no BatchTargetProgress / BatchTargetStatus / TargetConfigFactory types for the batch migrator, no GetAllRecordsFn companion for the status dashboard, no ValidationError (under that name) export, and no surfaced MIGRATION_CODES registration step that downstream consumers can rely on at module load.

Consumers of the migration package today have to hand-roll an EventBus<MigrationEventMap> per call site to observe lifecycle events, redeclare batch-progress shape types in their own code, and either ignore validation errors or extract them out of an internal symbol. This change closes those gaps so downstream services (the status dashboard, ops tooling, future SurrealDB / Postgres adapter packages) can rely on a single coherent migration surface.

What changes

  • ADDED: migration-events module — exports a typed EventBus<MigrationEventMap> instance named migrationEvents plus a createMigrationEvents() factory; the existing MigrationEventMap interface gains scenario-named keys "migration:started", "migration:applied", "migration:failed", "migration:completed", "migration:rolled-back" as aliases for the existing migration:before / migration:after / migration:failed / migration:plan keys (existing keys preserved for one release).
  • ADDED: MigrationLockLike interface in lock/migration-lock — acquire(lockId: string, ttlMs: number): Promise<boolean>, release(lockId: string): Promise<void>, extend(lockId: string, ttlMs: number): Promise<boolean>; the existing genesis MigrationLockAdapter shape (no lockId) is preserved as a back-compat alias.
  • ADDED: MigrationStoreLike interface in store/migration-store — list(): Promise<MigrationRecord[]>, append(record: MigrationRecord): Promise<void>, markFailed(id: string, error: string): Promise<void>, markApplied(id: string): Promise<void>, getLatestVersion(): Promise<string | undefined>; the existing MigrationStoreAdapter shape (record / remove / clear) is preserved as a back-compat alias.
  • MODIFIED: MemoryMigrationLock and MemoryMigrationStore — gain implementations of the new MigrationLockLike / MigrationStoreLike interfaces alongside their existing methods; default constructions remain wire-compatible.
  • ADDED: BatchTargetStatus ("pending" | "running" | "completed" | "failed"), BatchTargetProgress, TargetConfigFactory types — exported from types/migration.types and the package root.
  • ADDED: GetAllRecordsFn type — exported from status/migration-status and the package root, used by the status dashboard for injectable history retrieval.
  • ADDED: ValidationError type alias — re-exports the existing MigrationValidationError shape under the upstream-canonical name; both names co-exist for one release.
  • MODIFIED: MIGRATION_CODES and MigrationError are guaranteed registered against the shared defineCodes registry from @suaveplan/error on module load (side-effect import); the registration is idempotent and verifiable via ErrorCodes.has("MIGRATION.LOCK_HELD").
  • ADDED: migration-events.md sibling docs and per-module .md for every new file.

Out of scope

  • SurrealDB / Postgres MigrationStoreLike adapters — the surface here defines the contract; concrete adapters land in their own change folders (migration-adapter-surrealdb, migration-adapter-postgres) so each can pull in its native client and integration test fixture independently.
  • SQL migration text DSL — pre-baked SQL templates are tracked separately; this change only stabilises the runner contracts.
  • Rollback-script auto-generation — synthesising a down from an up is a separate research item; this change keeps the existing manual down requirement.
  • Hot-reload migration discovery — file-watcher-driven re-indexing is its own change; this proposal only stabilises the in-memory store and lock contracts.

Source mapping

This change derives from the upstream package at ../suaveplan/packages/universal/migration/src and the upstream enterprise audit at ../suaveplan/enterprise/migration.md.

Requirement Source file Source lines Notes
MigrationLockLike interface (acquire(lockId, ttl), release(lockId), extend(lockId, ttl)) packages/universal/migration/src/lock/migration-lock.ts L1-L8 Verbatim port of upstream contract; renamed from MigrationLockAdapter to MigrationLockLike to follow the genesis *Like naming for ambient interfaces (see LoggerLike / StorageLike)
MigrationStoreLike interface (list, append, markFailed, markApplied, getLatestVersion) packages/universal/migration/src/store/migration-store.ts L1-L11 Verbatim port; renamed from MigrationStoreAdapter
migrationEvents shared EventBus instance packages/universal/migration/src/migration-events.ts L1-L42 Verbatim port; replace upstream EventEmitter with @suaveplan/events::EventBus per genesis convention
MigrationEventMap lifecycle keys (migration:started / migration:applied / migration:failed / migration:completed) packages/universal/migration/src/migration-events.ts L13-L41 Add as additive keys alongside genesis migration:before / migration:after / migration:plan
BatchTargetStatus type packages/universal/migration/src/batch/batch-migrator.ts L9 Verbatim
BatchTargetProgress interface packages/universal/migration/src/batch/batch-migrator.ts L11-L20 Verbatim
TargetConfigFactory type packages/universal/migration/src/batch/batch-migrator.ts L31-L34 Verbatim
GetAllRecordsFn type packages/universal/migration/src/status/migration-status.ts L61 Verbatim
ValidationError type re-export packages/universal/migration/src/types/migration.types.ts L155-L162 Re-export the genesis MigrationValidationError shape under upstream canonical name
MIGRATION_CODES registered via shared defineCodes registry packages/universal/migration/src/error-codes.ts (whole file) Side-effect import already present in index.ts; this change adds a registerMigrationErrorCodes() named export and a verification scenario

Requirements with no source line

Requirement Justification
MemoryMigrationLock and MemoryMigrationStore implement the new *Like interfaces alongside the legacy genesis ones Keeps existing genesis call sites (acquire(timeout?), record(record)) compiling for one release while exposing the upstream contract; pure-additive method set on the same class.
migration-events.md sibling doc Genesis-wide CLAUDE.md doc rule — every implementation file ships a .md.
Idempotent registerMigrationErrorCodes() (callable more than once without throwing) Genesis @suaveplan/error registry rule (CLAUDE.md §Workflow rule 2) — re-registration during HMR / multi-bundle load must not crash.

Verification approach

Each spec delta requirement carries a **Verified by:** line pointing at concrete it(...) titles. The test files that will exist after this change ships:

  • packages/database/migration/src/lock/migration-lock/migration-lock.test.ts — MigrationLockLike contract tests for MemoryMigrationLock (acquire / release / extend / TTL expiry / lockId isolation)
  • packages/database/migration/src/lock/migration-lock/migration-lock.spec-conformance.test.ts — pins back-compat alias to legacy MigrationLockAdapter
  • packages/database/migration/src/store/migration-store/migration-store.test.ts — MigrationStoreLike contract tests for MemoryMigrationStore (list / append / markFailed / markApplied / getLatestVersion / idempotency)
  • packages/database/migration/src/store/migration-store/migration-store.spec-conformance.test.ts — pins back-compat alias to legacy MigrationStoreAdapter
  • packages/database/migration/src/migration-events/migration-events.test.ts — typed event emission for every key in MigrationEventMap (started / applied / failed / completed / rolled-back)
  • packages/database/migration/src/migration-events/migration-events.spec-conformance.test.ts — listener teardown, FIFO order, error in listener does not break bus
  • packages/database/migration/src/batch/batch-migrator/batch-migrator.test.ts — BatchTargetProgress lifecycle, TargetConfigFactory invocation per target, partial failure isolation
  • packages/database/migration/src/status/migration-status/migration-status.test.ts — GetAllRecordsFn injection
  • packages/database/migration/src/errors/migration-error.spec-conformance.test.ts — MIGRATION_CODES resolves through ErrorCodes.has() after package load AND after a second registerMigrationErrorCodes() call

Impact

Affected dependents

Run bunx turbo ls --filter=...@suaveplan/migration to enumerate. Today grep -rl "@suaveplan/migration" packages/ --include="package.json" returns zero matches outside packages/database/migration/package.json itself, so the change is purely additive — existing genesis surface is preserved by the back-compat aliases.

  • @suaveplan/migration (self) — [x] tests run / [x] docs reviewed / [ ] no action needed
  • (no in-repo dependents) — verified by bunx turbo ls --filter=...@suaveplan/migration returning a single package.

If tests run is unchecked for any dependent, the change MUST NOT be folded into the canonical spec — finish that work first or split the proposal.

Migration impact

  • MigrationLockAdapter consumers — no break. The interface remains exported under its existing name; MigrationLockLike is a strictly additive companion. New code SHOULD prefer MigrationLockLike because its lockId parameter enables a single lock instance to gate multiple migration targets (the upstream batch migrator pattern).
  • MigrationStoreAdapter consumers — no break. Alias preserved; MigrationStoreLike is the canonical name going forward.
  • MigrationEventMap consumers — no break. Existing keys (migration:before, migration:after, migration:plan) preserved; new keys are additive. New code SHOULD prefer the lifecycle-named keys; the executor will continue to emit both (legacy + canonical) for one release, then drop legacy in a follow-up change.
  • MigrationValidationError consumers — no break. ValidationError is a type alias for the same shape.