Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,60 @@
# surrealdb-adapter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`SurrealDbAdapter` adapts a SurrealDB instance to the minimal `execute`/`query` interface required by `DatabaseMigrationStore` and migration context objects. Because SurrealDB uses named query variables rather than positional parameters, this adapter translates positional `params` arrays to `{ p0, p1, … }` named variable maps automatically. This allows the same migration SQL templates to work across SQLite (positional `?`) and SurrealDB (named `$p0`) by using a consistent calling convention.
## Features
- `query<T>(sql, params?)` — executes a SurrealDB query and returns typed results.
- `execute(sql, params?)` — executes a non-query statement; always returns `{ rowsAffected: 0 }` (SurrealDB does not expose row counts).
- Translates positional params array to named vars (`p0`, `p1`, …).
- Wraps errors in `MigrationError(MIGRATION.STORE_ERROR)` or `MIGRATION.FAILED`.
## Usage: Basic Example
```ts
import { SurrealDbAdapter } from "@suaveplan/migration";
import { DatabaseMigrationStore } from "@suaveplan/migration";
import Surreal from "surrealdb";
const db = new Surreal();
await db.connect("http://localhost:8000");
const adapter = new SurrealDbAdapter({ db });
const store = new DatabaseMigrationStore({ adapter });
```
## Usage: Advanced Example
```ts
import { SurrealDbAdapter } from "@suaveplan/migration";
import { MigrationRunner, MemoryMigrationStore } from "@suaveplan/migration";
const adapter = new SurrealDbAdapter({ db: surrealInstance });
const store = new MemoryMigrationStore(); // or DatabaseMigrationStore
const runner = new MigrationRunner({ migrations, store });
```
## API Reference
### `class SurrealDbAdapter`
**Constructor**: `(config: SurrealDbAdapterConfig)`
- `config.db` — object implementing `query<T>(sql, vars?): Promise<T[]>`.
### `query<T>(sql: string, params?: readonly unknown[]): Promise<T[]>`
Translates params to named vars and delegates to `db.query`. Returns an empty array if the result is not an array.
### `execute(sql: string, params?: readonly unknown[]): Promise<{ rowsAffected: number }>`
Executes SQL via `db.query`. Always returns `{ rowsAffected: 0 }`.
## Implementation Notes
SurrealDB's `query` method returns an array of result sets per SQL statement. This adapter flattens the outer array and returns the first result set. For multi-statement SQL, callers should split statements and call `query` for each.

View File

@@ -0,0 +1,64 @@
# batch-migrator
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`BatchMigrator` orchestrates migrations across multiple database targets simultaneously. It is useful in multi-tenant architectures where each tenant has a separate database, or in microservice deployments where several service databases must be migrated as part of a single release. The migrator processes targets in configurable concurrency batches using `Promise.allSettled`, so a single target failure does not block others.
## Features
- Configurable `concurrency` (default 5) — processes this many targets in parallel per batch.
- `Promise.allSettled` semantics — partial failures are collected rather than aborting the run.
- Returns `{ results, failed, succeeded }` with a map from target name to `MigrationResult`.
- AbortSignal forwarding to each runner.
## Usage: Basic Example
```ts
import { BatchMigrator } from "@suaveplan/migration";
const migrator = new BatchMigrator({ targets: [
{ name: "tenant-a", runner: runnerA },
{ name: "tenant-b", runner: runnerB },
] });
const result = await migrator.run();
// result.succeeded = ["tenant-a", "tenant-b"]
// result.failed = []
```
## Usage: Advanced Example
```ts
import { BatchMigrator } from "@suaveplan/migration";
const migrator = new BatchMigrator({ targets: tenantRunners, concurrency: 10 });
const result = await migrator.run({ signal: controller.signal });
if (result.failed.length > 0) {
console.error(`Failed targets: ${result.failed.join(", ")}`);
}
```
## API Reference
### `class BatchMigrator`
**Constructor**: `(config: BatchMigratorConfig)`
- `config.targets` — array of `{ name: string; runner: MigrationRunnerLike }`.
- `config.concurrency?` — parallel target count per batch (default 5).
### `run(options?: RunOptions): Promise<BatchMigrationResult>`
Returns:
- `results: Map<string, MigrationResult>` — per-target outcome.
- `failed: string[]` — names of targets that threw.
- `succeeded: string[]` — names of targets that resolved.
## Implementation Notes
The concurrency loop slices `targets` into chunks of `concurrency` and awaits each chunk before starting the next. This provides bounded parallelism without spinning up all targets at once. Failed targets receive a stub `MigrationResult` with `failed: [name]` to ensure the results map is always complete.

View File

@@ -0,0 +1,64 @@
# migration-error
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the structured error class and stable error-code registry for the `@suaveplan/migration` package. All thrown errors in this package extend `MigrationError`, which itself extends `SuaveplanError` from `@suaveplan/error`, ensuring that every error carries a machine-readable code, an optional cause chain, and OTel-compatible span events.
Using a dedicated error hierarchy makes it straightforward for callers to distinguish migration failures from other application errors, and to implement targeted recovery logic (e.g. retry on lock timeouts, alert on checksum mismatches).
## Features
- `MIGRATION_CODES` — frozen record mapping reason strings to `MIGRATION.<REASON>` codes, built with `defineCodes`.
- `MigrationError` — concrete error class that accepts one of the defined codes and a human-readable message, forwarding optional `cause` through the SuaveplanError chain.
- Full TypeScript literal typing: `MigrationErrorCode` is inferred from the keys of `MIGRATION_CODES`.
## Usage: Basic Example
```ts
import { MigrationError, MIGRATION_CODES } from "@suaveplan/migration/errors/migration-error";
throw new MigrationError(
MIGRATION_CODES.FAILED,
"Migration 001_create_users failed during execution",
{ cause: originalError },
);
```
## Usage: Advanced Example
```ts
import { MigrationError, MIGRATION_CODES } from "@suaveplan/migration/errors/migration-error";
function assertChecksum(stored: string, computed: string, id: string): void {
if (stored !== computed) {
throw new MigrationError(
MIGRATION_CODES.CHECKSUM_MISMATCH,
`Checksum mismatch for migration ${id}: stored=${stored} computed=${computed}`,
);
}
}
```
## API Reference
### `MIGRATION_CODES`
Frozen record: `{ FAILED, LOCK_TIMEOUT, CHECKSUM_MISMATCH, NOT_FOUND, STORE_ERROR, PLAN_INVALID, ABORTED, DRY_RUN }`.
Each value is the string `"MIGRATION.<REASON>"`.
### `MigrationErrorCode`
Union type of all values in `MIGRATION_CODES`.
### `class MigrationError`
Extends `SuaveplanError<MigrationErrorCode>`.
**Constructor**: `(code: MigrationErrorCode, message: string, options?: { cause?: unknown })`
## Implementation Notes
`defineCodes` from `@suaveplan/error` builds the code map at module load time and freezes it, preventing accidental mutation. The `cause` option is only forwarded when it is an `Error` instance, matching `SuaveplanError`'s convention.

View File

@@ -0,0 +1,61 @@
# distributed-lock
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides a `DistributedMigrationLock` that coordinates migration locking across multiple application instances using a pluggable `DistributedLockAdapter`. Also exports `InMemoryDistributedLockAdapter` for testing and single-node use. The distributed lock uses the same exponential back-off polling strategy as the memory lock, delegating actual lock storage to the adapter.
## Features
- `DistributedMigrationLock` — delegates to any `DistributedLockAdapter` with configurable `key` and `ttl`.
- `InMemoryDistributedLockAdapter` — TTL-based in-memory adapter for testing without external infrastructure.
- Exponential back-off polling from 50 ms up to 1 s ceiling.
## Usage: Basic Example
```ts
import { DistributedMigrationLock, InMemoryDistributedLockAdapter } from "@suaveplan/migration";
const lock = new DistributedMigrationLock({
adapter: new InMemoryDistributedLockAdapter(),
key: "app_migration_lock",
ttl: 60_000,
});
const acquired = await lock.acquire(30_000);
```
## Usage: Advanced Example
```ts
import { DistributedMigrationLock } from "@suaveplan/migration";
import type { DistributedLockAdapter } from "@suaveplan/migration";
class PgAdvisoryLockAdapter implements DistributedLockAdapter {
async acquire(key: string, ttl: number) { /* pg_try_advisory_lock */ return true; }
async release(key: string) { /* pg_advisory_unlock */ }
}
const lock = new DistributedMigrationLock({ adapter: new PgAdvisoryLockAdapter() });
```
## API Reference
### `class DistributedMigrationLock implements MigrationLockAdapter`
**Constructor**: `(config: DistributedLockConfig)`
- `config.adapter` — `DistributedLockAdapter` implementation.
- `config.key?` — lock key (default `"migration_lock"`).
- `config.ttl?` — lock TTL in ms (default `60_000`).
### `class InMemoryDistributedLockAdapter implements DistributedLockAdapter`
- `acquire(key, ttl): Promise<boolean>` — acquires if no unexpired entry for `key` exists.
- `release(key): Promise<void>` — removes the entry.
## Implementation Notes
`isLocked` uses a probe key (`<key>__probe`) with zero TTL to test whether anything is competing for the lock — this avoids reading lock state directly, which the interface does not expose.

View File

@@ -0,0 +1,57 @@
# memory-lock
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`MemoryMigrationLock` implements `MigrationLockAdapter` using an in-process boolean flag. It prevents concurrent migration runs within a single Node.js process — useful during development and in single-instance deployments. Multi-instance deployments should use `DistributedMigrationLock` or `RedisDistributedLock` instead.
## Features
- `acquire(timeoutMs?)` — polls with exponential back-off (10 ms → 500 ms ceiling) until the lock is available or the timeout expires.
- `release()` — unsets the lock flag immediately.
- `isLocked()` — synchronous check returning the current lock state.
## Usage: Basic Example
```ts
import { MemoryMigrationLock } from "@suaveplan/migration";
const lock = new MemoryMigrationLock();
const acquired = await lock.acquire(5000);
if (acquired) {
// run migrations
await lock.release();
}
```
## Usage: Advanced Example
```ts
import { MemoryMigrationLock } from "@suaveplan/migration";
const lock = new MemoryMigrationLock();
// Attempt to acquire with a short timeout
const acquired = await lock.acquire(100);
if (!acquired) {
throw new Error("Could not acquire migration lock within 100 ms");
}
try {
// critical section
} finally {
await lock.release();
}
```
## API Reference
### `class MemoryMigrationLock implements MigrationLockAdapter`
- `acquire(timeoutMs?: number): Promise<boolean>` — returns `true` if acquired within the timeout (default 30 000 ms), `false` otherwise.
- `release(): Promise<void>` — always resolves, sets locked flag to `false`.
- `isLocked(): Promise<boolean>` — returns current locked state.
## Implementation Notes
The polling loop uses exponential back-off starting at 10 ms and capped at 500 ms to balance responsiveness with CPU efficiency. `Math.max(0, ms)` guards against negative delays at the deadline boundary.

View File

@@ -0,0 +1,68 @@
# redis-lock
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`RedisDistributedLock` implements `MigrationLockAdapter` using Redis `SET NX PX` semantics. This provides a distributed advisory lock suitable for multi-instance deployments where a shared Redis server is available. The implementation requires only a minimal Redis client interface (`RedisLockClient`) with `set` and `del` methods, avoiding any hard dependency on a specific Redis client library.
## Features
- `SET NX PX` lock acquisition — atomic test-and-set with TTL expiry.
- Exponential back-off polling from 50 ms up to 1 s ceiling.
- Configurable `key`, `ttl`, and acquisition `timeoutMs`.
- `isLocked()` probe using a disposable key to avoid reading lock state directly.
## Usage: Basic Example
```ts
import { RedisDistributedLock } from "@suaveplan/migration";
import { createClient } from "redis";
const redisClient = createClient();
await redisClient.connect();
const lock = new RedisDistributedLock({
client: redisClient,
key: "app_migration_lock",
ttl: 30_000,
});
const acquired = await lock.acquire(10_000);
if (acquired) {
try { /* run migrations */ } finally { await lock.release(); }
}
```
## Usage: Advanced Example
```ts
import { RedisDistributedLock } from "@suaveplan/migration";
// Inject a stub Redis client in tests
const stubClient = {
async set() { return "OK"; },
async del() { return 1; },
};
const lock = new RedisDistributedLock({ client: stubClient });
```
## API Reference
### `class RedisDistributedLock implements MigrationLockAdapter`
**Constructor**: `(config: RedisLockConfig)`
- `config.client` — Redis-compatible client with `set(key, value, opts)` and `del(key)` methods.
- `config.key?` — lock key (default `"migration_lock"`).
- `config.ttl?` — lock TTL in ms (default `60_000`).
- `acquire(timeoutMs?): Promise<boolean>` — polls until acquired or timeout.
- `release(): Promise<void>` — deletes the lock key.
- `isLocked(): Promise<boolean>` — uses a probe key to check lock state.
## Implementation Notes
The `isLocked` probe key (`<key>__probe`) uses a 1 ms TTL and is immediately deleted if acquired, minimising leakage. This approach avoids a Redis `GET` which the minimal `RedisLockClient` interface does not expose.

View File

@@ -0,0 +1,56 @@
# migration-planner
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The migration planner produces `MigrationPlan` objects that describe which migrations should be applied or rolled back, without executing them. Separating planning from execution makes dry-run reporting straightforward, enables validation before any database writes, and simplifies unit testing by removing I/O dependencies from the planning step.
## Features
- `sortMigrations(migrations)` — lexicographic sort by `id`, putting numeric prefixes in ascending order.
- `planUp(all, applied)` — returns all pending (not-yet-applied) migrations sorted ascending.
- `planDown(all, applied, steps?)` — returns the last N applied migrations in reverse (rollback) order.
- `checksumForMigration(migration)` — returns the stored checksum or computes one from the up function body.
## Usage: Basic Example
```ts
import { planUp, sortMigrations } from "@suaveplan/migration";
const plan = planUp(allMigrations, []);
// plan.toApply contains all migrations in ascending id order
```
## Usage: Advanced Example
```ts
import { planDown } from "@suaveplan/migration";
// Rollback only the last 2 applied migrations
const plan = planDown(allMigrations, appliedRecords, 2);
// plan.toRollback[0] is the most recently applied migration
```
## API Reference
### `sortMigrations(migrations: Migration[]): Migration[]`
Returns a new array sorted by `id` ascending. Does not mutate the input.
### `planUp(all: Migration[], applied: MigrationRecord[]): MigrationPlan`
Returns `{ toApply: Migration[], toRollback: [] }`. `toApply` contains migrations whose `id` is not in `applied`.
### `planDown(all: Migration[], applied: MigrationRecord[], steps?: number): MigrationPlan`
Returns `{ toApply: [], toRollback: Migration[] }`. `toRollback` is at most `steps` migrations (all if omitted), most-recent first.
### `checksumForMigration(migration: Migration): string`
Returns `migration.checksum` if set, otherwise computes SHA-256 of `migration.up.toString()`.
## Implementation Notes
`planDown` reverses the sorted applied list before slicing, ensuring rollbacks proceed in most-recent-first order (LIFO). `sortMigrations` uses `localeCompare` for reliable multi-character prefix handling.

View File

@@ -0,0 +1,56 @@
# dry-run-reporter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`DryRunReporter` collects per-migration entries during a dry-run pass and produces a structured `DryRunReport` with an aggregate risk level. It allows callers to understand which migrations would be applied, their individual risk levels, and the worst-case overall risk without executing any SQL. This is particularly valuable in CI pipelines and change-review workflows.
## Features
- `record(entry)` — accumulates a `DryRunEntry` (migrationId, direction, riskLevel, optional reason).
- `report()` — returns a snapshot of all entries, the computed total risk, and a human-readable summary.
- Risk level aggregation: `totalRisk` is the maximum across all entries (`low` < `medium` < `high`).
- Summary string differentiates between `up` and `down` counts.
## Usage: Basic Example
```ts
import { DryRunReporter } from "@suaveplan/migration";
const reporter = new DryRunReporter();
reporter.record({ migrationId: "001", direction: "up", riskLevel: "low" });
const report = reporter.report();
// report.totalRisk === "low"
// report.summary === "Dry run: 1 up migration(s) would be applied. Overall risk: low."
```
## Usage: Advanced Example
```ts
import { DryRunReporter } from "@suaveplan/migration";
const reporter = new DryRunReporter();
reporter.record({ migrationId: "001", direction: "up", riskLevel: "low", reason: "add table" });
reporter.record({ migrationId: "002", direction: "up", riskLevel: "high", reason: "drop column" });
const report = reporter.report();
// report.totalRisk === "high"
```
## API Reference
### `class DryRunReporter`
- `record(entry: DryRunEntry): void` — appends an entry. `entry` fields: `migrationId`, `direction`, `riskLevel` (`"low"|"medium"|"high"`), optional `reason`.
- `report(): DryRunReport` — returns `{ entries, totalRisk, summary }`.
### `DryRunReport`
- `entries: DryRunEntry[]` — snapshot of all recorded entries.
- `totalRisk: RiskLevel` — maximum risk level across all entries.
- `summary: string` — human-readable description.
## Implementation Notes
`RISK_ORDER` maps risk levels to integers for comparison. `report()` returns a spread copy of `entries` so callers cannot mutate the internal list. When no entries are recorded, `totalRisk` is `"low"` and the summary says "No migrations to apply."

View File

@@ -0,0 +1,61 @@
# migration-executor
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`MigrationExecutor` takes a `MigrationPlan` and actually runs each migration in the specified direction, recording results in the store, emitting lifecycle events, and surfacing structured errors. It is the only component in the migration package that performs database writes. Separating execution from planning makes it easy to test each in isolation and to slot in different execution strategies (e.g. transactional, concurrent) without changing the planner logic.
## Features
- Runs `toApply` (direction `up`) or `toRollback` (direction `down`) from the plan.
- AbortSignal cancellation between each migration step.
- Checksum validation before applying already-recorded migrations.
- Dry-run mode: records which migrations would run without executing SQL.
- Event emission: `migration:plan`, `migration:before`, `migration:after`, `migration:failed`.
- Wraps all thrown errors in `MigrationError(MIGRATION.FAILED)` unless already a `MigrationError`.
## Usage: Basic Example
```ts
import { MigrationExecutor } from "@suaveplan/migration";
const executor = new MigrationExecutor({ store, context, dryRun: false });
const result = await executor.execute(plan, "up");
```
## Usage: Advanced Example
```ts
import { MigrationExecutor } from "@suaveplan/migration";
import { EventBus } from "@suaveplan/events";
const events = new EventBus<MigrationEventMap>();
events.on("migration:after", ({ migration, durationMs }) => {
console.log(`${migration.id} applied in ${durationMs}ms`);
});
const executor = new MigrationExecutor({ store, context, events });
const result = await executor.execute(plan, "up", { signal });
```
## API Reference
### `class MigrationExecutor`
**Constructor**: `(options: ExecutorOptions, clock?: Clock)`
- `options.store` — migration store adapter for recording results.
- `options.context` — migration context injected into each migration function.
- `options.events?` — optional event bus for lifecycle events.
- `options.dryRun?` — if `true`, collects applied IDs without running SQL (default `false`).
- `options.checksumValidation?` — if `true`, validates checksums on up-direction runs (default `true`).
### `execute(plan, direction, runOptions?): Promise<MigrationResult>`
Returns `{ applied, skipped, failed, direction, dryRun }`. Throws `MigrationError` on abort, checksum mismatch, or execution failure.
## Implementation Notes
Checksum validation runs only for `up` migrations that already have a ledger record with a checksum — it does not block the first application. The `down` direction requires `migration.down` to be defined; if absent, a `MIGRATION.NOT_FOUND` error is thrown.

View File

@@ -0,0 +1,84 @@
# migration-runner
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`MigrationRunner` is the top-level orchestrator for the migration package. It wires together the planner, executor, store, lock, and event bus into a single high-level API. Consumers create a runner with their migration list and store adapter, then call `up()` to apply pending migrations, `down()` to roll back, `status()` to inspect the current state, and `validate()` to verify migration metadata consistency before running.
## Features
- `up(options?)` — plans and applies all pending migrations; respects advisory locks, AbortSignal, and dry-run mode.
- `down(options?)` — plans and rolls back the last N applied migrations.
- `status()` — returns combined status of all registered migrations.
- `validate()` — checks for duplicate IDs and returns structured validation errors.
- Injectable `Clock`, `LoggerLike`, `EventBus`, and `MigrationLockAdapter`.
- `createMigrationRunner` factory function for ergonomic construction.
## Usage: Basic Example
```ts
import { MigrationRunner, MemoryMigrationStore } from "@suaveplan/migration";
const store = new MemoryMigrationStore();
const runner = new MigrationRunner({ migrations, store });
const result = await runner.up();
// result.applied lists migration IDs that were applied
```
## Usage: Advanced Example
```ts
import { MigrationRunner, MemoryMigrationStore, MemoryMigrationLock } from "@suaveplan/migration";
import { EventBus } from "@suaveplan/events";
const store = new MemoryMigrationStore();
const lock = new MemoryMigrationLock();
const events = new EventBus<MigrationEventMap>();
events.on("migration:after", ({ migration, durationMs }) => {
console.log(`Applied ${migration.id} in ${durationMs}ms`);
});
const runner = new MigrationRunner({ migrations, store, lock, events, checksumValidation: true });
await runner.up({ signal: abortController.signal });
```
## API Reference
### `class MigrationRunner`
**Constructor**: `(config: MigrationRunnerConfig, clock?: Clock)`
- `config.migrations` — array of `Migration` objects.
- `config.store` — `MigrationStoreAdapter` for ledger persistence.
- `config.lock?` — optional `MigrationLockAdapter`.
- `config.logger?` — optional `LoggerLike` for structured log output.
- `config.events?` — optional `EventBus<MigrationEventMap>`.
- `config.dryRun?` — default `false`.
- `config.checksumValidation?` — default `true`.
### `up(options?: RunOptions): Promise<MigrationResult>`
Returns `{ applied, skipped, failed, direction: "up", dryRun }`.
### `down(options?: RunOptions & { steps?: number }): Promise<MigrationResult>`
Returns `{ applied, skipped, failed, direction: "down", dryRun }`.
### `status(): Promise<MigrationStatusEntry[]>`
Returns status for every registered migration sorted by id.
### `validate(): Promise<ValidationResult>`
Returns `{ valid, errors }`.
### `createMigrationRunner(config, clock?): MigrationRunner`
Factory function — equivalent to `new MigrationRunner(config, clock)`.
## Implementation Notes
The `buildContext()` method provides a no-op `execute`/`query` implementation suitable for runners that do not need direct database access in migration functions (e.g. when the migration functions close over an external adapter). Consumers requiring SQL execution in migration functions should provide a custom context via a subclass.

View File

@@ -0,0 +1,46 @@
# migration.schemas
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Zod schemas for all public option types in `@suaveplan/migration`. Providing schemas alongside the TypeScript types enables runtime validation of configuration objects, safe deserialization of stored migration state, and structured error messages when callers pass invalid options. Every schema is strict (no unrecognised keys allowed at runtime), ensuring forward compatibility does not accidentally silently accept typo'd option names.
## Features
- `RunOptionsSchema` — validates `signal` (AbortSignal) and `target` (string) run options.
- `MigrationRunnerOptionsSchema` — validates `dryRun` and `checksumValidation` booleans with defaults.
- `DistributedLockConfigSchema` — validates distributed lock `key` and `ttl` options.
- `RedisLockConfigSchema` — validates Redis lock `key` and `ttl` options.
- `BatchMigratorConfigSchema` — validates `concurrency` with a positive-integer constraint and default 5.
- `MemoryStorageOptionsSchema` — validates optional `maxSize` for the in-memory store LRU limit.
## Usage: Basic Example
```ts
import { MigrationRunnerOptionsSchema } from "@suaveplan/migration";
const opts = MigrationRunnerOptionsSchema.parse({ dryRun: true });
// opts.dryRun === true, opts.checksumValidation === true (default)
```
## Usage: Advanced Example
```ts
import { BatchMigratorConfigSchema } from "@suaveplan/migration";
const result = BatchMigratorConfigSchema.safeParse({ concurrency: 0 });
if (!result.success) {
console.error(result.error.format());
// concurrency must be a positive integer
}
```
## API Reference
All exports are `z.ZodObject` instances. Use `.parse()` for throwing validation, `.safeParse()` for non-throwing. All schemas are strict — extra keys cause validation failures.
## Implementation Notes
Schemas use `AbortSignal` via `z.instanceof(AbortSignal)` which requires the schema to be evaluated in an environment where `AbortSignal` is a global. All modern JS runtimes and Node.js ≥ 15 provide this. Numeric constraints use `.positive()` which rejects zero and negatives.

View File

@@ -0,0 +1,56 @@
# migration-status
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`MigrationStatusDashboard` provides a read-only view over the migration store. It computes per-migration status, aggregate counts, and a health check that can be wired into application readiness probes. Separating status reporting from the runner ensures that status queries never trigger migrations and can be called at any time without locking concerns.
## Features
- `getAll()` — returns every registered migration with its current status (`pending`, `applied`, `rolled-back`).
- `getSummary()` — returns aggregate counts for total, applied, pending, and failed migrations.
- `healthCheck()` — returns `{ healthy, pendingCount, failedCount }` suitable for a `/healthz` endpoint.
- `getHistory(id)` — returns all ledger entries for a specific migration id (supports audit queries).
## Usage: Basic Example
```ts
import { MigrationStatusDashboard, MemoryMigrationStore } from "@suaveplan/migration";
const dashboard = new MigrationStatusDashboard({ store, migrations });
const summary = await dashboard.getSummary();
// { total: 5, applied: 4, pending: 1, failed: 0 }
```
## Usage: Advanced Example
```ts
import { MigrationStatusDashboard } from "@suaveplan/migration";
const dashboard = new MigrationStatusDashboard({ store, migrations });
const health = await dashboard.healthCheck();
if (!health.healthy) {
console.warn(`${health.pendingCount} pending migrations, ${health.failedCount} failed`);
}
```
## API Reference
### `class MigrationStatusDashboard`
**Constructor**: `(config: StatusConfig)`
- `config.store` — `MigrationStoreAdapter`.
- `config.migrations` — registered `Migration[]`.
- `config.clock?` — injectable clock.
- `getAll(): Promise<ExtendedMigrationStatus[]>` — sorted by id.
- `getSummary(): Promise<MigrationSummary>` — `{ total, applied, pending, failed }`.
- `healthCheck(): Promise<MigrationHealthCheck>` — `{ healthy, pendingCount, failedCount }`.
- `getHistory(id: string): Promise<MigrationHistoryEntry[]>` — ledger entries for one migration.
## Implementation Notes
`healthy` is `true` only when both `pending === 0` and `failed === 0`. This is intentionally strict: unnapplied migrations in production are a deployment concern that should always be visible. The `getHistory` method supports audit use cases where the same migration may have been applied and rolled back multiple times.

View File

@@ -0,0 +1,76 @@
# database-store
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`DatabaseMigrationStore` implements `MigrationStoreAdapter` against any SQL database that provides a minimal `query`/`execute` interface. It creates a ledger table (default name `_migrations`) using `CREATE TABLE IF NOT EXISTS`, making it idempotent on every call. Records are upserted with `INSERT OR REPLACE` for SQLite compatibility; Postgres adapters should use `ON CONFLICT DO UPDATE` via a custom tableName or subclass.
## Features
- `ensureTable()` — creates the ledger table if it does not exist; called automatically before every operation.
- Full `MigrationStoreAdapter` implementation: `getApplied`, `record`, `remove`, `clear`.
- Wraps all database errors in `MigrationError(MIGRATION_CODES.STORE_ERROR)` with the original as `cause`.
- Configurable `tableName` (default `_migrations`).
## Usage: Basic Example
```ts
import { DatabaseMigrationStore } from "@suaveplan/migration";
const store = new DatabaseMigrationStore({ adapter: myDb });
const applied = await store.getApplied();
```
## Usage: Advanced Example
```ts
import { DatabaseMigrationStore } from "@suaveplan/migration";
const store = new DatabaseMigrationStore({
adapter: sqliteAdapter,
tableName: "_schema_versions",
});
await store.record({
id: "001_init",
name: "Initial schema",
appliedAt: Date.now(),
checksum: "abc123",
direction: "up",
});
```
## API Reference
### `class DatabaseMigrationStore implements MigrationStoreAdapter`
**Constructor**: `(config: DatabaseStoreConfig)`
- `config.adapter` — object with `query<T>(sql, params?): Promise<T[]>` and `execute(sql, params?): Promise<{ rowsAffected: number }>`.
- `config.tableName?` — override the default `_migrations` ledger table name.
### `ensureTable(): Promise<void>`
Creates the ledger table if absent. Idempotent.
### `getApplied(): Promise<MigrationRecord[]>`
Returns all ledger rows ordered by `applied_at` ascending.
### `record(record: MigrationRecord): Promise<void>`
Upserts a ledger row (`INSERT OR REPLACE`).
### `remove(id: string): Promise<void>`
Deletes the ledger row for `id`.
### `clear(): Promise<void>`
Deletes all ledger rows.
## Implementation Notes
The `DatabaseStoreDb` interface is intentionally minimal so any adapter — SQLite, Postgres, MySQL — can implement it without pulling in the full `DatabaseAdapter` type from `@suaveplan/db`. The `ensureTable` call on every operation ensures the table exists even if the store is constructed before the database is fully initialised. This adds a tiny overhead per operation, but the cost is negligible compared to actual SQL execution.

View File

@@ -0,0 +1,54 @@
# memory-store
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`MemoryMigrationStore` implements `MigrationStoreAdapter` using an in-process `Map<string, MigrationRecord>`. It is the zero-dependency store used in unit tests, integration tests, CLI tools, and any scenario where persistence across process restarts is not required. Because it holds no external connections, it is always available and never fails with connectivity errors.
## Features
- Full `MigrationStoreAdapter` implementation: `getApplied`, `record`, `remove`, `clear`.
- Thread-safe within a single event-loop tick (no race conditions in async contexts without concurrent modification).
- Zero-allocation `getApplied()` returns a snapshot array, not the live map.
## Usage: Basic Example
```ts
import { MemoryMigrationStore } from "@suaveplan/migration";
const store = new MemoryMigrationStore();
await store.record({ id: "001", name: "init", appliedAt: Date.now(), direction: "up" });
const applied = await store.getApplied();
// applied has one entry
```
## Usage: Advanced Example
```ts
import { MemoryMigrationStore } from "@suaveplan/migration";
import { MigrationRunner } from "@suaveplan/migration";
const store = new MemoryMigrationStore();
const runner = new MigrationRunner({ migrations, store });
await runner.up();
// After: store holds all applied records
await store.clear();
// Store is now empty — clean state for next test
```
## API Reference
### `class MemoryMigrationStore implements MigrationStoreAdapter`
All methods are async to match the adapter interface, though none perform I/O.
- `getApplied(): Promise<MigrationRecord[]>` — returns a snapshot of all recorded migrations.
- `record(record: MigrationRecord): Promise<void>` — upserts a record by `id`.
- `remove(id: string): Promise<void>` — deletes a record by `id`; no-op if not present.
- `clear(): Promise<void>` — removes all records.
## Implementation Notes
Records are stored with `id` as the map key, making upsert semantics natural. `getApplied` spreads the map values into a new array so callers cannot mutate the internal store by modifying the returned array.

View File

@@ -0,0 +1,64 @@
# migration.types
## Purpose
Central type definitions for the `@suaveplan/migration` package. All interfaces, unions, and type aliases consumed by the runner, planner, executor, store adapters, lock adapters, dry-run reporter, batch migrator, and status dashboard originate here. Keeping types in a single file enables tree-shaking (zero runtime bytes) and prevents circular-dependency risks across the feature modules.
## Features
- `Migration` — the core unit: an `id`, `name`, `up` function (async, receives `MigrationContext`), optional `down`, and optional pre-computed `checksum`.
- `MigrationContext` — injected into each migration function: `execute`, `query`, and a scoped `logger`.
- `MigrationRecord` — the ledger row type persisted by `MigrationStoreAdapter`.
- `MigrationPlan` — produced by the planner: `toApply` and `toRollback` arrays.
- `MigrationResult` — returned by `MigrationRunner.up` / `.down`: applied/skipped/failed id lists.
- Adapter interfaces: `MigrationStoreAdapter`, `MigrationLockAdapter`, `DistributedLockAdapter`.
- `MigrationEventMap` — typed event names for the event bus integration.
- `BatchMigratorConfig` / `BatchMigrationResult` — multi-target orchestration types.
- `StatusConfig` / `ExtendedMigrationStatus` / `MigrationSummary` / `MigrationHealthCheck` — dashboard types.
## Usage: Basic Example
```ts
import type { Migration, MigrationContext } from "@suaveplan/migration";
const createUsers: Migration = {
id: "001_create_users",
name: "Create users table",
async up(ctx: MigrationContext): Promise<void> {
await ctx.execute("CREATE TABLE users (id TEXT PRIMARY KEY)");
},
};
```
## Usage: Advanced Example
```ts
import type {
Migration,
MigrationStoreAdapter,
MigrationRecord,
MigrationRunnerConfig,
} from "@suaveplan/migration";
class MyStore implements MigrationStoreAdapter {
private records: MigrationRecord[] = [];
async getApplied() { return [...this.records]; }
async record(r: MigrationRecord) { this.records.push(r); }
async remove(id: string) { this.records = this.records.filter(r => r.id !== id); }
async clear() { this.records = []; }
}
const config: MigrationRunnerConfig = {
migrations: [createUsers],
store: new MyStore(),
checksumValidation: true,
};
```
## API Reference
See individual type definitions in the source. All are plain TypeScript interfaces with no runtime code — zero bytes in the compiled bundle.
## Implementation Notes
Types are placed in a `.types.ts` file and excluded from coverage thresholds, following the project convention. The `MigrationEventMap` uses string-literal keys matching the pattern `"migration:<event>"` for consistency with other event maps in the project.

View File

@@ -0,0 +1,53 @@
# checksum
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides SHA-256 checksum utilities for the migration package. The checksum system detects when a migration's source has been modified after it was applied to a database, preventing accidental schema drift from silent SQL edits. Using `node:crypto` directly avoids introducing a dependency on `@suaveplan/crypto-hash`, keeping the migration package's dependency graph minimal.
## Features
- `computeChecksum(content)` — returns the SHA-256 hex digest of any string.
- `checksumMigration(up)` — serialises a migration's `up` function to its source string and hashes it.
## Usage: Basic Example
```ts
import { computeChecksum } from "@suaveplan/migration";
const hash = computeChecksum("CREATE TABLE users (id TEXT PRIMARY KEY)");
// "a4c2f..." (64-char hex string)
```
## Usage: Advanced Example
```ts
import { checksumMigration } from "@suaveplan/migration";
async function validateChecksums(migrations, appliedRecords) {
for (const migration of migrations) {
const record = appliedRecords.find(r => r.id === migration.id);
if (!record?.checksum) continue;
const computed = checksumMigration(migration.up);
if (computed !== record.checksum) {
throw new Error(`Checksum mismatch for ${migration.id}`);
}
}
}
```
## API Reference
### `computeChecksum(content: string): string`
Returns the lowercase hex SHA-256 digest of `content`. Uses `node:crypto` synchronously.
### `checksumMigration(up: (ctx: unknown) => Promise<void>): string`
Calls `up.toString()` to obtain the function source and passes it to `computeChecksum`. The result is deterministic for a given function body.
## Implementation Notes
`Function.prototype.toString()` is mandated by the ECMAScript specification to return the original source text (or a representation of it). In practice, transpilers and minifiers may change function bodies; for best reliability, checksums should be computed from source-file content in a pre-commit hook rather than at runtime in production builds.