@prisma-next/cli 0.4.1 → 0.4.3

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 (170) hide show
  1. package/README.md +56 -26
  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-DDeVsP2Y.d.mts +5 -0
  7. package/dist/cli.mjs +131 -15
  8. package/dist/cli.mjs.map +1 -1
  9. package/dist/{client-DGKrciLM.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 +7 -2
  13. package/dist/commands/contract-infer.d.mts.map +1 -1
  14. package/dist/commands/contract-infer.mjs +8 -2
  15. package/dist/commands/db-init.d.mts.map +1 -1
  16. package/dist/commands/db-init.mjs +11 -9
  17. package/dist/commands/db-init.mjs.map +1 -1
  18. package/dist/commands/db-schema.mjs +8 -5
  19. package/dist/commands/db-schema.mjs.map +1 -1
  20. package/dist/commands/db-sign.mjs +8 -7
  21. package/dist/commands/db-sign.mjs.map +1 -1
  22. package/dist/commands/db-update.mjs +10 -9
  23. package/dist/commands/db-update.mjs.map +1 -1
  24. package/dist/commands/db-verify.mjs +10 -9
  25. package/dist/commands/db-verify.mjs.map +1 -1
  26. package/dist/commands/migration-apply.d.mts +5 -2
  27. package/dist/commands/migration-apply.d.mts.map +1 -1
  28. package/dist/commands/migration-apply.mjs +57 -57
  29. package/dist/commands/migration-apply.mjs.map +1 -1
  30. package/dist/commands/migration-new.d.mts.map +1 -1
  31. package/dist/commands/migration-new.mjs +26 -32
  32. package/dist/commands/migration-new.mjs.map +1 -1
  33. package/dist/commands/migration-plan.d.mts +14 -5
  34. package/dist/commands/migration-plan.d.mts.map +1 -1
  35. package/dist/commands/migration-plan.mjs +45 -48
  36. package/dist/commands/migration-plan.mjs.map +1 -1
  37. package/dist/commands/migration-ref.d.mts +6 -4
  38. package/dist/commands/migration-ref.d.mts.map +1 -1
  39. package/dist/commands/migration-ref.mjs +31 -40
  40. package/dist/commands/migration-ref.mjs.map +1 -1
  41. package/dist/commands/migration-show.d.mts +13 -7
  42. package/dist/commands/migration-show.d.mts.map +1 -1
  43. package/dist/commands/migration-show.mjs +28 -29
  44. package/dist/commands/migration-show.mjs.map +1 -1
  45. package/dist/commands/migration-status.d.mts +23 -5
  46. package/dist/commands/migration-status.d.mts.map +1 -1
  47. package/dist/commands/migration-status.mjs +8 -3
  48. package/dist/{config-loader-_xQZsw0i.mjs → config-loader-ih8ViDb_.mjs} +2 -2
  49. package/dist/config-loader-ih8ViDb_.mjs.map +1 -0
  50. package/dist/config-loader.mjs +1 -1
  51. package/dist/contract-emit-DS5NzZh2.mjs +6 -0
  52. package/dist/contract-emit-RZBWzkop.mjs +329 -0
  53. package/dist/contract-emit-RZBWzkop.mjs.map +1 -0
  54. package/dist/contract-emit-rt_Nmdwq.mjs +150 -0
  55. package/dist/contract-emit-rt_Nmdwq.mjs.map +1 -0
  56. package/dist/{contract-enrichment-BV4KpbNW.mjs → contract-enrichment-4Ptgw3Pe.mjs} +1 -1
  57. package/dist/{contract-enrichment-BV4KpbNW.mjs.map → contract-enrichment-4Ptgw3Pe.mjs.map} +1 -1
  58. package/dist/{contract-infer-CUbiWGX0.mjs → contract-infer-Cf5J2wVg.mjs} +11 -19
  59. package/dist/contract-infer-Cf5J2wVg.mjs.map +1 -0
  60. package/dist/exports/control-api.d.mts +86 -21
  61. package/dist/exports/control-api.d.mts.map +1 -1
  62. package/dist/exports/control-api.mjs +7 -5
  63. package/dist/exports/index.mjs +8 -3
  64. package/dist/exports/index.mjs.map +1 -1
  65. package/dist/exports/init-output.d.mts +39 -0
  66. package/dist/exports/init-output.d.mts.map +1 -0
  67. package/dist/exports/init-output.mjs +3 -0
  68. package/dist/{framework-components-B__p--vT.mjs → framework-components-Bgcre3Z6.mjs} +2 -2
  69. package/dist/{framework-components-B__p--vT.mjs.map → framework-components-Bgcre3Z6.mjs.map} +1 -1
  70. package/dist/init-DAbQMxIR.mjs +2062 -0
  71. package/dist/init-DAbQMxIR.mjs.map +1 -0
  72. package/dist/{inspect-live-schema-wIYBTdL3.mjs → inspect-live-schema-LWtXfxm_.mjs} +9 -9
  73. package/dist/inspect-live-schema-LWtXfxm_.mjs.map +1 -0
  74. package/dist/migration-cli.d.mts +80 -0
  75. package/dist/migration-cli.d.mts.map +1 -0
  76. package/dist/migration-cli.mjs +408 -0
  77. package/dist/migration-cli.mjs.map +1 -0
  78. package/dist/{migration-command-scaffold-BC73xQSo.mjs → migration-command-scaffold-CU452v9h.mjs} +7 -7
  79. package/dist/{migration-command-scaffold-BC73xQSo.mjs.map → migration-command-scaffold-CU452v9h.mjs.map} +1 -1
  80. package/dist/{migration-status-CXBbScH5.mjs → migration-status-DoPrFIOQ.mjs} +119 -64
  81. package/dist/migration-status-DoPrFIOQ.mjs.map +1 -0
  82. package/dist/{migrations-DYRAjiVh.mjs → migrations-MEoKMiV5.mjs} +42 -21
  83. package/dist/migrations-MEoKMiV5.mjs.map +1 -0
  84. package/dist/output-BpcQrnnq.mjs +103 -0
  85. package/dist/output-BpcQrnnq.mjs.map +1 -0
  86. package/dist/{progress-adapter-Bwouy73-.mjs → progress-adapter-DgRGldpT.mjs} +1 -1
  87. package/dist/{progress-adapter-Bwouy73-.mjs.map → progress-adapter-DgRGldpT.mjs.map} +1 -1
  88. package/dist/quick-reference-mongo.md +34 -13
  89. package/dist/quick-reference-postgres.md +11 -9
  90. package/dist/{result-handler-CGohaH1o.mjs → result-handler-Ch6hVnOo.mjs} +36 -94
  91. package/dist/result-handler-Ch6hVnOo.mjs.map +1 -0
  92. package/dist/{terminal-ui-BuPXVRFY.mjs → terminal-ui-u2YgKghu.mjs} +76 -2
  93. package/dist/terminal-ui-u2YgKghu.mjs.map +1 -0
  94. package/dist/{verify-Cm2UFuZA.mjs → verify-BT9tgCOH.mjs} +2 -2
  95. package/dist/{verify-Cm2UFuZA.mjs.map → verify-BT9tgCOH.mjs.map} +1 -1
  96. package/package.json +27 -17
  97. package/src/cli.ts +32 -6
  98. package/src/commands/contract-emit.ts +67 -163
  99. package/src/commands/contract-infer.ts +7 -20
  100. package/src/commands/db-init.ts +1 -0
  101. package/src/commands/db-update.ts +1 -1
  102. package/src/commands/init/detect-pnpm-catalog.ts +141 -0
  103. package/src/commands/init/errors.ts +254 -0
  104. package/src/commands/init/exit-codes.ts +62 -0
  105. package/src/commands/init/hygiene-gitattributes.ts +97 -0
  106. package/src/commands/init/hygiene-gitignore.ts +48 -0
  107. package/src/commands/init/hygiene-package-scripts.ts +91 -0
  108. package/src/commands/init/index.ts +112 -7
  109. package/src/commands/init/init.ts +766 -144
  110. package/src/commands/init/inputs.ts +421 -0
  111. package/src/commands/init/output.ts +147 -0
  112. package/src/commands/init/probe-db.ts +308 -0
  113. package/src/commands/init/reinit-cleanup.ts +83 -0
  114. package/src/commands/init/templates/agent-skill-mongo.md +63 -31
  115. package/src/commands/init/templates/agent-skill-postgres.md +1 -1
  116. package/src/commands/init/templates/agent-skill.ts +25 -3
  117. package/src/commands/init/templates/code-templates.ts +125 -32
  118. package/src/commands/init/templates/env.ts +80 -0
  119. package/src/commands/init/templates/quick-reference-mongo.md +34 -13
  120. package/src/commands/init/templates/quick-reference-postgres.md +11 -9
  121. package/src/commands/init/templates/quick-reference.ts +42 -3
  122. package/src/commands/init/templates/tsconfig.ts +167 -5
  123. package/src/commands/inspect-live-schema.ts +10 -5
  124. package/src/commands/migration-apply.ts +86 -65
  125. package/src/commands/migration-new.ts +28 -34
  126. package/src/commands/migration-plan.ts +80 -56
  127. package/src/commands/migration-ref.ts +40 -54
  128. package/src/commands/migration-show.ts +53 -36
  129. package/src/commands/migration-status.ts +202 -71
  130. package/src/config-path-validation.ts +0 -1
  131. package/src/control-api/client.ts +21 -0
  132. package/src/control-api/operations/contract-emit.ts +198 -115
  133. package/src/control-api/operations/db-init.ts +10 -6
  134. package/src/control-api/operations/db-update.ts +10 -6
  135. package/src/control-api/operations/migration-apply.ts +30 -9
  136. package/src/control-api/types.ts +69 -7
  137. package/src/exports/control-api.ts +2 -1
  138. package/src/exports/init-output.ts +10 -0
  139. package/src/migration-cli.ts +577 -0
  140. package/src/utils/cli-errors.ts +50 -2
  141. package/src/utils/command-helpers.ts +48 -26
  142. package/src/utils/emit-queue.ts +26 -0
  143. package/src/utils/formatters/graph-migration-mapper.ts +7 -3
  144. package/src/utils/formatters/migrations.ts +62 -26
  145. package/src/utils/publish-contract-artifact-pair.ts +134 -0
  146. package/dist/cli-errors-CznZA5-d.mjs +0 -5
  147. package/dist/cli-errors-z37sV3eR.d.mts +0 -4
  148. package/dist/client-DGKrciLM.mjs.map +0 -1
  149. package/dist/config-loader-_xQZsw0i.mjs.map +0 -1
  150. package/dist/contract-emit-304WZtZJ.mjs +0 -4
  151. package/dist/contract-emit-DgeWdonT.mjs +0 -122
  152. package/dist/contract-emit-DgeWdonT.mjs.map +0 -1
  153. package/dist/contract-emit-mU1_B_m9.mjs +0 -195
  154. package/dist/contract-emit-mU1_B_m9.mjs.map +0 -1
  155. package/dist/contract-infer-CUbiWGX0.mjs.map +0 -1
  156. package/dist/extract-operation-statements-DWWFz1PK.mjs +0 -13
  157. package/dist/extract-operation-statements-DWWFz1PK.mjs.map +0 -1
  158. package/dist/extract-sql-ddl-7zn_AFS8.mjs +0 -26
  159. package/dist/extract-sql-ddl-7zn_AFS8.mjs.map +0 -1
  160. package/dist/init-DRquYpPa.mjs +0 -430
  161. package/dist/init-DRquYpPa.mjs.map +0 -1
  162. package/dist/inspect-live-schema-wIYBTdL3.mjs.map +0 -1
  163. package/dist/migration-status-CXBbScH5.mjs.map +0 -1
  164. package/dist/migrations-DYRAjiVh.mjs.map +0 -1
  165. package/dist/result-handler-CGohaH1o.mjs.map +0 -1
  166. package/dist/terminal-ui-BuPXVRFY.mjs.map +0 -1
  167. package/dist/validate-contract-deps-DZqv9m7H.mjs +0 -37
  168. package/dist/validate-contract-deps-DZqv9m7H.mjs.map +0 -1
  169. package/src/control-api/operations/extract-operation-statements.ts +0 -14
  170. 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
  }
@@ -345,7 +369,7 @@ async function executeMigrationStatusCommand(
345
369
  ui: TerminalUI,
346
370
  ): Promise<Result<MigrationStatusResult, CliStructuredError>> {
347
371
  const config = await loadConfig(options.config);
348
- const { configPath, migrationsDir, migrationsRelative, refsPath } = resolveMigrationPaths(
372
+ const { configPath, migrationsDir, migrationsRelative, refsDir } = resolveMigrationPaths(
349
373
  options.config,
350
374
  config,
351
375
  );
@@ -355,48 +379,35 @@ 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
- allRefs = await readRefs(refsPath);
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
  }
373
392
 
374
393
  if (options.ref) {
375
394
  activeRefName = options.ref;
376
- const refHash = allRefs[activeRefName];
377
- if (refHash) {
378
- activeRefHash = refHash;
379
- } else {
380
- try {
381
- activeRefHash = resolveRef(allRefs, activeRefName);
382
- } catch (error) {
383
- if (MigrationToolsError.is(error)) {
384
- return notOk(
385
- errorRuntime(error.message, {
386
- why: error.why,
387
- fix: error.fix,
388
- meta: { code: error.code },
389
- }),
390
- );
391
- }
392
- throw error;
395
+ try {
396
+ activeRefEntry = resolveRef(allRefs, activeRefName);
397
+ activeRefHash = activeRefEntry.hash;
398
+ } catch (error) {
399
+ if (MigrationToolsError.is(error)) {
400
+ return notOk(mapMigrationToolsError(error));
393
401
  }
402
+ throw error;
394
403
  }
395
404
  }
396
405
 
397
- const statusRefs: StatusRef[] = Object.entries(allRefs).map(([name, hash]) => ({
406
+ const requiredInvariants: readonly string[] = [...(activeRefEntry?.invariants ?? [])].sort();
407
+
408
+ const statusRefs: StatusRef[] = Object.entries(allRefs).map(([name, entry]) => ({
398
409
  name,
399
- hash,
410
+ hash: entry.hash,
400
411
  active: name === activeRefName,
401
412
  }));
402
413
 
@@ -411,6 +422,12 @@ async function executeMigrationStatusCommand(
411
422
  if (activeRefName) {
412
423
  details.push({ label: 'ref', value: activeRefName });
413
424
  }
425
+ if (activeRefEntry && activeRefEntry.invariants.length > 0) {
426
+ details.push({
427
+ label: 'required',
428
+ value: formatInvariantList(activeRefEntry.invariants),
429
+ });
430
+ }
414
431
  const header = formatStyledHeader({
415
432
  command: 'migration status',
416
433
  description: 'Show migration history and applied status',
@@ -434,15 +451,13 @@ async function executeMigrationStatusCommand(
434
451
  });
435
452
  }
436
453
 
437
- let bundles: readonly MigrationBundle[];
454
+ let bundles: readonly MigrationPackage[];
438
455
  let graph: MigrationGraph;
439
456
  try {
440
- ({ bundles, graph } = await loadAllBundles(migrationsDir));
457
+ ({ bundles, graph } = await loadMigrationPackages(migrationsDir));
441
458
  } catch (error) {
442
459
  if (MigrationToolsError.is(error)) {
443
- return notOk(
444
- errorRuntime(error.message, { why: error.why, fix: error.fix, meta: { code: error.code } }),
445
- );
460
+ return notOk(mapMigrationToolsError(error));
446
461
  }
447
462
  return notOk(
448
463
  errorUnexpected(error instanceof Error ? error.message : String(error), {
@@ -470,6 +485,7 @@ async function executeMigrationStatusCommand(
470
485
  contractHash,
471
486
  summary: 'No migrations found',
472
487
  diagnostics,
488
+ requiredInvariants,
473
489
  });
474
490
  }
475
491
 
@@ -497,6 +513,7 @@ async function executeMigrationStatusCommand(
497
513
  }
498
514
 
499
515
  let markerHash: string | undefined;
516
+ let markerInvariants: readonly string[] = [];
500
517
  let mode: 'online' | 'offline' = 'offline';
501
518
 
502
519
  if (dbConnection && hasDriver) {
@@ -509,7 +526,9 @@ async function executeMigrationStatusCommand(
509
526
  });
510
527
  try {
511
528
  await client.connect(dbConnection);
512
- markerHash = (await client.readMarker())?.storageHash;
529
+ const marker = await client.readMarker();
530
+ markerHash = marker?.storageHash;
531
+ markerInvariants = marker?.invariants ?? [];
513
532
  mode = 'online';
514
533
  } catch {
515
534
  if (!flags.json && !flags.quiet) {
@@ -520,6 +539,32 @@ async function executeMigrationStatusCommand(
520
539
  }
521
540
  }
522
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
+
523
568
  // Marker exists but is not in the migration graph and doesn't match the
524
569
  // contract hash. The DB is at an unknown state relative to the graph.
525
570
  // Bail out early with a clear diagnostic instead of rendering a confusing
@@ -565,6 +610,7 @@ async function executeMigrationStatusCommand(
565
610
  summary: `${bundles.length} migration(s) on disk`,
566
611
  diagnostics,
567
612
  markerHash,
613
+ requiredInvariants,
568
614
  ...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
569
615
  });
570
616
  }
@@ -605,6 +651,7 @@ async function executeMigrationStatusCommand(
605
651
  summary: `${bundles.length} migration(s) on disk`,
606
652
  diagnostics,
607
653
  ...ifDefined('markerHash', markerHash),
654
+ requiredInvariants,
608
655
  ...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
609
656
  graph,
610
657
  bundles,
@@ -629,14 +676,39 @@ async function executeMigrationStatusCommand(
629
676
  const pendingCount = edgeStatuses.filter((e) => e.status === 'pending').length;
630
677
  const appliedCount = edgeStatuses.filter((e) => e.status === 'applied').length;
631
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
+
632
701
  let summary: string;
633
702
  if (mode === 'online') {
634
703
  if (markerHash !== undefined && !graph.nodes.has(markerHash) && markerHash === contractHash) {
635
704
  summary = `${bundles.length} migration(s) on disk`;
636
705
  } else if (activeRefHash && markerHash !== undefined) {
637
- summary = summarizeRefDistance(graph, markerHash, activeRefHash, activeRefName!);
638
- } 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) {
639
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`;
640
712
  } else if (markerHash === undefined) {
641
713
  summary = `${pendingCount} pending migration(s) — database has no marker`;
642
714
  } else {
@@ -646,6 +718,37 @@ async function executeMigrationStatusCommand(
646
718
  summary = `${entries.length} migration(s) on disk`;
647
719
  }
648
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
+
649
752
  if (mode === 'online') {
650
753
  if (markerHash !== undefined && !graph.nodes.has(markerHash) && markerHash === contractHash) {
651
754
  diagnostics.push({
@@ -662,7 +765,16 @@ async function executeMigrationStatusCommand(
662
765
  message: `${pendingCount} migration(s) pending`,
663
766
  hints: ["Run 'prisma-next migration apply' to apply pending migrations"],
664
767
  });
665
- } 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) {
666
778
  diagnostics.push({
667
779
  code: 'MIGRATION.UP_TO_DATE',
668
780
  severity: 'info',
@@ -672,14 +784,6 @@ async function executeMigrationStatusCommand(
672
784
  }
673
785
  }
674
786
 
675
- let pathDecision: MigrationStatusResult['pathDecision'];
676
- if (mode === 'online' && markerHash !== undefined) {
677
- const decision = findPathWithDecision(graph, markerHash, targetHash, activeRefName);
678
- if (decision) {
679
- pathDecision = toPathDecisionResult(decision);
680
- }
681
- }
682
-
683
787
  const result: MigrationStatusResult = {
684
788
  ok: true,
685
789
  mode,
@@ -689,6 +793,9 @@ async function executeMigrationStatusCommand(
689
793
  summary,
690
794
  diagnostics,
691
795
  ...ifDefined('markerHash', markerHash),
796
+ requiredInvariants,
797
+ ...ifDefined('appliedInvariants', appliedInvariants),
798
+ ...ifDefined('missingInvariants', missingInvariants),
692
799
  ...(statusRefs.length > 0 ? { refs: statusRefs } : {}),
693
800
  ...ifDefined('pathDecision', pathDecision),
694
801
  graph,
@@ -729,13 +836,19 @@ export function createMigrationStatusCommand(): Command {
729
836
 
730
837
  const exitCode = handleResult(result, flags, ui, (statusResult) => {
731
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.
732
845
  const {
733
- graph: _g,
734
- bundles: _b,
735
- edgeStatuses: _es,
736
- activeRefHash: _arh,
737
- activeRefName: _arn,
738
- diverged: _d,
846
+ graph: _graph,
847
+ bundles: _bundles,
848
+ edgeStatuses: _edgeStatuses,
849
+ activeRefHash: _activeRefHash,
850
+ activeRefName: _activeRefName,
851
+ diverged: _diverged,
739
852
  ...jsonResult
740
853
  } = statusResult;
741
854
  ui.output(JSON.stringify(jsonResult, null, 2));
@@ -794,7 +907,7 @@ function formatLegend(colorize: boolean): string {
794
907
  return c(dim, parts.join(' '));
795
908
  }
796
909
 
797
- function formatStatusSummary(result: MigrationStatusResult, colorize: boolean): string {
910
+ export function formatStatusSummary(result: MigrationStatusResult, colorize: boolean): string {
798
911
  const c = (fn: (s: string) => string, s: string) => (colorize ? fn(s) : s);
799
912
  const lines: string[] = [];
800
913
 
@@ -802,11 +915,16 @@ function formatStatusSummary(result: MigrationStatusResult, colorize: boolean):
802
915
  const pendingCount = result.migrations.filter((e) => e.status === 'pending').length;
803
916
 
804
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;
805
923
 
806
924
  if (result.mode === 'online') {
807
925
  if (hasUnknown || hasWarnings) {
808
926
  lines.push(`${c(yellow, '⚠')} ${result.summary}`);
809
- } else if (pendingCount === 0) {
927
+ } else if (pendingCount === 0 && !hasInvariantPending) {
810
928
  lines.push(`${c(cyan, '✔')} ${result.summary}`);
811
929
  } else {
812
930
  lines.push(`${c(yellow, '⧗')} ${result.summary}`);
@@ -815,6 +933,15 @@ function formatStatusSummary(result: MigrationStatusResult, colorize: boolean):
815
933
  lines.push(result.summary);
816
934
  }
817
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
+
818
945
  const warnings = result.diagnostics?.filter((d) => d.severity === 'warn') ?? [];
819
946
  for (const diag of warnings) {
820
947
  lines.push(`${c(yellow, '⚠')} ${diag.message}`);
@@ -826,6 +953,10 @@ function formatStatusSummary(result: MigrationStatusResult, colorize: boolean):
826
953
  return lines.join('\n');
827
954
  }
828
955
 
956
+ function formatInvariantList(ids: readonly string[]): string {
957
+ return ids.length === 0 ? '(none)' : ids.join(', ');
958
+ }
959
+
829
960
  function summarizeRefDistance(
830
961
  graph: MigrationGraph,
831
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