@prisma-next/cli 0.5.0-dev.4 → 0.5.0-dev.41

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 (165) hide show
  1. package/README.md +56 -21
  2. package/dist/agent-skill-mongo.md +63 -31
  3. package/dist/agent-skill-postgres.md +1 -1
  4. package/dist/cli-errors-By1iVE3z.mjs +34 -0
  5. package/dist/cli-errors-By1iVE3z.mjs.map +1 -0
  6. package/dist/{cli-errors-C0JhVj0c.d.mts → cli-errors-DDeVsP2Y.d.mts} +1 -0
  7. package/dist/cli.mjs +123 -15
  8. package/dist/cli.mjs.map +1 -1
  9. package/dist/{client-TG7rbCWT.mjs → client-1JqqkiC7.mjs} +45 -20
  10. package/dist/client-1JqqkiC7.mjs.map +1 -0
  11. package/dist/commands/contract-emit.d.mts.map +1 -1
  12. package/dist/commands/contract-emit.mjs +2 -2
  13. package/dist/commands/contract-infer.d.mts.map +1 -1
  14. package/dist/commands/contract-infer.mjs +2 -2
  15. package/dist/commands/db-init.d.mts.map +1 -1
  16. package/dist/commands/db-init.mjs +10 -9
  17. package/dist/commands/db-init.mjs.map +1 -1
  18. package/dist/commands/db-schema.mjs +5 -5
  19. package/dist/commands/db-sign.mjs +7 -7
  20. package/dist/commands/db-update.mjs +9 -9
  21. package/dist/commands/db-update.mjs.map +1 -1
  22. package/dist/commands/db-verify.mjs +9 -9
  23. package/dist/commands/migration-apply.d.mts +5 -2
  24. package/dist/commands/migration-apply.d.mts.map +1 -1
  25. package/dist/commands/migration-apply.mjs +55 -56
  26. package/dist/commands/migration-apply.mjs.map +1 -1
  27. package/dist/commands/migration-new.d.mts.map +1 -1
  28. package/dist/commands/migration-new.mjs +26 -32
  29. package/dist/commands/migration-new.mjs.map +1 -1
  30. package/dist/commands/migration-plan.d.mts +14 -5
  31. package/dist/commands/migration-plan.d.mts.map +1 -1
  32. package/dist/commands/migration-plan.mjs +45 -48
  33. package/dist/commands/migration-plan.mjs.map +1 -1
  34. package/dist/commands/migration-ref.d.mts +1 -1
  35. package/dist/commands/migration-ref.d.mts.map +1 -1
  36. package/dist/commands/migration-ref.mjs +6 -10
  37. package/dist/commands/migration-ref.mjs.map +1 -1
  38. package/dist/commands/migration-show.d.mts +13 -7
  39. package/dist/commands/migration-show.d.mts.map +1 -1
  40. package/dist/commands/migration-show.mjs +27 -29
  41. package/dist/commands/migration-show.mjs.map +1 -1
  42. package/dist/commands/migration-status.d.mts +23 -5
  43. package/dist/commands/migration-status.d.mts.map +1 -1
  44. package/dist/commands/migration-status.mjs +3 -3
  45. package/dist/{config-loader-_W4T21X1.mjs → config-loader-ih8ViDb_.mjs} +2 -2
  46. package/dist/config-loader-ih8ViDb_.mjs.map +1 -0
  47. package/dist/config-loader.mjs +1 -1
  48. package/dist/contract-emit-LjzCoicC.mjs +4 -0
  49. package/dist/contract-emit-RZBWzkop.mjs +329 -0
  50. package/dist/contract-emit-RZBWzkop.mjs.map +1 -0
  51. package/dist/contract-emit-rt_Nmdwq.mjs +150 -0
  52. package/dist/contract-emit-rt_Nmdwq.mjs.map +1 -0
  53. package/dist/{contract-enrichment-CGW6mm-E.mjs → contract-enrichment-4Ptgw3Pe.mjs} +1 -1
  54. package/dist/{contract-enrichment-CGW6mm-E.mjs.map → contract-enrichment-4Ptgw3Pe.mjs.map} +1 -1
  55. package/dist/{contract-infer-BS4kIX9c.mjs → contract-infer-Cf5J2wVg.mjs} +11 -19
  56. package/dist/contract-infer-Cf5J2wVg.mjs.map +1 -0
  57. package/dist/exports/control-api.d.mts +86 -21
  58. package/dist/exports/control-api.d.mts.map +1 -1
  59. package/dist/exports/control-api.mjs +5 -5
  60. package/dist/exports/index.mjs +3 -3
  61. package/dist/exports/init-output.d.mts +39 -0
  62. package/dist/exports/init-output.d.mts.map +1 -0
  63. package/dist/exports/init-output.mjs +3 -0
  64. package/dist/{framework-components-DfZKQBQ2.mjs → framework-components-Bgcre3Z6.mjs} +2 -2
  65. package/dist/{framework-components-DfZKQBQ2.mjs.map → framework-components-Bgcre3Z6.mjs.map} +1 -1
  66. package/dist/init-C7dE9KOJ.mjs +2062 -0
  67. package/dist/init-C7dE9KOJ.mjs.map +1 -0
  68. package/dist/{inspect-live-schema-BsoFVoS1.mjs → inspect-live-schema-LWtXfxm_.mjs} +9 -9
  69. package/dist/inspect-live-schema-LWtXfxm_.mjs.map +1 -0
  70. package/dist/migration-cli.d.mts +41 -11
  71. package/dist/migration-cli.d.mts.map +1 -1
  72. package/dist/migration-cli.mjs +308 -84
  73. package/dist/migration-cli.mjs.map +1 -1
  74. package/dist/{migration-command-scaffold-DOXnheFa.mjs → migration-command-scaffold-CU452v9h.mjs} +7 -7
  75. package/dist/{migration-command-scaffold-DOXnheFa.mjs.map → migration-command-scaffold-CU452v9h.mjs.map} +1 -1
  76. package/dist/{migration-status-Ry3TnEya.mjs → migration-status-DoPrFIOQ.mjs} +114 -57
  77. package/dist/migration-status-DoPrFIOQ.mjs.map +1 -0
  78. package/dist/{migrations-fU0xoKjS.mjs → migrations-MEoKMiV5.mjs} +42 -21
  79. package/dist/migrations-MEoKMiV5.mjs.map +1 -0
  80. package/dist/output-BpcQrnnq.mjs +103 -0
  81. package/dist/output-BpcQrnnq.mjs.map +1 -0
  82. package/dist/{progress-adapter-B-YvmcDu.mjs → progress-adapter-DgRGldpT.mjs} +1 -1
  83. package/dist/{progress-adapter-B-YvmcDu.mjs.map → progress-adapter-DgRGldpT.mjs.map} +1 -1
  84. package/dist/quick-reference-mongo.md +34 -13
  85. package/dist/quick-reference-postgres.md +11 -9
  86. package/dist/{result-handler-BJwA7ufw.mjs → result-handler-Ch6hVnOo.mjs} +35 -93
  87. package/dist/result-handler-Ch6hVnOo.mjs.map +1 -0
  88. package/dist/{terminal-ui-C5k88MmW.mjs → terminal-ui-u2YgKghu.mjs} +76 -2
  89. package/dist/terminal-ui-u2YgKghu.mjs.map +1 -0
  90. package/dist/{verify-bl__PkXk.mjs → verify-BT9tgCOH.mjs} +2 -2
  91. package/dist/{verify-bl__PkXk.mjs.map → verify-BT9tgCOH.mjs.map} +1 -1
  92. package/package.json +22 -16
  93. package/src/cli.ts +32 -6
  94. package/src/commands/contract-emit.ts +67 -163
  95. package/src/commands/contract-infer.ts +7 -20
  96. package/src/commands/db-init.ts +1 -0
  97. package/src/commands/db-update.ts +1 -1
  98. package/src/commands/init/detect-pnpm-catalog.ts +141 -0
  99. package/src/commands/init/errors.ts +254 -0
  100. package/src/commands/init/exit-codes.ts +62 -0
  101. package/src/commands/init/hygiene-gitattributes.ts +97 -0
  102. package/src/commands/init/hygiene-gitignore.ts +48 -0
  103. package/src/commands/init/hygiene-package-scripts.ts +91 -0
  104. package/src/commands/init/index.ts +112 -7
  105. package/src/commands/init/init.ts +766 -144
  106. package/src/commands/init/inputs.ts +421 -0
  107. package/src/commands/init/output.ts +147 -0
  108. package/src/commands/init/probe-db.ts +308 -0
  109. package/src/commands/init/reinit-cleanup.ts +83 -0
  110. package/src/commands/init/templates/agent-skill-mongo.md +63 -31
  111. package/src/commands/init/templates/agent-skill-postgres.md +1 -1
  112. package/src/commands/init/templates/agent-skill.ts +25 -3
  113. package/src/commands/init/templates/code-templates.ts +125 -32
  114. package/src/commands/init/templates/env.ts +80 -0
  115. package/src/commands/init/templates/quick-reference-mongo.md +34 -13
  116. package/src/commands/init/templates/quick-reference-postgres.md +11 -9
  117. package/src/commands/init/templates/quick-reference.ts +42 -3
  118. package/src/commands/init/templates/tsconfig.ts +167 -5
  119. package/src/commands/inspect-live-schema.ts +10 -5
  120. package/src/commands/migration-apply.ts +84 -63
  121. package/src/commands/migration-new.ts +28 -34
  122. package/src/commands/migration-plan.ts +80 -56
  123. package/src/commands/migration-ref.ts +8 -7
  124. package/src/commands/migration-show.ts +53 -36
  125. package/src/commands/migration-status.ts +194 -58
  126. package/src/config-path-validation.ts +0 -1
  127. package/src/control-api/client.ts +21 -0
  128. package/src/control-api/operations/contract-emit.ts +198 -115
  129. package/src/control-api/operations/db-init.ts +10 -6
  130. package/src/control-api/operations/db-update.ts +10 -6
  131. package/src/control-api/operations/migration-apply.ts +30 -9
  132. package/src/control-api/types.ts +69 -7
  133. package/src/exports/control-api.ts +2 -1
  134. package/src/exports/init-output.ts +10 -0
  135. package/src/migration-cli.ts +445 -122
  136. package/src/utils/cli-errors.ts +49 -2
  137. package/src/utils/command-helpers.ts +45 -23
  138. package/src/utils/emit-queue.ts +26 -0
  139. package/src/utils/formatters/graph-migration-mapper.ts +7 -3
  140. package/src/utils/formatters/migrations.ts +62 -26
  141. package/src/utils/publish-contract-artifact-pair.ts +134 -0
  142. package/dist/cli-errors-DHq6GQGu.mjs +0 -5
  143. package/dist/client-TG7rbCWT.mjs.map +0 -1
  144. package/dist/config-loader-_W4T21X1.mjs.map +0 -1
  145. package/dist/contract-emit-CQfj7xJn.mjs +0 -122
  146. package/dist/contract-emit-CQfj7xJn.mjs.map +0 -1
  147. package/dist/contract-emit-DpPjuFy-.mjs +0 -195
  148. package/dist/contract-emit-DpPjuFy-.mjs.map +0 -1
  149. package/dist/contract-emit-fhNwwhkQ.mjs +0 -4
  150. package/dist/contract-infer-BS4kIX9c.mjs.map +0 -1
  151. package/dist/extract-operation-statements-DZUJNmL3.mjs +0 -13
  152. package/dist/extract-operation-statements-DZUJNmL3.mjs.map +0 -1
  153. package/dist/extract-sql-ddl-DDMX-9mz.mjs +0 -26
  154. package/dist/extract-sql-ddl-DDMX-9mz.mjs.map +0 -1
  155. package/dist/init-CQfo_4Ro.mjs +0 -430
  156. package/dist/init-CQfo_4Ro.mjs.map +0 -1
  157. package/dist/inspect-live-schema-BsoFVoS1.mjs.map +0 -1
  158. package/dist/migration-status-Ry3TnEya.mjs.map +0 -1
  159. package/dist/migrations-fU0xoKjS.mjs.map +0 -1
  160. package/dist/result-handler-BJwA7ufw.mjs.map +0 -1
  161. package/dist/terminal-ui-C5k88MmW.mjs.map +0 -1
  162. package/dist/validate-contract-deps-esa-VQ0h.mjs +0 -37
  163. package/dist/validate-contract-deps-esa-VQ0h.mjs.map +0 -1
  164. package/src/control-api/operations/extract-operation-statements.ts +0 -14
  165. package/src/control-api/operations/extract-sql-ddl.ts +0 -47
@@ -1,18 +1,19 @@
1
1
  import type { MigrationPlanOperation } from '@prisma-next/framework-components/control';
2
2
  import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
3
+ import {
4
+ errorNoInvariantPath,
5
+ errorUnknownInvariant,
6
+ MigrationToolsError,
7
+ } from '@prisma-next/migration-tools/errors';
8
+ import type { MigrationEdge, MigrationGraph } from '@prisma-next/migration-tools/graph';
3
9
  import {
4
10
  findPath,
5
11
  findPathWithDecision,
6
12
  findReachableLeaves,
7
- } from '@prisma-next/migration-tools/dag';
8
- import type { Refs } from '@prisma-next/migration-tools/refs';
13
+ } from '@prisma-next/migration-tools/migration-graph';
14
+ import type { MigrationPackage } from '@prisma-next/migration-tools/package';
15
+ import type { RefEntry, Refs } from '@prisma-next/migration-tools/refs';
9
16
  import { readRefs, resolveRef } from '@prisma-next/migration-tools/refs';
10
- import type {
11
- MigrationBundle,
12
- MigrationChainEntry,
13
- MigrationGraph,
14
- } from '@prisma-next/migration-tools/types';
15
- import { MigrationToolsError } from '@prisma-next/migration-tools/types';
16
17
  import { ifDefined } from '@prisma-next/utils/defined';
17
18
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
18
19
  import { cyan, dim, magenta, yellow } from 'colorette';
@@ -20,16 +21,23 @@ import { Command } from 'commander';
20
21
 
21
22
  import { loadConfig } from '../config-loader';
22
23
  import { createControlClient } from '../control-api/client';
23
- import { type CliStructuredError, errorRuntime, errorUnexpected } from '../utils/cli-errors';
24
+ import {
25
+ type CliStructuredError,
26
+ errorRuntime,
27
+ errorUnexpected,
28
+ mapMigrationToolsError,
29
+ } from '../utils/cli-errors';
24
30
  import {
25
31
  addGlobalOptions,
26
- loadAllBundles,
32
+ collectDeclaredInvariants,
33
+ loadMigrationPackages,
27
34
  maskConnectionUrl,
28
35
  readContractEnvelope,
29
36
  resolveMigrationPaths,
30
37
  setCommandDescriptions,
31
38
  setCommandExamples,
32
39
  toPathDecisionResult,
40
+ toStructuralEdge,
33
41
  } from '../utils/command-helpers';
34
42
  import {
35
43
  type EdgeStatus,
@@ -61,7 +69,7 @@ export interface MigrationStatusEntry {
61
69
  readonly dirName: string;
62
70
  readonly from: string;
63
71
  readonly to: string;
64
- readonly migrationId: string;
72
+ readonly migrationHash: string;
65
73
  readonly operationCount: number;
66
74
  readonly operationSummary: string;
67
75
  readonly hasDestructive: boolean;
@@ -78,23 +86,39 @@ export interface MigrationStatusResult {
78
86
  readonly targetHash: string;
79
87
  readonly contractHash: string;
80
88
  readonly refs?: readonly StatusRef[];
89
+ /** Required invariants from the active ref, sorted ascending. Always present (`[]` when no `--ref` or the ref declares none) — knowable offline. */
90
+ readonly requiredInvariants: readonly string[];
91
+ /**
92
+ * Invariants the marker has applied at least once, intersected with
93
+ * `requiredInvariants` for display relevance. JSON consumers see only the
94
+ * subset overlapping the active ref's required set — the full unfiltered
95
+ * marker invariant list lives on `marker.invariants` (control plane) and
96
+ * is not surfaced here. Present only in `mode === 'online'`; absent when
97
+ * offline (the marker is unknown, not empty).
98
+ */
99
+ readonly appliedInvariants?: readonly string[];
100
+ /** required − applied. Present only in `mode === 'online'`; absent when offline. */
101
+ readonly missingInvariants?: readonly string[];
81
102
  readonly pathDecision?: {
82
103
  readonly fromHash: string;
83
104
  readonly toHash: string;
84
105
  readonly alternativeCount: number;
85
106
  readonly tieBreakReasons: readonly string[];
86
107
  readonly refName?: string;
108
+ readonly requiredInvariants: readonly string[];
109
+ readonly satisfiedInvariants: readonly string[];
87
110
  readonly selectedPath: readonly {
88
111
  readonly dirName: string;
89
- readonly migrationId: string;
112
+ readonly migrationHash: string;
90
113
  readonly from: string;
91
114
  readonly to: string;
115
+ readonly invariants: readonly string[];
92
116
  }[];
93
117
  };
94
118
  readonly summary: string;
95
119
  readonly diagnostics: readonly StatusDiagnostic[];
96
120
  readonly graph?: MigrationGraph;
97
- readonly bundles?: readonly MigrationBundle[];
121
+ readonly bundles?: readonly MigrationPackage[];
98
122
  readonly edgeStatuses?: readonly EdgeStatus[];
99
123
  readonly activeRefHash?: string;
100
124
  readonly activeRefName?: string;
@@ -154,7 +178,7 @@ export function deriveEdgeStatuses(
154
178
  ): EdgeStatus[] {
155
179
  if (mode === 'offline') return [];
156
180
 
157
- const edgeKey = (e: MigrationChainEntry) => `${e.from}\0${e.to}`;
181
+ const edgeKey = (e: MigrationEdge) => `${e.from}\0${e.to}`;
158
182
 
159
183
  // No marker = empty DB — treat root as the marker (nothing applied, everything pending)
160
184
  const effectiveMarker = markerHash ?? EMPTY_CONTRACT_HASH;
@@ -224,8 +248,8 @@ export function deriveEdgeStatuses(
224
248
  * @param markerHash — the marker hash from the database, or undefined if no marker row / offline
225
249
  */
226
250
  function buildMigrationEntries(
227
- chain: readonly MigrationChainEntry[],
228
- packages: readonly MigrationBundle[],
251
+ chain: readonly MigrationEdge[],
252
+ packages: readonly MigrationPackage[],
229
253
  mode: 'online' | 'offline',
230
254
  markerHash: string | undefined,
231
255
  edgeStatuses?: readonly EdgeStatus[],
@@ -261,7 +285,7 @@ function buildMigrationEntries(
261
285
  dirName: migration.dirName,
262
286
  from: migration.from,
263
287
  to: migration.to,
264
- migrationId: migration.migrationId,
288
+ migrationHash: migration.migrationHash,
265
289
  operationCount: ops.length,
266
290
  operationSummary: summary,
267
291
  hasDestructive,
@@ -294,7 +318,7 @@ function resolveDisplayChain(
294
318
  graph: MigrationGraph,
295
319
  targetHash: string,
296
320
  markerHash: string | undefined,
297
- ): readonly MigrationChainEntry[] | null {
321
+ ): readonly MigrationEdge[] | null {
298
322
  if (markerHash === undefined) {
299
323
  return findPath(graph, EMPTY_CONTRACT_HASH, targetHash);
300
324
  }
@@ -355,18 +379,13 @@ async function executeMigrationStatusCommand(
355
379
 
356
380
  let activeRefName: string | undefined;
357
381
  let activeRefHash: string | undefined;
382
+ let activeRefEntry: RefEntry | undefined;
358
383
  let allRefs: Refs = {};
359
384
  try {
360
385
  allRefs = await readRefs(refsDir);
361
386
  } catch (error) {
362
387
  if (MigrationToolsError.is(error)) {
363
- return notOk(
364
- errorRuntime(error.message, {
365
- why: error.why,
366
- fix: error.fix,
367
- meta: { code: error.code },
368
- }),
369
- );
388
+ return notOk(mapMigrationToolsError(error));
370
389
  }
371
390
  throw error;
372
391
  }
@@ -374,21 +393,18 @@ async function executeMigrationStatusCommand(
374
393
  if (options.ref) {
375
394
  activeRefName = options.ref;
376
395
  try {
377
- activeRefHash = resolveRef(allRefs, activeRefName).hash;
396
+ activeRefEntry = resolveRef(allRefs, activeRefName);
397
+ activeRefHash = activeRefEntry.hash;
378
398
  } catch (error) {
379
399
  if (MigrationToolsError.is(error)) {
380
- return notOk(
381
- errorRuntime(error.message, {
382
- why: error.why,
383
- fix: error.fix,
384
- meta: { code: error.code },
385
- }),
386
- );
400
+ return notOk(mapMigrationToolsError(error));
387
401
  }
388
402
  throw error;
389
403
  }
390
404
  }
391
405
 
406
+ const requiredInvariants: readonly string[] = [...(activeRefEntry?.invariants ?? [])].sort();
407
+
392
408
  const statusRefs: StatusRef[] = Object.entries(allRefs).map(([name, entry]) => ({
393
409
  name,
394
410
  hash: entry.hash,
@@ -406,6 +422,12 @@ async function executeMigrationStatusCommand(
406
422
  if (activeRefName) {
407
423
  details.push({ label: 'ref', value: activeRefName });
408
424
  }
425
+ if (activeRefEntry && activeRefEntry.invariants.length > 0) {
426
+ details.push({
427
+ label: 'required',
428
+ value: formatInvariantList(activeRefEntry.invariants),
429
+ });
430
+ }
409
431
  const header = formatStyledHeader({
410
432
  command: 'migration status',
411
433
  description: 'Show migration history and applied status',
@@ -429,15 +451,13 @@ async function executeMigrationStatusCommand(
429
451
  });
430
452
  }
431
453
 
432
- let bundles: readonly MigrationBundle[];
454
+ let bundles: readonly MigrationPackage[];
433
455
  let graph: MigrationGraph;
434
456
  try {
435
- ({ bundles, graph } = await loadAllBundles(migrationsDir));
457
+ ({ bundles, graph } = await loadMigrationPackages(migrationsDir));
436
458
  } catch (error) {
437
459
  if (MigrationToolsError.is(error)) {
438
- return notOk(
439
- errorRuntime(error.message, { why: error.why, fix: error.fix, meta: { code: error.code } }),
440
- );
460
+ return notOk(mapMigrationToolsError(error));
441
461
  }
442
462
  return notOk(
443
463
  errorUnexpected(error instanceof Error ? error.message : String(error), {
@@ -465,6 +485,7 @@ async function executeMigrationStatusCommand(
465
485
  contractHash,
466
486
  summary: 'No migrations found',
467
487
  diagnostics,
488
+ requiredInvariants,
468
489
  });
469
490
  }
470
491
 
@@ -492,6 +513,7 @@ async function executeMigrationStatusCommand(
492
513
  }
493
514
 
494
515
  let markerHash: string | undefined;
516
+ let markerInvariants: readonly string[] = [];
495
517
  let mode: 'online' | 'offline' = 'offline';
496
518
 
497
519
  if (dbConnection && hasDriver) {
@@ -504,7 +526,9 @@ async function executeMigrationStatusCommand(
504
526
  });
505
527
  try {
506
528
  await client.connect(dbConnection);
507
- markerHash = (await client.readMarker())?.storageHash;
529
+ const marker = await client.readMarker();
530
+ markerHash = marker?.storageHash;
531
+ markerInvariants = marker?.invariants ?? [];
508
532
  mode = 'online';
509
533
  } catch {
510
534
  if (!flags.json && !flags.quiet) {
@@ -515,6 +539,32 @@ async function executeMigrationStatusCommand(
515
539
  }
516
540
  }
517
541
 
542
+ // Pre-check unknown invariants. Online: union the graph's declared
543
+ // invariants with the marker's recorded set so a retired-but-applied
544
+ // invariant doesn't surface as MIGRATION.UNKNOWN_INVARIANT — apply would
545
+ // route fine because marker subtraction empties `effectiveRequired`.
546
+ // Offline: keep the check graph-strict (the marker is unknown, and a
547
+ // missing declarer is the dominant signal we can offer).
548
+ if (activeRefEntry && activeRefEntry.invariants.length > 0) {
549
+ const declared = collectDeclaredInvariants(graph);
550
+ const known = new Set<string>(declared);
551
+ if (mode === 'online') {
552
+ for (const id of markerInvariants) known.add(id);
553
+ }
554
+ const unknown = activeRefEntry.invariants.filter((id) => !known.has(id));
555
+ if (unknown.length > 0) {
556
+ return notOk(
557
+ mapMigrationToolsError(
558
+ errorUnknownInvariant({
559
+ ...ifDefined('refName', activeRefName),
560
+ unknown,
561
+ declared: [...declared].sort(),
562
+ }),
563
+ ),
564
+ );
565
+ }
566
+ }
567
+
518
568
  // Marker exists but is not in the migration graph and doesn't match the
519
569
  // contract hash. The DB is at an unknown state relative to the graph.
520
570
  // Bail out early with a clear diagnostic instead of rendering a confusing
@@ -560,6 +610,7 @@ async function executeMigrationStatusCommand(
560
610
  summary: `${bundles.length} migration(s) on disk`,
561
611
  diagnostics,
562
612
  markerHash,
613
+ requiredInvariants,
563
614
  ...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
564
615
  });
565
616
  }
@@ -600,6 +651,7 @@ async function executeMigrationStatusCommand(
600
651
  summary: `${bundles.length} migration(s) on disk`,
601
652
  diagnostics,
602
653
  ...ifDefined('markerHash', markerHash),
654
+ requiredInvariants,
603
655
  ...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
604
656
  graph,
605
657
  bundles,
@@ -624,14 +676,39 @@ async function executeMigrationStatusCommand(
624
676
  const pendingCount = edgeStatuses.filter((e) => e.status === 'pending').length;
625
677
  const appliedCount = edgeStatuses.filter((e) => e.status === 'applied').length;
626
678
 
679
+ let appliedInvariants: readonly string[] | undefined;
680
+ let missingInvariants: readonly string[] | undefined;
681
+ let effectiveRequired = new Set<string>();
682
+ if (mode === 'online') {
683
+ // Mirrors `migration-apply.ts`: compute `effectiveRequired = required −
684
+ // marker.invariants` directly, then derive the display fields from it.
685
+ // `appliedInvariants` is the intersection (`required ∩ marker`), which
686
+ // is what JSON consumers see for the active ref; the unfiltered set
687
+ // lives on `marker.invariants`.
688
+ const markerSet = new Set(markerInvariants);
689
+ effectiveRequired = new Set(requiredInvariants.filter((id) => !markerSet.has(id)));
690
+ appliedInvariants = requiredInvariants.filter((id) => markerSet.has(id));
691
+ missingInvariants = [...effectiveRequired].sort();
692
+ }
693
+
694
+ // The marker can match the structural target while still missing required
695
+ // invariants — for example, a self-edge that provides X, applied via a ref
696
+ // declaring X. `pendingCount` (structural) says zero in that case but
697
+ // `effectiveRequired` is non-empty, so up-to-date messaging would mislead.
698
+ const hasInvariantWork = effectiveRequired.size > 0;
699
+ const missingList = [...effectiveRequired].sort().join(', ');
700
+
627
701
  let summary: string;
628
702
  if (mode === 'online') {
629
703
  if (markerHash !== undefined && !graph.nodes.has(markerHash) && markerHash === contractHash) {
630
704
  summary = `${bundles.length} migration(s) on disk`;
631
705
  } else if (activeRefHash && markerHash !== undefined) {
632
- summary = summarizeRefDistance(graph, markerHash, activeRefHash, activeRefName!);
633
- } else if (pendingCount === 0) {
706
+ const distance = summarizeRefDistance(graph, markerHash, activeRefHash, activeRefName!);
707
+ summary = hasInvariantWork ? `${distance} missing invariant(s): ${missingList}` : distance;
708
+ } else if (pendingCount === 0 && !hasInvariantWork) {
634
709
  summary = `Database is up to date (${appliedCount} migration${appliedCount !== 1 ? 's' : ''} applied)`;
710
+ } else if (pendingCount === 0 && hasInvariantWork) {
711
+ summary = `Missing invariant(s): ${missingList} — run 'prisma-next migration apply --ref ${activeRefName ?? '<ref>'}' to apply`;
635
712
  } else if (markerHash === undefined) {
636
713
  summary = `${pendingCount} pending migration(s) — database has no marker`;
637
714
  } else {
@@ -641,6 +718,37 @@ async function executeMigrationStatusCommand(
641
718
  summary = `${entries.length} migration(s) on disk`;
642
719
  }
643
720
 
721
+ let pathDecision: MigrationStatusResult['pathDecision'];
722
+ let routingUnreachable = false;
723
+ if (mode === 'online') {
724
+ const originHash = markerHash ?? EMPTY_CONTRACT_HASH;
725
+ const outcome = findPathWithDecision(graph, originHash, targetHash, {
726
+ ...ifDefined('refName', activeRefName),
727
+ required: effectiveRequired,
728
+ });
729
+ if (outcome.kind === 'ok') {
730
+ pathDecision = toPathDecisionResult(outcome.decision);
731
+ } else if (outcome.kind === 'unsatisfiable') {
732
+ return notOk(
733
+ mapMigrationToolsError(
734
+ errorNoInvariantPath({
735
+ ...ifDefined('refName', activeRefName),
736
+ required: [...effectiveRequired].sort(),
737
+ missing: outcome.missing,
738
+ structuralPath: outcome.structuralPath.map(toStructuralEdge),
739
+ }),
740
+ ),
741
+ );
742
+ } else {
743
+ // outcome.kind === 'unreachable' — origin (marker) has no structural
744
+ // path to the active target. `pendingCount` and `hasInvariantWork`
745
+ // both report zero in this case, but emitting MIGRATION.UP_TO_DATE
746
+ // would be wrong: the database simply cannot reach the requested
747
+ // ref/contract from its current state. Suppress UP_TO_DATE below.
748
+ routingUnreachable = true;
749
+ }
750
+ }
751
+
644
752
  if (mode === 'online') {
645
753
  if (markerHash !== undefined && !graph.nodes.has(markerHash) && markerHash === contractHash) {
646
754
  diagnostics.push({
@@ -657,7 +765,16 @@ async function executeMigrationStatusCommand(
657
765
  message: `${pendingCount} migration(s) pending`,
658
766
  hints: ["Run 'prisma-next migration apply' to apply pending migrations"],
659
767
  });
660
- } else {
768
+ } else if (hasInvariantWork) {
769
+ diagnostics.push({
770
+ code: 'MIGRATION.INVARIANTS_PENDING',
771
+ severity: 'info',
772
+ message: `Missing required invariant(s): ${missingList}`,
773
+ hints: [
774
+ `Run 'prisma-next migration apply --ref ${activeRefName ?? '<ref>'}' to apply a path that covers the required invariants`,
775
+ ],
776
+ });
777
+ } else if (!routingUnreachable) {
661
778
  diagnostics.push({
662
779
  code: 'MIGRATION.UP_TO_DATE',
663
780
  severity: 'info',
@@ -667,14 +784,6 @@ async function executeMigrationStatusCommand(
667
784
  }
668
785
  }
669
786
 
670
- let pathDecision: MigrationStatusResult['pathDecision'];
671
- if (mode === 'online' && markerHash !== undefined) {
672
- const decision = findPathWithDecision(graph, markerHash, targetHash, activeRefName);
673
- if (decision) {
674
- pathDecision = toPathDecisionResult(decision);
675
- }
676
- }
677
-
678
787
  const result: MigrationStatusResult = {
679
788
  ok: true,
680
789
  mode,
@@ -684,6 +793,9 @@ async function executeMigrationStatusCommand(
684
793
  summary,
685
794
  diagnostics,
686
795
  ...ifDefined('markerHash', markerHash),
796
+ requiredInvariants,
797
+ ...ifDefined('appliedInvariants', appliedInvariants),
798
+ ...ifDefined('missingInvariants', missingInvariants),
687
799
  ...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
688
800
  ...ifDefined('pathDecision', pathDecision),
689
801
  graph,
@@ -724,13 +836,19 @@ export function createMigrationStatusCommand(): Command {
724
836
 
725
837
  const exitCode = handleResult(result, flags, ui, (statusResult) => {
726
838
  if (flags.json) {
839
+ // Strip non-JSON-shape fields before emitting. These belong to
840
+ // the in-memory result so the human renderer can avoid
841
+ // recomputing them, but they would either bloat the wire format
842
+ // (graph, bundles, edgeStatuses) or expose internals
843
+ // (activeRefHash, activeRefName, diverged) that consumers should
844
+ // read off `pathDecision` / `refs` instead.
727
845
  const {
728
- graph: _g,
729
- bundles: _b,
730
- edgeStatuses: _es,
731
- activeRefHash: _arh,
732
- activeRefName: _arn,
733
- diverged: _d,
846
+ graph: _graph,
847
+ bundles: _bundles,
848
+ edgeStatuses: _edgeStatuses,
849
+ activeRefHash: _activeRefHash,
850
+ activeRefName: _activeRefName,
851
+ diverged: _diverged,
734
852
  ...jsonResult
735
853
  } = statusResult;
736
854
  ui.output(JSON.stringify(jsonResult, null, 2));
@@ -789,7 +907,7 @@ function formatLegend(colorize: boolean): string {
789
907
  return c(dim, parts.join(' '));
790
908
  }
791
909
 
792
- function formatStatusSummary(result: MigrationStatusResult, colorize: boolean): string {
910
+ export function formatStatusSummary(result: MigrationStatusResult, colorize: boolean): string {
793
911
  const c = (fn: (s: string) => string, s: string) => (colorize ? fn(s) : s);
794
912
  const lines: string[] = [];
795
913
 
@@ -797,11 +915,16 @@ function formatStatusSummary(result: MigrationStatusResult, colorize: boolean):
797
915
  const pendingCount = result.migrations.filter((e) => e.status === 'pending').length;
798
916
 
799
917
  const hasWarnings = result.diagnostics?.some((d) => d.severity === 'warn') ?? false;
918
+ // INVARIANTS_PENDING is filed at severity 'info' (per ADR 208) so the
919
+ // warn-severity check above doesn't see it. It still represents pending
920
+ // work, so it must promote the summary off the success icon.
921
+ const hasInvariantPending =
922
+ result.diagnostics?.some((d) => d.code === 'MIGRATION.INVARIANTS_PENDING') ?? false;
800
923
 
801
924
  if (result.mode === 'online') {
802
925
  if (hasUnknown || hasWarnings) {
803
926
  lines.push(`${c(yellow, '⚠')} ${result.summary}`);
804
- } else if (pendingCount === 0) {
927
+ } else if (pendingCount === 0 && !hasInvariantPending) {
805
928
  lines.push(`${c(cyan, '✔')} ${result.summary}`);
806
929
  } else {
807
930
  lines.push(`${c(yellow, '⧗')} ${result.summary}`);
@@ -810,6 +933,15 @@ function formatStatusSummary(result: MigrationStatusResult, colorize: boolean):
810
933
  lines.push(result.summary);
811
934
  }
812
935
 
936
+ if (result.requiredInvariants.length > 0) {
937
+ if (result.appliedInvariants !== undefined && result.missingInvariants !== undefined) {
938
+ lines.push(`${c(dim, 'applied ')}${formatInvariantList(result.appliedInvariants)}`);
939
+ lines.push(`${c(dim, 'missing ')}${formatInvariantList(result.missingInvariants)}`);
940
+ } else {
941
+ lines.push(`${c(dim, 'applied ')}(unknown — connect a database to evaluate)`);
942
+ }
943
+ }
944
+
813
945
  const warnings = result.diagnostics?.filter((d) => d.severity === 'warn') ?? [];
814
946
  for (const diag of warnings) {
815
947
  lines.push(`${c(yellow, '⚠')} ${diag.message}`);
@@ -821,6 +953,10 @@ function formatStatusSummary(result: MigrationStatusResult, colorize: boolean):
821
953
  return lines.join('\n');
822
954
  }
823
955
 
956
+ function formatInvariantList(ids: readonly string[]): string {
957
+ return ids.length === 0 ? '(none)' : ids.join(', ');
958
+ }
959
+
824
960
  function summarizeRefDistance(
825
961
  graph: MigrationGraph,
826
962
  markerHash: string,
@@ -57,7 +57,6 @@ export function finalizeConfig(config: PrismaNextConfig, configDir: string): Pri
57
57
  if (!config.contract) {
58
58
  return config;
59
59
  }
60
-
61
60
  const contract = normalizeContractConfig(config.contract);
62
61
  const source = finalizeContractSource(contract.source, configDir);
63
62
  const output = resolve(configDir, contract.output);
@@ -6,6 +6,8 @@ import type {
6
6
  ControlFamilyInstance,
7
7
  ControlStack,
8
8
  CoreSchemaView,
9
+ MigrationPlanOperation,
10
+ OperationPreview,
9
11
  SignDatabaseResult,
10
12
  VerifyDatabaseResult,
11
13
  VerifyDatabaseSchemaResult,
@@ -13,8 +15,11 @@ import type {
13
15
  import {
14
16
  createControlStack,
15
17
  hasMigrations,
18
+ hasOperationPreview,
19
+ hasPslContractInfer,
16
20
  hasSchemaView,
17
21
  } from '@prisma-next/framework-components/control';
22
+ import type { PslDocumentAst } from '@prisma-next/framework-components/psl-ast';
18
23
  import { ifDefined } from '@prisma-next/utils/defined';
19
24
  import { notOk, ok } from '@prisma-next/utils/result';
20
25
  import { assertFrameworkComponentsCompatible } from '../utils/framework-components';
@@ -469,6 +474,22 @@ class ControlClientImpl implements ControlClient {
469
474
  return undefined;
470
475
  }
471
476
 
477
+ inferPslContract(schemaIR: unknown): PslDocumentAst | undefined {
478
+ this.init();
479
+ if (this.familyInstance && hasPslContractInfer(this.familyInstance)) {
480
+ return this.familyInstance.inferPslContract(schemaIR);
481
+ }
482
+ return undefined;
483
+ }
484
+
485
+ toOperationPreview(operations: readonly MigrationPlanOperation[]): OperationPreview | undefined {
486
+ this.init();
487
+ if (this.familyInstance && hasOperationPreview(this.familyInstance)) {
488
+ return this.familyInstance.toOperationPreview(operations);
489
+ }
490
+ return undefined;
491
+ }
492
+
472
493
  async emit(options: EmitOptions): Promise<EmitResult> {
473
494
  const { onProgress, contractConfig } = options;
474
495