@alexify/migronaut 1.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +409 -1
  2. package/README.md +248 -24
  3. package/bin/migronaut.js +11 -3
  4. package/bullmq.d.ts +845 -0
  5. package/bullmq.js +1 -0
  6. package/index.d.ts +757 -29
  7. package/migronaut.schema.json +191 -1
  8. package/package.json +27 -6
  9. package/src/bullmq/index.js +55 -0
  10. package/src/bullmq/jobs.js +454 -0
  11. package/src/bullmq/processor.js +608 -0
  12. package/src/bullmq/producer.js +424 -0
  13. package/src/bullmq/service.js +653 -0
  14. package/src/bullmq/wait.js +124 -0
  15. package/src/cli/args.js +12 -2
  16. package/src/cli/commands/baseline.js +45 -0
  17. package/src/cli/commands/converge.js +160 -0
  18. package/src/cli/commands/down.js +2 -0
  19. package/src/cli/commands/lock.js +2 -1
  20. package/src/cli/commands/redo.js +8 -1
  21. package/src/cli/commands/unlock.js +12 -2
  22. package/src/cli/commands/up.js +14 -1
  23. package/src/cli/exit-codes.js +10 -2
  24. package/src/cli/index.js +4 -0
  25. package/src/cli/shared.js +29 -7
  26. package/src/cli/table.js +105 -0
  27. package/src/core/audit.js +17 -3
  28. package/src/core/baseline.js +80 -0
  29. package/src/core/changelog.js +140 -24
  30. package/src/core/collections.js +372 -0
  31. package/src/core/config.js +125 -27
  32. package/src/core/converge-log.js +47 -0
  33. package/src/core/converge-plan.js +483 -0
  34. package/src/core/converge.js +867 -0
  35. package/src/core/import-runner.js +34 -6
  36. package/src/core/import.js +14 -7
  37. package/src/core/index-spec.js +496 -0
  38. package/src/core/lock-wait.js +260 -0
  39. package/src/core/lock.js +71 -20
  40. package/src/core/migrator.js +805 -304
  41. package/src/core/options.js +251 -0
  42. package/src/core/run-recorder.js +157 -0
  43. package/src/core/run.js +71 -71
  44. package/src/core/runner.js +70 -20
  45. package/src/core/sequence.js +134 -0
  46. package/src/errors/index.js +71 -1
  47. package/src/index.js +16 -0
  48. package/src/utils/actor.js +48 -0
  49. package/src/utils/canonical.js +179 -0
  50. package/src/utils/collection-name.js +21 -0
  51. package/src/utils/error.js +18 -1
  52. package/src/utils/id.js +77 -0
  53. package/src/utils/loader.js +39 -21
  54. package/src/utils/logger.js +30 -12
  55. package/src/utils/migration-name.js +32 -0
  56. package/src/utils/redact.js +57 -4
  57. package/src/utils/sanitize.js +8 -3
  58. package/src/utils/telemetry.js +393 -0
  59. package/src/utils/template.js +60 -12
package/index.d.ts CHANGED
@@ -51,14 +51,15 @@ export interface MigrationModule {
51
51
 
52
52
  // ─── Changelog ────────────────────────────────────────────────────────────────
53
53
 
54
- export type MigrationStatus = 'applied' | 'reverted';
54
+ export type MigrationStatus = 'applied' | 'reverted' | 'failed';
55
55
 
56
56
  /**
57
57
  * Where a changelog record originated. `'migrate-mongo'` marks a record adopted
58
- * via `migronaut import`; such records are forward-only and cannot be reverted by migronaut.
59
- * Absent (or `'migronaut'`) means a natively-applied, reversible migration.
58
+ * via `migronaut import`, `'baseline'` one stamped by `migronaut baseline`; both are
59
+ * forward-only and cannot be reverted by migronaut. Absent (or `'migronaut'`)
60
+ * means a natively-applied, reversible migration.
60
61
  */
61
- export type MigrationOrigin = 'migronaut' | 'migrate-mongo';
62
+ export type MigrationOrigin = 'migronaut' | 'migrate-mongo' | 'baseline';
62
63
 
63
64
  /** A single record in the _migronaut_migrations changelog collection */
64
65
  export interface MigrationRecord {
@@ -71,6 +72,10 @@ export interface MigrationRecord {
71
72
  /** When this migration was applied the *first* time; survives a re-apply */
72
73
  firstAppliedAt?: Date;
73
74
  revertedAt?: Date;
75
+ /** When the last failed attempt was recorded (status `'failed'` only) */
76
+ failedAt?: Date;
77
+ /** Redacted message of the last failed attempt (status `'failed'` only) */
78
+ error?: string;
74
79
  /** Execution time in milliseconds */
75
80
  duration: number;
76
81
  /** SHA-256 hash of the file at time of execution */
@@ -138,6 +143,16 @@ export interface MigrationHooks {
138
143
  /** File type a created migration is written as */
139
144
  export type MigrationExtension = 'ts' | 'js';
140
145
 
146
+ /**
147
+ * Mints one identifier. Called with **no arguments** and no `this`, so a
148
+ * third-party generator passes straight through (`generateId: ulid`,
149
+ * `generateId: createId`, `generateId: nanoid`). It must be **synchronous** and
150
+ * return a non-empty string of at most 128 characters, different on every
151
+ * call; anything else — a throw and a returned promise included — fails the
152
+ * run with a {@link ConfigInvalidError}.
153
+ */
154
+ export type IdGenerator = () => string;
155
+
141
156
  /**
142
157
  * Every **scalar** option below is also settable from the environment as
143
158
  * `MIGRONAUT_<SCREAMING_SNAKE>` (`migrationsDir` → `MIGRONAUT_MIGRATIONS_DIR`,
@@ -147,9 +162,9 @@ export type MigrationExtension = 'ts' | 'js';
147
162
  * file and are outranked by CLI flags. A value that does not parse is rejected
148
163
  * with a {@link ConfigInvalidError} naming the variable — never coerced.
149
164
  *
150
- * `fileExtensions`, `clientOptions` and the live handles (`client`, `mongoose`,
151
- * `hooks`, `logger`) are config-file/API only: a single environment string
152
- * cannot express them.
165
+ * `fileExtensions`, `clientOptions`, `collections`, `generateId` and the live
166
+ * handles (`client`, `mongoose`, `hooks`, `logger`, `telemetry`) are
167
+ * config-file/API only: a single environment string cannot express them.
153
168
  */
154
169
  export interface MigronautConfig {
155
170
  /** MongoDB connection URI. Not required when `client` is supplied */
@@ -174,6 +189,12 @@ export interface MigronautConfig {
174
189
  migrationsCollection: string;
175
190
  /** Collection name for distributed lock. Default: '_migronaut_locks' */
176
191
  lockCollection: string;
192
+ /**
193
+ * Collection holding the converge history — one entry per converge that
194
+ * changed something or failed. Created by the first such converge, never
195
+ * before. Default: '_migronaut_converge'
196
+ */
197
+ convergeLogCollection: string;
177
198
  /** How long (seconds) a lock is considered stale. Default: 60 */
178
199
  lockTTLSeconds: number;
179
200
  /**
@@ -237,11 +258,65 @@ export interface MigronautConfig {
237
258
  * - `'warn'` — log and keep going.
238
259
  */
239
260
  onLockLost?: 'abort' | 'warn';
261
+ /**
262
+ * What a bulk `up` does when a pending migration sorts before the newest
263
+ * applied one — a file merged late from a parallel branch, which would apply
264
+ * out of authoring order (environments migrated at different times then
265
+ * disagree on the effective order).
266
+ *
267
+ * - `'warn'` (default) — log the late arrivals and apply them.
268
+ * - `'error'` — refuse the run with an {@link OutOfOrderMigrationError}.
269
+ * - `'allow'` — apply silently.
270
+ *
271
+ * A single-file `up` (an explicit, deliberate target) is never checked.
272
+ */
273
+ onOutOfOrder?: 'warn' | 'error' | 'allow';
274
+ /**
275
+ * Declared collections: their indexes and validator, as the end state you
276
+ * want. `converge()` (`migronaut converge`) compares them with the live
277
+ * database and makes the difference — no migration file per change, and
278
+ * nothing recorded. Combined with the files in `collectionsDir`; a
279
+ * collection declared twice is a {@link ConfigInvalidError}. Experimental.
280
+ */
281
+ collections?: CollectionDefinition[];
282
+ /**
283
+ * Directory of collection definition files, one collection per file (a
284
+ * `.ts`/`.js` default export or a `.json` document; the collection name
285
+ * defaults to the file name). Opt-in — nothing is read unless this is set.
286
+ * Files are loaded when a converge runs, not at config resolution.
287
+ */
288
+ collectionsDir?: string;
289
+ /**
290
+ * End every bulk `up` — no file, no `to` — by converging the declared
291
+ * collections, under the same lock and even when no migration was pending.
292
+ * Default: false
293
+ */
294
+ convergeAfterUp?: boolean;
240
295
  /** Mongoose instance — required only if your migrations use Mongoose models */
241
296
  mongoose?: MongooseLike;
242
297
  hooks?: MigrationHooks;
243
298
  /** Custom logger — set to null to silence all output (useful in tests) */
244
299
  logger?: MigronautLogger | null;
300
+ /**
301
+ * Your own identifier format (ULID, CUID, UUIDv7, …) for every id migronaut
302
+ * mints: the run id — stamped on changelog records, events and log lines,
303
+ * and stored as the lock's owner token — and, through
304
+ * `@alexify/migronaut/bullmq`, the group id of an enqueue call.
305
+ * Default: `crypto.randomUUID()`.
306
+ *
307
+ * Ids are for correlation. The lock adds a token of its own, so a generator
308
+ * that repeats a value blurs which run wrote what but never lets two runs
309
+ * hold the lock at once.
310
+ */
311
+ generateId?: IdGenerator;
312
+ /**
313
+ * OpenTelemetry, from your own `@opentelemetry/api`: a tracer, a meter, or
314
+ * both. Every run and every migration becomes a span — the migration's span
315
+ * is the active one while its `up`/`down` runs, so an instrumented MongoDB
316
+ * driver nests its command spans under it — and their durations are
317
+ * recorded as histograms. Absent, `null` or empty turns it off.
318
+ */
319
+ telemetry?: MigronautTelemetry | null;
245
320
  }
246
321
 
247
322
  /**
@@ -257,6 +332,237 @@ export type MigronautConfigInput =
257
332
  | Partial<MigronautConfig>
258
333
  | (() => Partial<MigronautConfig> | Promise<Partial<MigronautConfig>>);
259
334
 
335
+ // ─── Collections (converge) ───────────────────────────────────────────────────
336
+
337
+ /** An index key direction: ascending, descending, or a special index type */
338
+ export type IndexKeyDirection = 1 | -1 | 'text' | 'hashed' | '2d' | '2dsphere';
339
+
340
+ /** Collation of an index — `locale` required, the rest as MongoDB defines them */
341
+ export interface IndexCollation {
342
+ locale: string;
343
+ caseLevel?: boolean;
344
+ caseFirst?: 'upper' | 'lower' | 'off';
345
+ strength?: 1 | 2 | 3 | 4 | 5;
346
+ numericOrdering?: boolean;
347
+ alternate?: 'non-ignorable' | 'shifted';
348
+ maxVariable?: 'punct' | 'space';
349
+ backwards?: boolean;
350
+ normalization?: boolean;
351
+ }
352
+
353
+ /**
354
+ * One declared index, in the driver's own flat `createIndexes` shape. Every
355
+ * option is checked: an unknown one is a {@link ConfigInvalidError} rather
356
+ * than dropped, because the driver drops it silently and the index would be
357
+ * built without it.
358
+ * @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
359
+ */
360
+ export interface IndexDefinition {
361
+ /**
362
+ * Field → direction, in index order. A compound key with an integer-like
363
+ * field name must be a `Map` (a plain object reorders such names), with that
364
+ * field first — the live key is read back as a plain object.
365
+ */
366
+ key: Record<string, IndexKeyDirection> | Map<string, IndexKeyDirection>;
367
+ /** Defaults to the name MongoDB generates: `email_1`, `a_1_b_-1` */
368
+ name?: string;
369
+ unique?: boolean;
370
+ sparse?: boolean;
371
+ /** Changed in place (`collMod`) — no rebuild */
372
+ hidden?: boolean;
373
+ /** TTL in seconds. Changed in place when the live index already has one */
374
+ expireAfterSeconds?: number;
375
+ partialFilterExpression?: Record<string, unknown>;
376
+ collation?: IndexCollation;
377
+ /** For a wildcard (`$**`) index */
378
+ wildcardProjection?: Record<string, 0 | 1 | boolean>;
379
+ /** Text index field weights (default 1) */
380
+ weights?: Record<string, number>;
381
+ default_language?: string;
382
+ language_override?: string;
383
+ textIndexVersion?: number;
384
+ '2dsphereIndexVersion'?: number;
385
+ bits?: number;
386
+ min?: number;
387
+ max?: number;
388
+ storageEngine?: Record<string, unknown>;
389
+ /** Accepted and ignored — a no-op since MongoDB 4.2 */
390
+ background?: boolean;
391
+ }
392
+
393
+ export type ValidationLevel = 'off' | 'strict' | 'moderate';
394
+ export type ValidationAction = 'error' | 'warn' | 'errorAndLog';
395
+
396
+ /**
397
+ * A declared collection: the end state `converge()` keeps it in. Leave
398
+ * `indexes` or `validator` out to leave that part unmanaged.
399
+ * @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
400
+ */
401
+ export interface CollectionDefinition {
402
+ name: string;
403
+ /**
404
+ * Every index besides `_id`. Undeclared live indexes are kept (and reported)
405
+ * unless `prune` is on.
406
+ */
407
+ indexes?: IndexDefinition[];
408
+ /** A query or `{ $jsonSchema }` document; `null` (or `{}`) for no validator */
409
+ validator?: Record<string, unknown> | null;
410
+ /** Default: 'strict'. Only with a validator */
411
+ validationLevel?: ValidationLevel;
412
+ /** Default: 'error'. Only with a validator */
413
+ validationAction?: ValidationAction;
414
+ /** Drop live indexes this definition does not declare. Default: the call's `prune`, else false */
415
+ prune?: boolean;
416
+ }
417
+
418
+ /** What a `collectionsDir` file exports: a definition whose name defaults to the file name */
419
+ export type CollectionDefinitionFile = Omit<CollectionDefinition, 'name'> & { name?: string };
420
+
421
+ /**
422
+ * Options for {@link MigratorKit.converge}
423
+ * @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
424
+ */
425
+ export interface ConvergeOptions {
426
+ /** Plan without writing: no lock, no events. The result's rows are `'planned'` */
427
+ dryRun?: boolean;
428
+ /** Drop undeclared indexes in collections whose definition does not set `prune` */
429
+ prune?: boolean;
430
+ /** Skip lock acquisition (dev only) */
431
+ noLock?: boolean;
432
+ /**
433
+ * Refuse ({@link MigrationBlockedError}) while any migration is still
434
+ * pending — checked under the lock. How the queue adapter runs a converge
435
+ * as the tail of a deploy.
436
+ */
437
+ ordered?: boolean;
438
+ /**
439
+ * Allow a rebuild that drops a unique index and builds a unique one back.
440
+ * Without it such a rebuild plans as a `conflict`: the constraint is gone
441
+ * until the new index is built, and a duplicate written in between leaves
442
+ * neither index buildable. The after-up hook and queue jobs never set it.
443
+ * CLI: `--rebuild-unique`.
444
+ */
445
+ rebuildUnique?: boolean;
446
+ /** Who asked for this converge — recorded in the converge history */
447
+ requestedBy?: string;
448
+ /** Why — recorded in the converge history */
449
+ reason?: string;
450
+ }
451
+
452
+ export type ConvergeTarget = 'collection' | 'validator' | 'index';
453
+
454
+ /**
455
+ * What converge does to one target. `keep` is an undeclared index left alone
456
+ * (prune off); `conflict` refuses the run — an undeclared index covers the
457
+ * declared one's key under another name, a unique index would be rebuilt
458
+ * without {@link ConvergeOptions.rebuildUnique}, or the collection is a view
459
+ * or a time-series collection.
460
+ * @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
461
+ */
462
+ export type ConvergeActionKind =
463
+ | 'create'
464
+ | 'modify'
465
+ | 'recreate'
466
+ | 'drop'
467
+ | 'keep'
468
+ | 'unchanged'
469
+ | 'conflict';
470
+
471
+ /**
472
+ * `planned` in a dry run; otherwise `applied`, `failed`, or `skipped` — no
473
+ * change was needed, or the run stopped before reaching it.
474
+ */
475
+ export type ConvergeActionStatus = 'planned' | 'applied' | 'failed' | 'skipped';
476
+
477
+ /**
478
+ * One row of a converge result
479
+ * @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
480
+ */
481
+ export interface ConvergeAction {
482
+ target: ConvergeTarget;
483
+ /** The index name; the collection name for a `collection` or `validator` row */
484
+ name: string;
485
+ action: ConvergeActionKind;
486
+ status: ConvergeActionStatus;
487
+ /** What differs (`'unique, expireAfterSeconds'`), or why a row is what it is */
488
+ reason?: string;
489
+ /** The live index the row refers to when its name differs from the declared one */
490
+ liveName?: string;
491
+ durationMs?: number;
492
+ /**
493
+ * What is there now — the live index (`{ key, name, ...options }`) or
494
+ * validator (`{ validator, validationLevel, validationAction }`) — on rows
495
+ * that change or drop it, and on `keep` rows. Plain JSON.
496
+ */
497
+ from?: Record<string, unknown>;
498
+ /** What the row puts there — the declared index or validator — on rows that create or change it */
499
+ to?: Record<string, unknown>;
500
+ }
501
+
502
+ /**
503
+ * @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
504
+ */
505
+ export interface CollectionConvergeResult {
506
+ name: string;
507
+ actions: ConvergeAction[];
508
+ }
509
+
510
+ /**
511
+ * One entry of the converge history (`convergeLogCollection`): a converge that
512
+ * changed something or failed.
513
+ * @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
514
+ */
515
+ export interface ConvergeHistoryEntry {
516
+ runId?: string;
517
+ /** `'converge'`, or `'up'` for the converge that ended a bulk `up` */
518
+ trigger: ConvergeTrigger;
519
+ startedAt: Date;
520
+ finishedAt: Date;
521
+ durationMs: number;
522
+ success: boolean;
523
+ /** Redacted failure message (`success: false` only) */
524
+ error?: string;
525
+ executedBy: string;
526
+ host: string;
527
+ environment: string;
528
+ requestedBy?: string;
529
+ reason?: string;
530
+ /** Changes applied */
531
+ changed: number;
532
+ /** The rows that changed, failed or refused the run — each with its collection and `from` / `to` */
533
+ actions: Array<ConvergeAction & { collection: string }>;
534
+ unstable?: ConvergeUnstable[];
535
+ }
536
+
537
+ /**
538
+ * Something applied that still compares as changed — reported, never rebuilt in a loop
539
+ * @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
540
+ */
541
+ export interface ConvergeUnstable {
542
+ collection: string;
543
+ target: ConvergeTarget;
544
+ name: string;
545
+ action: ConvergeActionKind;
546
+ reason?: string;
547
+ }
548
+
549
+ /**
550
+ * Outcome of {@link MigratorKit.converge}
551
+ * @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
552
+ */
553
+ export interface ConvergeResult {
554
+ dryRun: boolean;
555
+ /** Changes applied — or, in a dry run, changes the run would make */
556
+ changed: number;
557
+ /**
558
+ * True when the database matches the declarations: nothing left to do and
559
+ * no conflict. Undeclared indexes kept with prune off do not count against it.
560
+ */
561
+ inSync: boolean;
562
+ collections: CollectionConvergeResult[];
563
+ unstable?: ConvergeUnstable[];
564
+ }
565
+
260
566
  // ─── Logger ───────────────────────────────────────────────────────────────────
261
567
 
262
568
  /**
@@ -285,11 +591,97 @@ export interface MigronautLogger {
285
591
  */
286
592
  export type LogMethod = (msg: string, fields?: Record<string, unknown>) => void;
287
593
 
594
+ // ─── Telemetry ────────────────────────────────────────────────────────────────
595
+
596
+ /** A span or metric attribute value — the scalar subset migronaut sets */
597
+ export type MigronautAttributes = Record<string, string | number | boolean>;
598
+
599
+ /**
600
+ * The slice of an OpenTelemetry `Span` migronaut calls. Declared structurally —
601
+ * `@opentelemetry/api` is deliberately not imported, so the package's types
602
+ * resolve for users who never installed it. A real `Span` satisfies it.
603
+ */
604
+ export interface MigronautSpan {
605
+ setAttribute(key: string, value: string | number | boolean): unknown;
606
+ /** `code` is OpenTelemetry's `SpanStatusCode` — migronaut only ever sets ERROR (2) */
607
+ setStatus(status: { code: number; message?: string }): unknown;
608
+ end(): void;
609
+ }
610
+
611
+ /**
612
+ * The slice of an OpenTelemetry `Tracer` migronaut calls — what
613
+ * `trace.getTracer('@alexify/migronaut')` returns. Only `startActiveSpan` is
614
+ * used: it is what makes a span the active context for the migration's own
615
+ * code, and so for any instrumentation running underneath it.
616
+ */
617
+ export interface MigronautTracer {
618
+ startActiveSpan<T>(
619
+ name: string,
620
+ options: { attributes?: MigronautAttributes },
621
+ fn: (span: MigronautSpan) => T,
622
+ ): T;
623
+ }
624
+
625
+ /** An OpenTelemetry `Histogram`, as far as migronaut uses one */
626
+ export interface MigronautHistogram {
627
+ record(value: number, attributes?: MigronautAttributes): void;
628
+ }
629
+
630
+ /** An OpenTelemetry `Counter`, as far as migronaut uses one */
631
+ export interface MigronautCounter {
632
+ add(value: number, attributes?: MigronautAttributes): void;
633
+ }
634
+
635
+ /** Options migronaut passes when it creates an instrument */
636
+ export interface MigronautMetricOptions {
637
+ description?: string;
638
+ unit?: string;
639
+ /** Histogram bucket boundaries, in the instrument's unit (seconds) */
640
+ advice?: { explicitBucketBoundaries?: number[] };
641
+ }
642
+
643
+ /**
644
+ * The slice of an OpenTelemetry `Meter` migronaut calls — what
645
+ * `metrics.getMeter('@alexify/migronaut')` returns.
646
+ */
647
+ export interface MigronautMeter {
648
+ createHistogram(name: string, options?: MigronautMetricOptions): MigronautHistogram;
649
+ createCounter(name: string, options?: MigronautMetricOptions): MigronautCounter;
650
+ }
651
+
652
+ /**
653
+ * The `telemetry` config option. Both parts are optional and independent:
654
+ * a tracer alone gives spans, a meter alone gives metrics.
655
+ *
656
+ * Spans: `migronaut.run` (one per run that held the lock) and
657
+ * `migronaut.migration` (one per migration executed, a child of the run).
658
+ * Metrics: `migronaut.run.duration`, `migronaut.migration.duration` and
659
+ * `migronaut.lock.acquire.duration` (histograms, seconds), plus the counters
660
+ * `migronaut.lock.refused` and `migronaut.lock.lost`. A failure sets the span's
661
+ * status to ERROR with a redacted message, and `error.type` — on the span and
662
+ * the metric point — to the {@link MigronautErrorCode}, or for an error that is
663
+ * not migronaut's to its class name (`_OTHER` when it has none).
664
+ *
665
+ * A tracer or meter that throws never fails a run.
666
+ * @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
667
+ */
668
+ export interface MigronautTelemetry {
669
+ tracer?: MigronautTracer | null;
670
+ meter?: MigronautMeter | null;
671
+ /**
672
+ * Static attributes added to every span and every metric point — your own
673
+ * low-cardinality dimensions (`{ tenant: 'acme' }`). At most 20. They cannot
674
+ * replace migronaut's own: `db.namespace` (the database name, always
675
+ * present) and the `migronaut.*` attributes win.
676
+ */
677
+ attributes?: Record<string, string | number | boolean>;
678
+ }
679
+
288
680
  // ─── Progress Reporter ─────────────────────────────────────────────────────────
289
681
 
290
682
  /**
291
- * Receives migration lifecycle callbacks so a presentation layer (e.g. an ora
292
- * spinner) can react. Deliberately separate from {@link MigrationHooks}: hooks
683
+ * Receives migration lifecycle callbacks so a presentation layer (e.g. a
684
+ * progress spinner) can react. Deliberately separate from {@link MigrationHooks}: hooks
293
685
  * run user DB logic inside the migration; this only drives a UI indicator and
294
686
  * never touches the database.
295
687
  */
@@ -318,19 +710,59 @@ export interface RunResult {
318
710
 
319
711
  export interface StatusRow {
320
712
  file: string;
321
- status: 'applied' | 'pending';
713
+ /**
714
+ * `'failed'` marks a recorded failed attempt — the file still counts as
715
+ * pending for every run path (the next `up` retries it), but the failure is
716
+ * surfaced instead of rendering as a plain pending row. A reverted record
717
+ * reports as `'pending'`, with `revertedAt` carrying its history.
718
+ */
719
+ status: 'applied' | 'pending' | 'failed';
322
720
  batch: number | null;
323
721
  appliedAt: Date | null;
324
722
  duration: number | null;
325
723
  /** null = never applied, true = match, false = mismatch */
326
724
  checksumOk: boolean | null;
327
725
  description?: string;
726
+ /** Who ran it — from the changelog's audit trail, when recorded */
727
+ executedBy?: string;
728
+ /** Environment stamped at apply time, when recorded */
729
+ environment?: string;
730
+ /** Correlation id of the run that wrote the record, when recorded */
731
+ runId?: string;
732
+ /** When the migration was reverted — present on reverted history rows */
733
+ revertedAt?: Date;
734
+ /** `'migrate-mongo'` / `'baseline'` mark forward-only adopted records */
735
+ origin?: MigrationOrigin;
736
+ /** Redacted message of the last failed attempt (status `'failed'` only) */
737
+ error?: string;
738
+ /** When the last failed attempt was recorded (status `'failed'` only) */
739
+ failedAt?: Date;
740
+ /**
741
+ * Present (true) on a not-yet-applied row that sorts before the newest
742
+ * applied migration — a file merged late from a parallel branch, which will
743
+ * apply out of authoring order. See `MigronautConfig.onOutOfOrder`.
744
+ */
745
+ outOfOrder?: true;
328
746
  /**
329
747
  * Present (true) when the changelog record's name is not a plain filename —
330
748
  * a legacy or tampered record. The row is reported as-is instead of failing
331
749
  * the whole status/audit call.
332
750
  */
333
751
  invalid?: true;
752
+ /**
753
+ * The file's current checksum (SHA-256 hex) — on `dryRun('up')` rows, so a
754
+ * caller that applies them later can insist on exactly this version
755
+ * (`up(file, { checksum })`).
756
+ */
757
+ checksum?: string;
758
+ /** Who asked for the apply, and why — when the run said (`requestedBy` / `reason` options) */
759
+ requestedBy?: string;
760
+ reason?: string;
761
+ /** Who asked for the revert, and why — on reverted history rows */
762
+ revertRequestedBy?: string;
763
+ revertReason?: string;
764
+ /** The checksum of the file version that failed (status `'failed'` only) */
765
+ failedChecksum?: string;
334
766
  }
335
767
 
336
768
  // ─── Import (migrate-mongo adoption) ────────────────────────────────────────────
@@ -388,6 +820,13 @@ export interface LockInfo {
388
820
  host: string;
389
821
  /** Username of the holder */
390
822
  executedBy: string;
823
+ /**
824
+ * The holder's run id — the `runId` of its events, log lines and changelog
825
+ * records. Absent for a document written by hand.
826
+ */
827
+ runId?: string;
828
+ /** The holder's lock TTL (ms), which paces its heartbeat. Absent before 2.1 */
829
+ ttlMs?: number;
391
830
  }
392
831
 
393
832
  // ─── Error Codes ──────────────────────────────────────────────────────────────
@@ -411,7 +850,12 @@ export type MigronautErrorCode =
411
850
  | 'CONNECTION_FAILED'
412
851
  | 'NOT_APPLIED'
413
852
  | 'IMPORT_TARGET_NOT_EMPTY'
414
- | 'MIGRATION_IRREVERSIBLE';
853
+ | 'MIGRATION_IRREVERSIBLE'
854
+ | 'MIGRATION_OUT_OF_ORDER'
855
+ | 'MIGRATION_BLOCKED'
856
+ | 'QUEUE_JOB_INVALID'
857
+ | 'QUEUE_JOB_FAILED'
858
+ | 'CONVERGE_FAILED';
415
859
 
416
860
  // ─── Config file format ─────────────────────────────────────────────────────────
417
861
 
@@ -443,6 +887,44 @@ export interface UpOptions {
443
887
  * Mutually exclusive with a filename and `steps`.
444
888
  */
445
889
  to?: string;
890
+ /**
891
+ * Stamp this batch number on what the run applies, instead of the next free
892
+ * one ({@link MigratorKit.nextBatch}). A label, not a reservation: it may
893
+ * equal a batch already in use, which is how several single-file runs become
894
+ * one rollback unit — the queue adapter gives every job of an enqueue group
895
+ * the same value. Positive integer; mutually exclusive with `step`.
896
+ */
897
+ batch?: number;
898
+ /**
899
+ * Refuse ({@link MigrationBlockedError}) to apply the named file while an
900
+ * earlier file on disk is still pending — the invariant a bulk `up` gets by
901
+ * construction, enforced from the changelog rather than from the caller's
902
+ * memory. Also makes the single-file run honour `strict` drift checks and
903
+ * `onOutOfOrder` like a bulk run. Requires a filename.
904
+ */
905
+ ordered?: boolean;
906
+ /**
907
+ * The SHA-256 (hex) the named file must have — refuse ({@link
908
+ * ChecksumMismatchError}, `context.planned: true`) to apply any other
909
+ * version of it. A queue job carries the checksum its plan saw, so a worker
910
+ * from another deploy never applies a different file under the same name.
911
+ * Requires a filename; an already-applied file is skipped as usual.
912
+ */
913
+ checksum?: string;
914
+ /**
915
+ * Converge the declared collections after the migrations, under the same
916
+ * lock — overrides `convergeAfterUp` for this call. Bulk runs only: refused
917
+ * with a filename or `to`.
918
+ */
919
+ converge?: boolean;
920
+ /**
921
+ * Who asked for this run (≤ 128 characters) — stamped on the changelog
922
+ * records it writes. `executedBy` is the OS user that ran it; on a queue
923
+ * worker that is the container's, which is why the requester is separate.
924
+ */
925
+ requestedBy?: string;
926
+ /** Why (≤ 512 characters) — a ticket, a sentence; stamped like `requestedBy` */
927
+ reason?: string;
446
928
  }
447
929
 
448
930
  /** Options for {@link MigratorKit.down} */
@@ -463,6 +945,20 @@ export interface DownOptions {
463
945
  * to the same state. Mutually exclusive with `batch`, `steps` and a filename.
464
946
  */
465
947
  to?: string;
948
+ /**
949
+ * Refuse ({@link MigrationBlockedError}) to revert the named file while a
950
+ * migration applied *after* it is still applied — reverts must go newest
951
+ * first (by `appliedAt`, the order `steps` uses). Requires a filename.
952
+ */
953
+ ordered?: boolean;
954
+ /**
955
+ * Who asked for this run (≤ 128 characters) — stamped on the records it
956
+ * reverts (`revertRequestedBy`). `executedBy` is the OS user that ran it; on a queue
957
+ * worker that is the container's, which is why the requester is separate.
958
+ */
959
+ requestedBy?: string;
960
+ /** Why (≤ 512 characters) — a ticket, a sentence; stamped as `revertReason` */
961
+ reason?: string;
466
962
  }
467
963
 
468
964
  /** Payload common to every lifecycle event */
@@ -486,7 +982,7 @@ export interface MigrationEvent extends MigronautEventBase {
486
982
  }
487
983
 
488
984
  export interface RunStartEvent extends MigronautEventBase {
489
- /** Which command started the run: 'up' | 'down' | 'redo' | 'import' */
985
+ /** Which command started the run: 'up' | 'down' | 'redo' | 'import' | 'baseline' | 'converge' */
490
986
  command?: string;
491
987
  direction?: 'up' | 'down';
492
988
  }
@@ -520,10 +1016,62 @@ export interface LockEvent extends MigronautEventBase {
520
1016
  acquireMs?: number;
521
1017
  }
522
1018
 
1019
+ /** Who started a converge: the `converge` call itself, or a bulk `up` (`convergeAfterUp`) */
1020
+ export type ConvergeTrigger = 'converge' | 'up';
1021
+
1022
+ /**
1023
+ * @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
1024
+ */
1025
+ export interface ConvergeStartEvent extends MigronautEventBase {
1026
+ trigger: ConvergeTrigger;
1027
+ /** Declared collections being converged */
1028
+ collections: number;
1029
+ }
1030
+
1031
+ /**
1032
+ * One step a converge carried out (or failed)
1033
+ * @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
1034
+ */
1035
+ export interface ConvergeActionEvent extends MigronautEventBase {
1036
+ collection: string;
1037
+ target: ConvergeTarget;
1038
+ name: string;
1039
+ action: ConvergeActionKind;
1040
+ /**
1041
+ * `'started'` fires before the step runs — an index build can take hours,
1042
+ * and this is how a subscriber sees which one is in progress; `'applied'`
1043
+ * or `'failed'` follows when it ends.
1044
+ */
1045
+ status: 'started' | 'applied' | 'failed';
1046
+ /** `'applied'` only */
1047
+ durationMs?: number;
1048
+ reason?: string;
1049
+ /** Redacted failure message (status `'failed'` only) */
1050
+ error?: string;
1051
+ }
1052
+
1053
+ /**
1054
+ * @experimental New in 2.1 — the shape may still change in a minor release (named in the CHANGELOG).
1055
+ */
1056
+ export interface ConvergeEndEvent extends MigronautEventBase {
1057
+ trigger: ConvergeTrigger;
1058
+ success: boolean;
1059
+ durationMs: number;
1060
+ changed: number;
1061
+ inSync: boolean;
1062
+ /** Rows per action kind */
1063
+ counts: Partial<Record<ConvergeActionKind, number>>;
1064
+ /** The full result — partial on the failure path */
1065
+ result: ConvergeResult;
1066
+ /** Redacted failure message */
1067
+ error?: string;
1068
+ }
1069
+
523
1070
  /**
524
1071
  * Lifecycle events emitted by {@link MigratorKit}. Subscribe to feed metrics or
525
1072
  * alerting without parsing log lines; a listener that throws is contained and
526
- * never fails the run.
1073
+ * never fails the run. The `converge:*` events fire for real converge runs
1074
+ * only, not for a dry run.
527
1075
  */
528
1076
  export interface MigronautEvents {
529
1077
  'run:start': (event: RunStartEvent) => void;
@@ -535,6 +1083,9 @@ export interface MigronautEvents {
535
1083
  'lock:acquired': (event: LockEvent) => void;
536
1084
  'lock:released': (event: LockEvent) => void;
537
1085
  'lock:lost': (event: LockEvent) => void;
1086
+ 'converge:start': (event: ConvergeStartEvent) => void;
1087
+ 'converge:action': (event: ConvergeActionEvent) => void;
1088
+ 'converge:end': (event: ConvergeEndEvent) => void;
538
1089
  }
539
1090
 
540
1091
  /** One check performed by {@link MigratorKit.audit} */
@@ -554,10 +1105,20 @@ export interface AuditReport {
554
1105
  checks: AuditCheck[];
555
1106
  }
556
1107
 
1108
+ /** Options for {@link MigratorKit.status} and {@link MigratorKit.list} */
1109
+ export interface StatusOptions {
1110
+ /** Hash applied files to fill `checksumOk`. Default true */
1111
+ checksums?: boolean;
1112
+ }
1113
+
557
1114
  /** Options for {@link MigratorKit.redo} */
558
1115
  export interface RedoOptions {
559
1116
  /** Skip lock acquisition (dev only) */
560
1117
  noLock?: boolean;
1118
+ /** Who asked — stamped on the revert and on the re-apply */
1119
+ requestedBy?: string;
1120
+ /** Why — stamped like `requestedBy` */
1121
+ reason?: string;
561
1122
  }
562
1123
 
563
1124
  /** Options for {@link MigratorKit.create} */
@@ -585,6 +1146,24 @@ export interface InitOptions {
585
1146
  secretProvider?: boolean;
586
1147
  }
587
1148
 
1149
+ /** Options for {@link MigratorKit.baseline} */
1150
+ export interface BaselineOptions {
1151
+ /** Baseline pending files up to and including this one, instead of all */
1152
+ to?: string;
1153
+ /** Skip lock acquisition (dev only) */
1154
+ noLock?: boolean;
1155
+ }
1156
+
1157
+ /** Outcome of a {@link MigratorKit.baseline} call */
1158
+ export interface BaselineSummary {
1159
+ /** Files marked applied by this call, in name order */
1160
+ baselined: string[];
1161
+ /** Files on disk that were already applied (or beyond `--to`) and untouched */
1162
+ skipped: number;
1163
+ /** The shared batch number stamped on the baselined records; null when none */
1164
+ batch: number | null;
1165
+ }
1166
+
588
1167
  /** Options for {@link MigratorKit.import} */
589
1168
  export interface ImportOptions {
590
1169
  /** Source collection to read. Default: `changelog` (migrate-mongo's default) */
@@ -605,9 +1184,16 @@ export interface ImportOptions {
605
1184
  export interface MigratorKitOptions {
606
1185
  /** Explicit config file path — overrides auto-discovery */
607
1186
  configPath?: string;
1187
+ /**
1188
+ * Project root this instance resolves against: config-file discovery, the
1189
+ * `.env` file and a relative `migrationsDir`. Defaults to `process.cwd()`.
1190
+ * Set it when one process hosts kits for several projects, so their
1191
+ * relative paths stop sharing one global working directory.
1192
+ */
1193
+ cwd?: string;
608
1194
  /**
609
1195
  * Optional lifecycle reporter, invoked around each migration's execution so a
610
- * UI (the CLI's ora spinner) can show progress. Core never imports a spinner
1196
+ * UI (the CLI's spinner) can show progress. Core never imports a spinner
611
1197
  * library — it only calls these callbacks.
612
1198
  */
613
1199
  progress?: ProgressReporter;
@@ -650,6 +1236,19 @@ export class MigratorKit extends EventEmitter {
650
1236
  * removed, or null if no lock was held.
651
1237
  */
652
1238
  forceUnlock(): Promise<LockInfo | null>;
1239
+ /**
1240
+ * The batch number the next `up` would use (highest recorded batch + 1,
1241
+ * reverted and failed records included). A peek, not a reservation — pair it
1242
+ * with `up(name, { batch })` to stamp several single-file runs as one batch.
1243
+ * Connects if needed.
1244
+ */
1245
+ nextBatch(): Promise<number>;
1246
+ /**
1247
+ * A new id in this kit's configured format — the `generateId` option, else a
1248
+ * random UUID. The same source every run id comes from, for code that wants
1249
+ * its own ids to match. Resolves the config; does not connect.
1250
+ */
1251
+ generateId(): Promise<string>;
653
1252
  /** Run all pending migrations, or a specific named file */
654
1253
  up(filename?: string, options?: UpOptions): Promise<RunResult[]>;
655
1254
  /** Rollback the last batch, a specific batch, a specific file, or the last N steps */
@@ -679,16 +1278,22 @@ export class MigratorKit extends EventEmitter {
679
1278
  filename?: string,
680
1279
  options?: { steps?: number; batch?: number; to?: string },
681
1280
  ): Promise<StatusRow[]>;
682
- /** Full migration status for all known files and records */
683
- status(): Promise<StatusRow[]>;
1281
+ /**
1282
+ * Full migration status for all known files and records. `checksums: false`
1283
+ * skips hashing the applied files (`checksumOk` stays null).
1284
+ */
1285
+ status(options?: StatusOptions): Promise<StatusRow[]>;
684
1286
  /**
685
1287
  * Read-only health check: configuration, connectivity, transaction support,
686
1288
  * changelog indexes, lock state, checksum drift and runtime. Reports
687
1289
  * problems; fixes none of them.
688
1290
  */
689
1291
  audit(): Promise<AuditReport>;
690
- /** Filtered list of migrations. Default: 'all' */
691
- list(filter?: 'all' | 'pending' | 'applied'): Promise<StatusRow[]>;
1292
+ /**
1293
+ * Filtered list of migrations. Default: 'all'. `checksums: false` skips
1294
+ * hashing the applied files — for a caller that needs names and dates only.
1295
+ */
1296
+ list(filter?: 'all' | 'pending' | 'applied', options?: StatusOptions): Promise<StatusRow[]>;
692
1297
  /** Create a new migration file and return its absolute path */
693
1298
  create(name: string, options?: CreateOptions): Promise<string>;
694
1299
  /** Create a migronaut config file in the working directory and return its path */
@@ -698,6 +1303,33 @@ export class MigratorKit extends EventEmitter {
698
1303
  * records into our schema and writing them to `migrationsCollection`.
699
1304
  */
700
1305
  import(options?: ImportOptions): Promise<ImportResult>;
1306
+ /**
1307
+ * Adopt an existing database with no prior migration tool: mark migration
1308
+ * files on disk as applied — checksums from disk, one shared batch,
1309
+ * `origin: 'baseline'` — without executing anything. Forward-only:
1310
+ * `down`/`redo` refuse baselined records. Idempotent: already-applied names
1311
+ * are skipped, so a partial baseline can simply be re-run.
1312
+ */
1313
+ baseline(options?: BaselineOptions): Promise<BaselineSummary>;
1314
+ /**
1315
+ * Bring the declared collections (`collections`, `collectionsDir`) to their
1316
+ * declared indexes and validators. Stateless: the live database is read and
1317
+ * compared on every call; what a run changed is appended to the converge
1318
+ * history ({@link MigratorKit.convergeHistory}), which no run reads back. A
1319
+ * real run holds the migration lock; a plan with a conflict is refused before
1320
+ * any write, and a failed step throws {@link ConvergeFailedError}. Experimental.
1321
+ */
1322
+ converge(options?: ConvergeOptions): Promise<ConvergeResult>;
1323
+ /**
1324
+ * Whether a bulk `up` on this kit ends by converging: `convergeAfterUp` is
1325
+ * on and something is declared. Resolves the config; does not connect.
1326
+ */
1327
+ convergesAfterUp(): Promise<boolean>;
1328
+ /**
1329
+ * The converge history, newest first (`limit` 1–1000, default 20): one entry
1330
+ * per converge that changed something or failed. Read-only.
1331
+ */
1332
+ convergeHistory(options?: { limit?: number }): Promise<ConvergeHistoryEntry[]>;
701
1333
  }
702
1334
 
703
1335
  // ─── Programmatic entry points ─────────────────────────────────────────────────
@@ -717,13 +1349,36 @@ export interface RunMigrationsOptions extends MigratorKitOptions {
717
1349
  */
718
1350
  onLockHeld?: OnLockHeld;
719
1351
  /**
720
- * Max time (ms) to wait when `onLockHeld: 'wait'`. Default: 90000 — sized to
721
- * outlast a peer's typical run plus one lock TTL, so parallel deploys don't
722
- * give up while a healthy peer is still migrating.
1352
+ * Max time (ms) to wait when `onLockHeld: 'wait'` **without observing holder
1353
+ * progress**. While the holder's heartbeat visibly advances its lock, the
1354
+ * deadline is re-armed — a healthy peer working through a long backlog never
1355
+ * times its waiting peers out; only a stalled holder runs this budget down.
1356
+ * Default: 90000, or 1.5× the holder's lock TTL when that is longer — its
1357
+ * heartbeat only moves the lock every TTL/2, and a crashed holder's lock is
1358
+ * reclaimable only after a full TTL. An explicit value is used as given.
723
1359
  */
724
1360
  lockWaitTimeoutMs?: number;
725
- /** Poll interval (ms) while waiting for the lock. Default: 500 */
1361
+ /**
1362
+ * First poll interval (ms) while waiting for the lock. Polls back off from
1363
+ * it, doubling, up to 5 s (and never more than a quarter of the wait budget).
1364
+ * Default: 500
1365
+ */
726
1366
  lockPollIntervalMs?: number;
1367
+ /**
1368
+ * Abort the call: a wait for the lock stops between polls, and a run that
1369
+ * holds it stops between migrations (one already executing finishes), with
1370
+ * a {@link RunAbortedError}. Wire it to SIGTERM so a pod being shut down
1371
+ * does not take the lock just before it is killed.
1372
+ */
1373
+ signal?: AbortSignal;
1374
+ /**
1375
+ * Receives the internally-constructed {@link MigratorKit} right after
1376
+ * construction (before connect), so an embedding application can subscribe
1377
+ * to its lifecycle events — `kit.on('migration:success', …)` for metrics,
1378
+ * lock telemetry, runId correlation — while keeping the managed
1379
+ * connect/run/disconnect lifecycle.
1380
+ */
1381
+ onKit?: (kit: MigratorKit) => void;
727
1382
  }
728
1383
 
729
1384
  /** Outcome of a {@link runMigrations} call */
@@ -734,10 +1389,12 @@ export interface MigrationSummary {
734
1389
  upToDate: boolean;
735
1390
  /** True when this instance waited for a peer to release the lock before running */
736
1391
  waited: boolean;
737
- /** Total time (ms) spent waiting for a peer's lock. 0 when the lock was free */
1392
+ /** Time (ms) from the first refusal to the run, by the clock. 0 when the lock was free */
738
1393
  waitedMs: number;
739
1394
  /** Number of `up` attempts made — 1 when the lock was free on the first try */
740
1395
  attempts: number;
1396
+ /** The converge that ended the run — present only when `convergeAfterUp` converged */
1397
+ converge?: ConvergeResult;
741
1398
  }
742
1399
 
743
1400
  /**
@@ -761,16 +1418,34 @@ export function pendingMigrations(
761
1418
  ): Promise<StatusRow[]>;
762
1419
 
763
1420
  /**
764
- * The CLI's exit-code map: one entry per {@link MigronautErrorCode}, plus two
1421
+ * The CLI's exit-code map: one entry per {@link MigronautErrorCode}, plus three
765
1422
  * CLI-condition codes with no error class — `PENDING_MIGRATIONS` (from
766
- * `status --check`) and `AUDIT_FAILED`. Lets a wrapper script mirror the
767
- * CLI's exit semantics without hardcoding numbers. Anything unmapped exits 1;
768
- * success is 0.
1423
+ * `status --check`), `AUDIT_FAILED` and `COLLECTIONS_DRIFT` (from
1424
+ * `converge --check`). Lets a wrapper script mirror the CLI's exit semantics
1425
+ * without hardcoding numbers. Anything unmapped exits 1; success is 0.
769
1426
  */
770
1427
  export const EXIT_CODES: Readonly<
771
- Record<MigronautErrorCode | 'PENDING_MIGRATIONS' | 'AUDIT_FAILED', number>
1428
+ Record<
1429
+ MigronautErrorCode | 'PENDING_MIGRATIONS' | 'AUDIT_FAILED' | 'COLLECTIONS_DRIFT',
1430
+ number
1431
+ >
772
1432
  >;
773
1433
 
1434
+ // ─── Logger factory ───────────────────────────────────────────────────────────
1435
+
1436
+ /** Threshold accepted by {@link createLogger} — drops anything less severe */
1437
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
1438
+
1439
+ /**
1440
+ * Create the default console logger (pino-compatible surface, terminal-escape
1441
+ * sanitization and colors included). `debug`/`info` write to `stream`
1442
+ * (stdout by default); `warn`/`error` always write to stderr. For programmatic
1443
+ * callers who want migronaut's own output at a chosen verbosity — e.g.
1444
+ * `logger: createLogger(process.stdout, 'debug')` — without hand-writing a
1445
+ * four-method logger.
1446
+ */
1447
+ export function createLogger(stream?: NodeJS.WritableStream, level?: LogLevel): MigronautLogger;
1448
+
774
1449
  // ─── Errors ───────────────────────────────────────────────────────────────────
775
1450
 
776
1451
  /** Construction options shared by every migronaut error — `cause` keeps the wrapped Error */
@@ -897,7 +1572,60 @@ export class ImportTargetNotEmptyError extends MigronautError {
897
1572
  constructor(message: string, context?: Record<string, unknown>, options?: MigronautErrorOptions);
898
1573
  }
899
1574
 
900
- /** Thrown when attempting to roll back a migrate-mongo-imported (forward-only) migration */
1575
+ /** Thrown when attempting to roll back a forward-only (imported or baselined) migration */
901
1576
  export class IrreversibleMigrationError extends MigronautError {
902
1577
  constructor(message: string, context?: Record<string, unknown>, options?: MigronautErrorOptions);
903
1578
  }
1579
+
1580
+ /**
1581
+ * Thrown by a bulk `up` under `onOutOfOrder: 'error'` when a pending migration
1582
+ * sorts before the newest applied one — a file merged late from a parallel
1583
+ * branch. `context.names` lists the late arrivals.
1584
+ */
1585
+ export class OutOfOrderMigrationError extends MigronautError {
1586
+ constructor(message: string, context?: Record<string, unknown>, options?: MigronautErrorOptions);
1587
+ }
1588
+
1589
+ /**
1590
+ * Thrown by an `ordered` single-file `up`/`down` that would run out of
1591
+ * sequence: an earlier migration is still pending (`up`), or one applied later
1592
+ * is still applied (`down`). `context.name`, `context.direction` and
1593
+ * `context.blockedBy` (the migrations that must go first).
1594
+ */
1595
+ export class MigrationBlockedError extends MigronautError {
1596
+ constructor(message: string, context?: Record<string, unknown>, options?: MigronautErrorOptions);
1597
+ }
1598
+
1599
+ /**
1600
+ * Thrown by the queue adapter (`@alexify/migronaut/bullmq`) when a job's
1601
+ * payload fails the contract check — an unknown job name or data version, a
1602
+ * migration name that is not a bare filename, malformed group fields. Job data
1603
+ * is untrusted input. `context.jobId`, `context.issue`.
1604
+ */
1605
+ export class QueueJobInvalidError extends MigronautError {
1606
+ constructor(message: string, context?: Record<string, unknown>, options?: MigronautErrorOptions);
1607
+ }
1608
+
1609
+ /**
1610
+ * Thrown by a queue group's `wait()` when one of its jobs failed or the wait
1611
+ * timed out (one budget for the whole call). `context.failedReason` is the
1612
+ * worker's (redacted) message, `context.code` the job's own typed error code
1613
+ * when it reported one (`MIGRATION_BLOCKED`, `CHECKSUM_MISMATCH`, …),
1614
+ * `context.results` the jobs that finished before it, plus `groupId`, `jobId`,
1615
+ * `migration`, `direction` and `timedOut`.
1616
+ */
1617
+ export class QueueJobFailedError extends MigronautError {
1618
+ constructor(message: string, context?: Record<string, unknown>, options?: MigronautErrorOptions);
1619
+ }
1620
+
1621
+ /**
1622
+ * Thrown by {@link MigratorKit.converge} when the database cannot be brought
1623
+ * to the declared state. `context.phase` is `'plan'` for a refused plan
1624
+ * (`context.conflicts` lists why; nothing was written) or `'apply'` for a
1625
+ * failed step (`collection`, `target`, `name`, `action`, `cause`, and
1626
+ * `mongoCode`, `hint` and — after a failed rebuild — `restored` when they
1627
+ * apply). `context.converge` is the {@link ConvergeResult} so far.
1628
+ */
1629
+ export class ConvergeFailedError extends MigronautError {
1630
+ constructor(message: string, context?: Record<string, unknown>, options?: MigronautErrorOptions);
1631
+ }