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

88 lines
11 KiB
Markdown

# 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.