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,87 @@
# 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.

View File

@@ -0,0 +1,178 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: migration (migration-adapters-completion)
**Package**: `@suaveplan/migration`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions. (See `openspec/AGENTS.md` Section 13.)
## ADDED Requirements
### Requirement: MigrationLockLike interface with lockId-scoped locking
The package SHALL expose a `MigrationLockLike` interface and a default `MemoryMigrationLock` implementation that satisfies it. The interface SHALL declare `acquire(lockId: string, ttlMs: number): Promise<boolean>`, `release(lockId: string): Promise<void>`, and `extend(lockId: string, ttlMs: number): Promise<boolean>`. Multiple `lockId` values held against a single instance MUST be isolated — acquiring lockId `"a"` SHALL NOT prevent another caller from acquiring lockId `"b"`. TTL expiry SHALL automatically free the lock without an explicit `release` call.
#### Scenario: uncontested acquire returns true
- **WHEN** `lock.acquire("migrate-tenant-1", 60000)` is called against an unheld lockId
- **THEN** the call resolves to `true` AND a subsequent `lock.acquire("migrate-tenant-1", 60000)` resolves to `false`
#### Scenario: lockId isolation
- **WHEN** `lock.acquire("a", 60000)` resolves to `true`
- **THEN** `lock.acquire("b", 60000)` also resolves to `true`
#### Scenario: extend refreshes TTL
- **WHEN** a lock is acquired with `ttlMs: 50` and `extend(lockId, 5000)` is called before expiry
- **THEN** the lock remains held 100ms after the original expiry would have fired
#### Scenario: TTL expiry auto-releases
- **WHEN** a lock is acquired with `ttlMs: 50` and no `release` or `extend` is called
- **THEN** after 100ms, a fresh `acquire(lockId, 60000)` resolves to `true`
#### Scenario: legacy MigrationLockAdapter alias preserved
- **WHEN** consumer code imports `MigrationLockAdapter` from `@suaveplan/migration`
- **THEN** the import resolves to the same shape it had before this change (no break)
**Verified by:** `packages/database/migration/src/lock/migration-lock/migration-lock.test.ts::"acquire returns true on uncontested lockId"` AND `packages/database/migration/src/lock/migration-lock/migration-lock.test.ts::"isolates separate lockIds"` AND `packages/database/migration/src/lock/migration-lock/migration-lock.test.ts::"extend refreshes TTL"` AND `packages/database/migration/src/lock/migration-lock/migration-lock.test.ts::"expired lockId auto-releases"` AND `packages/database/migration/src/lock/migration-lock/migration-lock.spec-conformance.test.ts::"preserves MigrationLockAdapter alias"`
---
### Requirement: MigrationStoreLike interface with ledger persistence contract
The package SHALL expose a `MigrationStoreLike` interface and a default `MemoryMigrationStore` implementation that satisfies it. The interface SHALL declare `list(): Promise<MigrationRecord[]>`, `append(record: MigrationRecord): Promise<void>`, `markFailed(id: string, error: string): Promise<void>`, `markApplied(id: string): Promise<void>`, and `getLatestVersion(): Promise<string | undefined>`. The default in-memory implementation SHALL be deterministic — `list()` always returns records sorted by `MigrationRecord.id` ascending.
#### Scenario: empty store lists no records
- **WHEN** `store.list()` is called on a fresh `MemoryMigrationStore`
- **THEN** the result is `[]`
#### Scenario: append then list returns single record
- **WHEN** `store.append({ id: "001", name: "init", appliedAt: 0, direction: "up" })` is called and then `store.list()` is called
- **THEN** the result is `[{ id: "001", name: "init", appliedAt: 0, direction: "up" }]`
#### Scenario: markApplied flips status
- **WHEN** a record is appended with status `"pending"` and `markApplied("001")` is called
- **THEN** `list()` returns the record with `status: "applied"`
#### Scenario: markFailed records error
- **WHEN** `markFailed("001", "constraint violation")` is called against an existing record
- **THEN** `list()` returns the record with `status: "failed"` AND `error: "constraint violation"`
#### Scenario: getLatestVersion returns max id
- **WHEN** records `"001"`, `"002"`, `"003"` are appended in any order
- **THEN** `getLatestVersion()` returns `"003"`
#### Scenario: legacy MigrationStoreAdapter alias preserved
- **WHEN** consumer code imports `MigrationStoreAdapter` from `@suaveplan/migration`
- **THEN** the import resolves to the same shape it had before this change (no break)
**Verified by:** `packages/database/migration/src/store/migration-store/migration-store.test.ts::"empty store returns no records"` AND `packages/database/migration/src/store/migration-store/migration-store.test.ts::"append then list returns the record"` AND `packages/database/migration/src/store/migration-store/migration-store.test.ts::"markApplied flips status to applied"` AND `packages/database/migration/src/store/migration-store/migration-store.test.ts::"markFailed records error string"` AND `packages/database/migration/src/store/migration-store/migration-store.test.ts::"getLatestVersion returns the highest id"` AND `packages/database/migration/src/store/migration-store/migration-store.spec-conformance.test.ts::"preserves MigrationStoreAdapter alias"`
---
### Requirement: migrationEvents shared event bus with lifecycle keys
The package SHALL export a typed `migrationEvents: EventBus<MigrationEventMap>` instance built on `@suaveplan/events::EventBus`. The `MigrationEventMap` type SHALL include lifecycle-named keys `"migration:started"`, `"migration:applied"`, `"migration:failed"`, `"migration:completed"`, and `"migration:rolled-back"` alongside the existing `"migration:before"`, `"migration:after"`, `"migration:plan"` keys. The `MigrationExecutor` SHALL emit both the legacy and the canonical key for each lifecycle transition until the legacy keys are removed in a follow-up change.
#### Scenario: started event fires before up()
- **WHEN** the executor begins applying a migration
- **THEN** subscribers to `"migration:started"` receive `{ migration, direction: "up" }` BEFORE the migration's `up` function is invoked
#### Scenario: applied event fires on success
- **WHEN** a migration's `up` resolves successfully
- **THEN** subscribers to `"migration:applied"` receive `{ migration, direction, durationMs }` and `"migration:after"` continues to fire (back-compat)
#### Scenario: failed event fires on rejection
- **WHEN** a migration's `up` rejects
- **THEN** subscribers to `"migration:failed"` receive `{ migration, direction, error }` AND the executor still throws
#### Scenario: completed event fires once per run
- **WHEN** the runner finishes a `run()` invocation (success or failure)
- **THEN** subscribers to `"migration:completed"` receive `{ summary, durationMs }` exactly once
#### Scenario: listener throwing does not break the bus
- **WHEN** a registered listener throws synchronously while handling `"migration:started"`
- **THEN** the executor continues, subsequent listeners still fire, and the migration runs
**Verified by:** `packages/database/migration/src/migration-events/migration-events.test.ts::"emits migration:started before up"` AND `packages/database/migration/src/migration-events/migration-events.test.ts::"emits migration:applied on success"` AND `packages/database/migration/src/migration-events/migration-events.test.ts::"emits migration:failed on rejection"` AND `packages/database/migration/src/migration-events/migration-events.test.ts::"emits migration:completed once per run"` AND `packages/database/migration/src/migration-events/migration-events.spec-conformance.test.ts::"listener throwing does not abort emit loop"`
---
### Requirement: BatchTargetStatus / BatchTargetProgress / TargetConfigFactory exports
The package SHALL export `BatchTargetStatus` (`"pending" | "running" | "completed" | "failed"`), `BatchTargetProgress` (`{ targetId, status, results, error?, startedAt?, completedAt? }`), and `TargetConfigFactory` (`(targetId: string) => MigrationRunnerConfig | Promise<MigrationRunnerConfig>`). The `BatchMigrator` SHALL emit a `BatchTargetProgress` update via an optional `onProgress` callback at every status transition.
#### Scenario: progress transitions through pending → running → completed
- **WHEN** `BatchMigrator.run()` processes a target whose runner succeeds
- **THEN** `onProgress` is called three times in order with statuses `"pending"`, `"running"`, `"completed"`
#### Scenario: failure isolates from siblings
- **WHEN** target `"a"` rejects and target `"b"` succeeds in the same batch
- **THEN** `onProgress` for `"a"` ends in `"failed"` AND `onProgress` for `"b"` ends in `"completed"`
#### Scenario: TargetConfigFactory invoked per target
- **WHEN** `BatchMigratorConfig.configFactory` is supplied with `targetIds: ["a", "b"]`
- **THEN** the factory is called exactly twice — once per `targetId`
**Verified by:** `packages/database/migration/src/batch/batch-migrator/batch-migrator.test.ts::"emits progress through pending, running, completed"` AND `packages/database/migration/src/batch/batch-migrator/batch-migrator.test.ts::"isolates failure to a single target"` AND `packages/database/migration/src/batch/batch-migrator/batch-migrator.test.ts::"invokes TargetConfigFactory once per target id"`
---
### Requirement: GetAllRecordsFn injectable history retrieval
The package SHALL export `GetAllRecordsFn = () => Promise<MigrationRecord[]>` and the `MigrationStatusDashboard` SHALL accept an optional `getAllRecords?: GetAllRecordsFn` constructor parameter. When the parameter is supplied, the dashboard SHALL call it to retrieve history; when absent, the dashboard SHALL fall back to `config.store.list()`.
#### Scenario: injected factory used for history
- **WHEN** the dashboard is constructed with a `getAllRecords` that returns a fixed array
- **THEN** `dashboard.getHistory()` returns that array without invoking `store.list()`
#### Scenario: fallback to store.list when factory absent
- **WHEN** the dashboard is constructed without `getAllRecords`
- **THEN** `dashboard.getHistory()` invokes `store.list()` and returns its result
**Verified by:** `packages/database/migration/src/status/migration-status/migration-status.test.ts::"uses injected getAllRecords when supplied"` AND `packages/database/migration/src/status/migration-status/migration-status.test.ts::"falls back to store.list when factory absent"`
---
### Requirement: ValidationError type alias and MIGRATION_CODES registration
The package SHALL re-export the existing `MigrationValidationError` shape under the upstream-canonical name `ValidationError` from the package root and from `types/migration.types`. The package SHALL guarantee that `MIGRATION_CODES` are registered against the shared `defineCodes` registry from `@suaveplan/error` on module load (side-effect import) and SHALL expose an idempotent `registerMigrationErrorCodes()` function for downstream consumers that bundle without side-effect imports. Calling `registerMigrationErrorCodes()` more than once SHALL NOT throw.
#### Scenario: ValidationError resolves to MigrationValidationError shape
- **WHEN** consumer code imports `ValidationError` from `@suaveplan/migration`
- **THEN** the imported type is structurally identical to `MigrationValidationError` and assignable in both directions
#### Scenario: error codes registered after package load
- **WHEN** `import "@suaveplan/migration"` has executed
- **THEN** `ErrorCodes.has("MIGRATION.LOCK_HELD")` returns `true` for every code in `MIGRATION_CODES`
#### Scenario: re-registration is idempotent
- **WHEN** `registerMigrationErrorCodes()` is called a second time after the side-effect import has already registered the codes
- **THEN** the call does not throw and `ErrorCodes.has(...)` continues to report `true`
**Verified by:** `packages/database/migration/src/errors/migration-error.spec-conformance.test.ts::"ValidationError is structurally MigrationValidationError"` AND `packages/database/migration/src/errors/migration-error.spec-conformance.test.ts::"ErrorCodes.has resolves every MIGRATION code after package load"` AND `packages/database/migration/src/errors/migration-error.spec-conformance.test.ts::"registerMigrationErrorCodes is idempotent"`
<!-- markdownlint-enable MD024 -->

View File

@@ -0,0 +1,119 @@
# Tasks — Migration adapters completion
> Every checkbox MUST reflect work actually done in this change. Do not retroactively tick boxes for work that was already in main when this change opened. Do not tick a box for a test that doesn't exist or doesn't pin the requirement it claims to verify.
## 0. Pre-flight (non-negotiable)
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
## Audit Note (2026-05-02)
Status: **DEFERRED**. None of the proposed additive surfaces exist: no `migrationEvents` shared bus, no `MigrationLockLike` / `MigrationStoreLike` interfaces (existing `MigrationLockAdapter` / `MigrationStoreAdapter` shapes are untouched), no `BatchTargetProgress` / `BatchTargetStatus` / `TargetConfigFactory` types, no `GetAllRecordsFn`, no `ValidationError` alias, no `registerMigrationErrorCodes()` named export. Lifecycle event-key aliases (`migration:started`, `migration:applied`, `migration:completed`, `migration:rolled-back`) are absent — code only emits `migration:before` / `migration:after` / `migration:failed` / `migration:plan`. 0% of work is done.
## 1. Spec authoring
- [ ] 1.1 Read `../suaveplan/enterprise/migration.md` end-to-end
- [ ] 1.2 Read `openspec/specs/migration/spec.md` (does not exist yet — confirm and start fresh)
- [ ] 1.3 Read `openspec/AGENTS.md` Sections 13 and 14
- [ ] 1.4 Draft `proposal.md` from `openspec/templates/proposal.md`
- [ ] 1.5 Fill in `## Source mapping` table — every requirement cites a line range OR appears in the "no source line" table with justification
- [ ] 1.6 Draft `specs/migration/spec.md` from `openspec/templates/spec-delta.md`
- [ ] 1.7 Every requirement in the spec delta carries a `**Verified by:**` line naming concrete `it(...)` titles
## 2. Implementation — `lock/migration-lock`
- [ ] 2.1 Create `packages/database/migration/src/lock/migration-lock/migration-lock.ts` exporting the `MigrationLockLike` interface (`acquire(lockId, ttlMs)`, `release(lockId)`, `extend(lockId, ttlMs)`)
- [ ] 2.2 Re-export the existing `MigrationLockAdapter` from this module under the same name for back-compat
- [ ] 2.3 Add a `migration-lock.md` sibling doc covering both interface contracts (≥200 words, basic + advanced examples)
- [ ] 2.4 Update `MemoryMigrationLock` to implement `MigrationLockLike` alongside the existing `MigrationLockAdapter` shape (additive methods only)
- [ ] 2.5 Update `lock/index.ts` to export `MigrationLockLike` from the package root via `index.ts`
## 3. Implementation — `store/migration-store`
- [ ] 3.1 Create `packages/database/migration/src/store/migration-store/migration-store.ts` exporting the `MigrationStoreLike` interface (`list`, `append`, `markFailed`, `markApplied`, `getLatestVersion`)
- [ ] 3.2 Re-export the existing `MigrationStoreAdapter` from this module under the same name for back-compat
- [ ] 3.3 Add a `migration-store.md` sibling doc (≥200 words, basic + advanced examples)
- [ ] 3.4 Update `MemoryMigrationStore` to implement `MigrationStoreLike` alongside the existing `MigrationStoreAdapter` shape (additive methods only)
- [ ] 3.5 Update `store/index.ts` to export `MigrationStoreLike` from the package root via `index.ts`
## 4. Implementation — `migration-events`
- [ ] 4.1 Create `packages/database/migration/src/migration-events/migration-events.ts` exporting `migrationEvents: EventBus<MigrationEventMap>` plus `createMigrationEvents()` factory
- [ ] 4.2 Extend `MigrationEventMap` in `types/migration.types.ts` with the lifecycle keys: `"migration:started"`, `"migration:applied"`, `"migration:failed"`, `"migration:completed"`, `"migration:rolled-back"` (additive — preserve `migration:before` / `migration:after` / `migration:plan`)
- [ ] 4.3 Update `MigrationExecutor` to emit both legacy AND canonical lifecycle keys for one release; add `// genesis: legacy emit removed in follow-up change` comment (no `@deprecated` per CLAUDE.md rule)
- [ ] 4.4 Add a `migration-events.md` sibling doc (≥200 words, basic + advanced examples) covering the event-bus subscription pattern
- [ ] 4.5 Wire `migrationEvents` into `index.ts` exports
## 5. Implementation — batch types
- [ ] 5.1 Add `BatchTargetStatus = "pending" | "running" | "completed" | "failed"` type alias to `types/migration.types.ts`
- [ ] 5.2 Add `BatchTargetProgress` interface (`targetId`, `status`, `results`, `error?`, `startedAt?`, `completedAt?`)
- [ ] 5.3 Add `TargetConfigFactory = (targetId: string) => MigrationRunnerConfig | Promise<MigrationRunnerConfig>` type alias
- [ ] 5.4 Update `BatchMigratorConfig` to accept either the existing `targets` array OR a `configFactory` + `targetIds` pair (additive)
- [ ] 5.5 Update `BatchMigrator` to emit `BatchTargetProgress` updates via an optional `onProgress?: (progress: BatchTargetProgress) => void` callback
- [ ] 5.6 Re-export the new types from `index.ts`
- [ ] 5.7 Update `batch-migrator.md` sibling doc to cover the new factory pattern
## 6. Implementation — status `GetAllRecordsFn`
- [ ] 6.1 Add `GetAllRecordsFn = () => Promise<MigrationRecord[]>` type alias to `status/migration-status.ts`
- [ ] 6.2 Update `MigrationStatusDashboard` constructor to accept an optional `getAllRecords?: GetAllRecordsFn` parameter; falls back to `store.list()` when absent
- [ ] 6.3 Re-export `GetAllRecordsFn` from `index.ts`
- [ ] 6.4 Update `migration-status.md` sibling doc to describe the injection point
## 7. Implementation — `ValidationError` re-export
- [ ] 7.1 Add `export type { MigrationValidationError as ValidationError }` to `types/migration.types.ts`
- [ ] 7.2 Update `index.ts` to export both `MigrationValidationError` and `ValidationError`
## 8. Implementation — `MIGRATION_CODES` registration
- [ ] 8.1 Confirm the side-effect import in `index.ts` already triggers `defineCodes` registration on load
- [ ] 8.2 Export an idempotent `registerMigrationErrorCodes()` function from `errors/migration-error.ts` for downstream consumers that bundle without side-effect imports
- [ ] 8.3 Re-running `registerMigrationErrorCodes()` MUST NOT throw — guard with the registry's existing dedup
- [ ] 8.4 Re-export `registerMigrationErrorCodes` from `index.ts`
## 9. Tests
- [ ] 9.1 `lock/migration-lock/migration-lock.test.ts` — acquire returns true on uncontested lockId, false when contested; release frees lockId; extend refreshes TTL; multiple lockIds isolated; expired locks auto-release
- [ ] 9.2 `lock/migration-lock/migration-lock.spec-conformance.test.ts` — pins `MigrationLockAdapter` back-compat alias; verifies `MemoryMigrationLock` satisfies BOTH interfaces
- [ ] 9.3 `store/migration-store/migration-store.test.ts` — empty list initial; append → list returns single; markApplied flips status; markFailed records error; getLatestVersion returns latest by id sort
- [ ] 9.4 `store/migration-store/migration-store.spec-conformance.test.ts` — pins `MigrationStoreAdapter` back-compat alias; verifies `MemoryMigrationStore` satisfies BOTH interfaces
- [ ] 9.5 `migration-events/migration-events.test.ts` — every lifecycle key emits with the correct payload shape; multiple subscribers receive in registration order
- [ ] 9.6 `migration-events/migration-events.spec-conformance.test.ts` — listener throwing does not abort emit loop; off() removes specific listener; clear() removes all
- [ ] 9.7 `batch/batch-migrator/batch-migrator.test.ts` — `BatchTargetProgress` transitions pending → running → completed; failure transitions to failed and isolates from siblings; `onProgress` called per transition
- [ ] 9.8 `status/migration-status/migration-status.test.ts` — `GetAllRecordsFn` injected source returned from `getHistory()`; absent factory falls back to `store.list()`
- [ ] 9.9 `errors/migration-error.spec-conformance.test.ts` — `ErrorCodes.has("MIGRATION.LOCK_HELD")` is true after package import; second `registerMigrationErrorCodes()` call does not throw
- [ ] 9.10 Every `it(...)` in 9.1–9.9 carries `{ timeout: 5000 }` (or 30000 for genuinely async)
## 10. Documentation
- [ ] 10.1 Sibling `.md` for every new implementation `.ts` (`migration-lock.md`, `migration-store.md`, `migration-events.md`); each ≥200 words with basic + advanced examples
- [ ] 10.2 Update `packages/database/migration/README.md` — add Modules links for the three new files; add Quick-start snippet showing `migrationEvents` subscription
- [ ] 10.3 Update `CLAUDE.md` package listing line for `@suaveplan/migration` if the public-surface description has changed
## 11. Pre-archive verification (the 5-gate triple)
- [ ] 11.1 `bun run check` — Biome 0 errors / 0 warnings / 0 info across the whole monorepo
- [ ] 11.2 `bunx turbo lint typecheck test --filter=@suaveplan/migration` — green
- [ ] 11.3 `bunx turbo test:coverage --filter=@suaveplan/migration` — 100/100/100/100
- [ ] 11.4 `bun scripts/openspec-conformance.ts @suaveplan/migration` — exit 0, no `error` findings
- [ ] 11.5 `bunx turbo build --filter=@suaveplan/migration` — green
## 12. Pre-archive 7-row checklist (AGENTS.md Section 13)
- [ ] 12.1 Generics threaded; `expectTypeOf` assertions on every literal-type claim (lifecycle key strings, `BatchTargetStatus` union)
- [ ] 12.2 Zod schemas wired at every declared boundary (`MigrationLockLike` / `MigrationStoreLike` consumers do not need Zod — interfaces are TS-only; verify schemas are still wired in `MigrationRunnerOptionsSchema` etc.)
- [ ] 12.3 Every error code in `errors/migration-error.ts` thrown by at least one production path
- [ ] 12.4 Strict-mode behaviors tested both on and off (lock TTL expiry honoured under `enableTtlEnforcement: true` and bypassed when `false`)
- [ ] 12.5 OTel instruments emitted from production paths and asserted with a real in-memory `MeterProvider`
- [ ] 12.6 Lifecycle dedup tested where claimed (idempotent `registerMigrationErrorCodes`)
- [ ] 12.7 No retroactive tick — every box above reflects work done in this change
## 13. Archive
- [ ] 13.1 Fold spec delta into `openspec/specs/migration/spec.md` (will be the first time this canonical spec exists)
- [ ] 13.2 Move `openspec/changes/migration-adapters-completion/` → `openspec/archive/<yyyy-mm-dd>-migration-adapters-completion/`
- [ ] 13.3 Refresh `MANIFEST.md` if package set or deps changed (no expected change here)