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

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 +23 -17
  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,5 +1,5 @@
1
1
  import type { CoreSchemaView } from '@prisma-next/framework-components/control';
2
- import { validatePrintableSqlSchemaIR } from '@prisma-next/psl-printer';
2
+ import type { PslDocumentAst } from '@prisma-next/framework-components/psl-ast';
3
3
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
4
4
  import { relative, resolve } from 'pathe';
5
5
  import { loadConfig } from '../config-loader';
@@ -33,6 +33,12 @@ export interface InspectLiveSchemaResult {
33
33
  readonly config: LoadedCliConfig;
34
34
  readonly schema: unknown;
35
35
  readonly schemaView: CoreSchemaView | undefined;
36
+ /**
37
+ * PSL AST inferred from the introspected schema, when the configured family
38
+ * implements `PslContractInferCapable`. `undefined` for families that do not
39
+ * support inference (e.g. Mongo today).
40
+ */
41
+ readonly pslContractAst: PslDocumentAst | undefined;
36
42
  readonly target: {
37
43
  readonly familyId: string;
38
44
  readonly id: string;
@@ -122,14 +128,12 @@ export async function inspectLiveSchema(
122
128
  const onProgress = createProgressAdapter({ ui, flags });
123
129
 
124
130
  try {
125
- const schemaIR = await client.introspect({
131
+ const schema = await client.introspect({
126
132
  connection: dbConnection,
127
133
  onProgress,
128
134
  });
129
- // TODO(TML-2251): Remove SQL-specific branching — SQL should use the same family-agnostic path as Mongo.
130
- const schema =
131
- config.family.familyId === 'sql' ? validatePrintableSqlSchemaIR(schemaIR) : schemaIR;
132
135
  const schemaView = client.toSchemaView(schema);
136
+ const pslContractAst = client.inferPslContract(schema);
133
137
 
134
138
  const dbUrl = typeof dbConnection === 'string' ? maskConnectionUrl(dbConnection) : undefined;
135
139
 
@@ -137,6 +141,7 @@ export async function inspectLiveSchema(
137
141
  config,
138
142
  schema,
139
143
  schemaView,
144
+ pslContractAst,
140
145
  target: {
141
146
  familyId: config.family.familyId,
142
147
  id: config.target.targetId,
@@ -1,9 +1,14 @@
1
- import { verifyMigrationBundle } from '@prisma-next/migration-tools/attestation';
2
1
  import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
3
- import { findPathWithDecision } from '@prisma-next/migration-tools/dag';
2
+ import {
3
+ errorNoInvariantPath,
4
+ errorUnknownInvariant,
5
+ MigrationToolsError,
6
+ } from '@prisma-next/migration-tools/errors';
7
+ import { findPathWithDecision } from '@prisma-next/migration-tools/migration-graph';
8
+ import type { MigrationPackage } from '@prisma-next/migration-tools/package';
9
+ import type { RefEntry } from '@prisma-next/migration-tools/refs';
4
10
  import { readRefs, resolveRef } from '@prisma-next/migration-tools/refs';
5
- import type { MigrationBundle } from '@prisma-next/migration-tools/types';
6
- import { MigrationToolsError } from '@prisma-next/migration-tools/types';
11
+ import { ifDefined } from '@prisma-next/utils/defined';
7
12
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
8
13
  import { Command } from 'commander';
9
14
 
@@ -18,11 +23,12 @@ import {
18
23
  errorRuntime,
19
24
  errorTargetMigrationNotSupported,
20
25
  errorUnexpected,
26
+ mapMigrationToolsError,
21
27
  } from '../utils/cli-errors';
22
28
  import {
23
29
  addGlobalOptions,
24
- loadAllBundles,
25
- type MigrationBundleSet,
30
+ collectDeclaredInvariants,
31
+ loadMigrationPackages,
26
32
  maskConnectionUrl,
27
33
  readContractEnvelope,
28
34
  resolveMigrationPaths,
@@ -30,6 +36,7 @@ import {
30
36
  setCommandExamples,
31
37
  targetSupportsMigrations,
32
38
  toPathDecisionResult,
39
+ toStructuralEdge,
33
40
  } from '../utils/command-helpers';
34
41
  import { formatMigrationApplyCommandOutput } from '../utils/formatters/migrations';
35
42
  import { formatStyledHeader } from '../utils/formatters/styled';
@@ -51,7 +58,7 @@ export interface MigrationApplyResult {
51
58
  readonly markerHash: string;
52
59
  readonly applied: readonly {
53
60
  readonly dirName: string;
54
- readonly from: string;
61
+ readonly from: string | null;
55
62
  readonly to: string;
56
63
  readonly operationsExecuted: number;
57
64
  }[];
@@ -62,11 +69,14 @@ export interface MigrationApplyResult {
62
69
  readonly alternativeCount: number;
63
70
  readonly tieBreakReasons: readonly string[];
64
71
  readonly refName?: string;
72
+ readonly requiredInvariants: readonly string[];
73
+ readonly satisfiedInvariants: readonly string[];
65
74
  readonly selectedPath: readonly {
66
75
  readonly dirName: string;
67
- readonly migrationId: string;
76
+ readonly migrationHash: string;
68
77
  readonly from: string;
69
78
  readonly to: string;
79
+ readonly invariants: readonly string[];
70
80
  }[];
71
81
  };
72
82
  readonly timings: {
@@ -74,19 +84,6 @@ export interface MigrationApplyResult {
74
84
  };
75
85
  }
76
86
 
77
- function mapMigrationToolsError(error: unknown): CliStructuredErrorType {
78
- if (MigrationToolsError.is(error)) {
79
- return errorRuntime(error.message, {
80
- why: error.why,
81
- fix: error.fix,
82
- meta: { code: error.code, ...(error.details ?? {}) },
83
- });
84
- }
85
- return errorUnexpected(error instanceof Error ? error.message : String(error), {
86
- why: `Unexpected error during migration apply: ${error instanceof Error ? error.message : String(error)}`,
87
- });
88
- }
89
-
90
87
  function mapApplyFailure(failure: MigrationApplyFailure): CliStructuredErrorType {
91
88
  return errorRuntime(failure.summary, {
92
89
  why: failure.why ?? 'Migration runner failed',
@@ -95,13 +92,14 @@ function mapApplyFailure(failure: MigrationApplyFailure): CliStructuredErrorType
95
92
  });
96
93
  }
97
94
 
98
- function packageToStep(pkg: MigrationBundle): MigrationApplyStep {
95
+ function packageToStep(pkg: MigrationPackage): MigrationApplyStep {
99
96
  return {
100
97
  dirName: pkg.dirName,
101
- from: pkg.manifest.from,
102
- to: pkg.manifest.to,
103
- toContract: pkg.manifest.toContract,
98
+ from: pkg.metadata.from,
99
+ to: pkg.metadata.to,
100
+ toContract: pkg.metadata.toContract,
104
101
  operations: pkg.ops,
102
+ providedInvariants: pkg.metadata.providedInvariants,
105
103
  };
106
104
  }
107
105
 
@@ -143,14 +141,14 @@ async function executeMigrationApplyCommand(
143
141
  );
144
142
  }
145
143
 
146
- let destinationHash: string;
147
- let refName: string | undefined;
144
+ let refEntry: RefEntry | undefined;
145
+ let envelopeHash: string | undefined;
146
+ const refName = options.ref;
148
147
 
149
- if (options.ref) {
150
- refName = options.ref;
148
+ if (refName) {
151
149
  try {
152
150
  const refs = await readRefs(refsDir);
153
- destinationHash = resolveRef(refs, refName).hash;
151
+ refEntry = resolveRef(refs, refName);
154
152
  } catch (error) {
155
153
  if (MigrationToolsError.is(error)) {
156
154
  return notOk(mapMigrationToolsError(error));
@@ -160,7 +158,7 @@ async function executeMigrationApplyCommand(
160
158
  } else {
161
159
  try {
162
160
  const envelope = await readContractEnvelope(config);
163
- destinationHash = envelope.storageHash;
161
+ envelopeHash = envelope.storageHash;
164
162
  } catch (error) {
165
163
  return notOk(
166
164
  errorRuntime('Current contract is unavailable', {
@@ -170,6 +168,7 @@ async function executeMigrationApplyCommand(
170
168
  );
171
169
  }
172
170
  }
171
+ const destinationHash = refEntry?.hash ?? envelopeHash!;
173
172
 
174
173
  if (!flags.json && !flags.quiet) {
175
174
  const details: Array<{ label: string; value: string }> = [
@@ -196,9 +195,9 @@ async function executeMigrationApplyCommand(
196
195
  }
197
196
 
198
197
  // Read migrations and build migration chain model (offline — no DB needed)
199
- let migrations: MigrationBundleSet;
198
+ let migrations: Awaited<ReturnType<typeof loadMigrationPackages>>;
200
199
  try {
201
- migrations = await loadAllBundles(migrationsDir);
200
+ migrations = await loadMigrationPackages(migrationsDir);
202
201
  } catch (error) {
203
202
  if (MigrationToolsError.is(error)) {
204
203
  return notOk(mapMigrationToolsError(error));
@@ -206,27 +205,6 @@ async function executeMigrationApplyCommand(
206
205
  throw error;
207
206
  }
208
207
 
209
- // Defense in depth: re-hash every bundle and confirm the recorded
210
- // `migrationId` matches the on-disk `(manifest, ops)`. Catches FS
211
- // corruption, partial writes, and post-emit hand edits before we
212
- // start touching the database.
213
- for (const bundle of migrations.bundles) {
214
- const verified = verifyMigrationBundle(bundle);
215
- if (!verified.ok) {
216
- return notOk(
217
- errorRuntime(`Migration package is corrupt: ${bundle.dirName}`, {
218
- why: `Stored migrationId "${verified.storedMigrationId}" does not match the recomputed hash "${verified.computedMigrationId}" for ${migrationsRelative}/${bundle.dirName}. The migration.json or ops.json has been edited or partially written since emit.`,
219
- fix: `Re-emit the package by running \`node "${migrationsRelative}/${bundle.dirName}/migration.ts"\`, or restore the directory from version control.`,
220
- meta: {
221
- dirName: bundle.dirName,
222
- storedMigrationId: verified.storedMigrationId,
223
- computedMigrationId: verified.computedMigrationId,
224
- },
225
- }),
226
- );
227
- }
228
- }
229
-
230
208
  const client = createControlClient({
231
209
  family: config.family,
232
210
  target: config.target,
@@ -239,6 +217,32 @@ async function executeMigrationApplyCommand(
239
217
  await client.connect(dbConnection);
240
218
  const marker = await client.readMarker();
241
219
 
220
+ // Pre-check unknown invariants against `(declared by graph) ∪
221
+ // (already on the marker)`. The union catches the edge case where the
222
+ // ref carries an invariant whose declaring migration was retired (e.g.
223
+ // history rewritten) but whose id is recorded on the marker —
224
+ // surfacing that as MIGRATION.UNKNOWN_INVARIANT would be misleading
225
+ // because the database has already satisfied the requirement, so the
226
+ // marker-subtraction below empties `effectiveRequired` and apply
227
+ // short-circuits to "Already up to date".
228
+ if (refEntry && refEntry.invariants.length > 0) {
229
+ const declared = collectDeclaredInvariants(migrations.graph);
230
+ const known = new Set<string>(declared);
231
+ for (const id of marker?.invariants ?? []) known.add(id);
232
+ const unknown = refEntry.invariants.filter((id) => !known.has(id));
233
+ if (unknown.length > 0) {
234
+ return notOk(
235
+ mapMigrationToolsError(
236
+ errorUnknownInvariant({
237
+ ...ifDefined('refName', refName),
238
+ unknown,
239
+ declared: [...declared].sort(),
240
+ }),
241
+ ),
242
+ );
243
+ }
244
+ }
245
+
242
246
  // --- No migrations on disk ---
243
247
  if (migrations.bundles.length === 0) {
244
248
  if (marker?.storageHash) {
@@ -308,13 +312,31 @@ async function executeMigrationApplyCommand(
308
312
  );
309
313
  }
310
314
 
311
- // --- Resolve path and apply ---
312
-
313
315
  // "No marker" means the database is fresh — start from the empty contract hash.
314
316
  const originHash = markerHash ?? EMPTY_CONTRACT_HASH;
315
317
 
316
- const decision = findPathWithDecision(migrations.graph, originHash, destinationHash, refName);
317
- if (!decision) {
318
+ const appliedInvariants = new Set(marker?.invariants ?? []);
319
+ const effectiveRequired = new Set(
320
+ (refEntry?.invariants ?? []).filter((id) => !appliedInvariants.has(id)),
321
+ );
322
+
323
+ const outcome = findPathWithDecision(migrations.graph, originHash, destinationHash, {
324
+ ...ifDefined('refName', refName),
325
+ required: effectiveRequired,
326
+ });
327
+ if (outcome.kind === 'unsatisfiable') {
328
+ return notOk(
329
+ mapMigrationToolsError(
330
+ errorNoInvariantPath({
331
+ ...ifDefined('refName', refName),
332
+ required: [...effectiveRequired].sort(),
333
+ missing: outcome.missing,
334
+ structuralPath: outcome.structuralPath.map(toStructuralEdge),
335
+ }),
336
+ ),
337
+ );
338
+ }
339
+ if (outcome.kind === 'unreachable') {
318
340
  return notOk(
319
341
  errorRuntime('No migration path from current state to target', {
320
342
  why: `Cannot find a path from "${originHash}" to target "${destinationHash}"`,
@@ -324,10 +346,9 @@ async function executeMigrationApplyCommand(
324
346
  );
325
347
  }
326
348
 
327
- const pendingPath = decision.selectedPath;
328
- const pathDecision = toPathDecisionResult(decision);
349
+ const pathDecision = toPathDecisionResult(outcome.decision);
329
350
 
330
- if (pendingPath.length === 0) {
351
+ if (outcome.decision.selectedPath.length === 0) {
331
352
  return ok({
332
353
  ok: true,
333
354
  migrationsApplied: 0,
@@ -342,7 +363,7 @@ async function executeMigrationApplyCommand(
342
363
 
343
364
  const bundleByDir = new Map(migrations.bundles.map((b) => [b.dirName, b]));
344
365
  const pendingMigrations: MigrationApplyStep[] = [];
345
- for (const migration of pendingPath) {
366
+ for (const migration of outcome.decision.selectedPath) {
346
367
  const pkg = bundleByDir.get(migration.dirName);
347
368
  if (!pkg) {
348
369
  return notOk(
@@ -376,7 +397,7 @@ async function executeMigrationApplyCommand(
376
397
  return ok({
377
398
  ok: true,
378
399
  migrationsApplied: value.migrationsApplied,
379
- migrationsTotal: pendingPath.length,
400
+ migrationsTotal: outcome.decision.selectedPath.length,
380
401
  markerHash: value.markerHash,
381
402
  applied: value.applied,
382
403
  summary: value.summary,
@@ -12,31 +12,35 @@ import { readFileSync } from 'node:fs';
12
12
  import type { Contract } from '@prisma-next/contract/types';
13
13
  import { getEmittedArtifactPaths } from '@prisma-next/emitter';
14
14
  import { createControlStack } from '@prisma-next/framework-components/control';
15
- import { computeMigrationId } from '@prisma-next/migration-tools/attestation';
16
- import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
17
- import { findLatestMigration, reconstructGraph } from '@prisma-next/migration-tools/dag';
15
+ import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
16
+ import { computeMigrationHash } from '@prisma-next/migration-tools/hash';
18
17
  import {
19
18
  copyFilesWithRename,
20
19
  formatMigrationDirName,
21
20
  readMigrationsDir,
22
21
  writeMigrationPackage,
23
22
  } from '@prisma-next/migration-tools/io';
23
+ import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
24
+ import {
25
+ findLatestMigration,
26
+ reconstructGraph,
27
+ } from '@prisma-next/migration-tools/migration-graph';
24
28
  import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
25
- import type { MigrationManifest } from '@prisma-next/migration-tools/types';
26
- import { MigrationToolsError } from '@prisma-next/migration-tools/types';
27
29
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
28
30
  import { Command } from 'commander';
29
- import { join, relative, resolve } from 'pathe';
31
+ import { join, relative } from 'pathe';
30
32
  import { loadConfig } from '../config-loader';
31
33
  import {
32
34
  CliStructuredError,
33
35
  errorRuntime,
34
36
  errorTargetMigrationNotSupported,
35
37
  errorUnexpected,
38
+ mapMigrationToolsError,
36
39
  } from '../utils/cli-errors';
37
40
  import {
38
41
  addGlobalOptions,
39
42
  getTargetMigrations,
43
+ resolveContractPath,
40
44
  resolveMigrationPaths,
41
45
  setCommandDescriptions,
42
46
  setCommandExamples,
@@ -57,7 +61,7 @@ interface MigrationNewOptions extends CommonCommandOptions {
57
61
  interface MigrationNewResult {
58
62
  readonly ok: true;
59
63
  readonly dir: string;
60
- readonly from: string;
64
+ readonly from: string | null;
61
65
  readonly to: string;
62
66
  readonly summary: string;
63
67
  }
@@ -68,11 +72,7 @@ async function executeMigrationNewCommand(
68
72
  const config = await loadConfig(options.config);
69
73
  const { migrationsDir, migrationsRelative } = resolveMigrationPaths(options.config, config);
70
74
 
71
- const contractPath = config.contract?.output ?? 'contract.json';
72
- const contractPathAbsolute = resolve(
73
- options.config ? resolve(options.config, '..') : process.cwd(),
74
- contractPath,
75
- );
75
+ const contractPathAbsolute = resolveContractPath(config);
76
76
 
77
77
  let contractJsonContent: string;
78
78
  try {
@@ -116,7 +116,7 @@ async function executeMigrationNewCommand(
116
116
  }
117
117
 
118
118
  let fromContract: Contract | null = null;
119
- let fromHash: string = EMPTY_CONTRACT_HASH;
119
+ let fromHash: string | null = null;
120
120
  let fromContractSourceDir: string | null = null;
121
121
 
122
122
  try {
@@ -126,7 +126,7 @@ async function executeMigrationNewCommand(
126
126
  const graph = reconstructGraph(packages);
127
127
 
128
128
  if (options.from) {
129
- const match = packages.find((p) => p.manifest.to.startsWith(options.from!));
129
+ const match = packages.find((p) => p.metadata.to.startsWith(options.from!));
130
130
  if (!match) {
131
131
  return notOk(
132
132
  errorRuntime('Starting contract not found', {
@@ -135,18 +135,18 @@ async function executeMigrationNewCommand(
135
135
  }),
136
136
  );
137
137
  }
138
- fromHash = match.manifest.to;
139
- fromContract = match.manifest.toContract;
138
+ fromHash = match.metadata.to;
139
+ fromContract = match.metadata.toContract;
140
140
  fromContractSourceDir = match.dirPath;
141
141
  } else {
142
142
  const latestMigration = findLatestMigration(graph);
143
143
  if (latestMigration) {
144
144
  fromHash = latestMigration.to;
145
145
  const leafPkg = packages.find(
146
- (p) => p.manifest.migrationId === latestMigration.migrationId,
146
+ (p) => p.metadata.migrationHash === latestMigration.migrationHash,
147
147
  );
148
148
  if (leafPkg) {
149
- fromContract = leafPkg.manifest.toContract;
149
+ fromContract = leafPkg.metadata.toContract;
150
150
  fromContractSourceDir = leafPkg.dirPath;
151
151
  }
152
152
  }
@@ -154,22 +154,16 @@ async function executeMigrationNewCommand(
154
154
  }
155
155
  } catch (error) {
156
156
  if (MigrationToolsError.is(error)) {
157
- return notOk(
158
- errorRuntime(error.message, {
159
- why: error.why,
160
- fix: error.fix,
161
- meta: { code: error.code },
162
- }),
163
- );
157
+ return notOk(mapMigrationToolsError(error));
164
158
  }
165
159
  throw error;
166
160
  }
167
161
 
168
- if (fromHash === toStorageHash) {
162
+ if (fromHash === toStorageHash && !options.from) {
169
163
  return notOk(
170
164
  errorRuntime('No changes detected', {
171
165
  why: 'The from and to contract hashes are identical — there is nothing to migrate.',
172
- fix: 'Change the contract and run `prisma-next contract emit` before creating a new migration.',
166
+ fix: 'Change the contract and run `prisma-next contract emit` before creating a new migration. To author a data-only migration on the current contract hash, pass `--from <hash>` explicitly.',
173
167
  }),
174
168
  );
175
169
  }
@@ -181,12 +175,11 @@ async function executeMigrationNewCommand(
181
175
 
182
176
  // `migration new` scaffolds an empty `migration.ts` for the user to
183
177
  // fill, so we attest over `ops: []`. Re-running self-emit after the
184
- // user adds operations will produce a different `migrationId` (over
178
+ // user adds operations will produce a different `migrationHash` (over
185
179
  // the real ops). This is intentional — there is no on-disk draft.
186
- const baseManifest: Omit<MigrationManifest, 'migrationId'> = {
180
+ const baseMetadata: Omit<MigrationMetadata, 'migrationHash'> = {
187
181
  from: fromHash,
188
182
  to: toStorageHash,
189
- kind: 'regular',
190
183
  fromContract,
191
184
  toContract: toContractJson,
192
185
  hints: {
@@ -195,11 +188,12 @@ async function executeMigrationNewCommand(
195
188
  plannerVersion: '1.0.0',
196
189
  },
197
190
  labels: [],
191
+ providedInvariants: [],
198
192
  createdAt: timestamp.toISOString(),
199
193
  };
200
- const manifest: MigrationManifest = {
201
- ...baseManifest,
202
- migrationId: computeMigrationId(baseManifest, []),
194
+ const metadata: MigrationMetadata = {
195
+ ...baseMetadata,
196
+ migrationHash: computeMigrationHash(baseMetadata, []),
203
197
  };
204
198
 
205
199
  const migrations = getTargetMigrations(config.target);
@@ -218,7 +212,7 @@ async function executeMigrationNewCommand(
218
212
  ...(config.extensionPacks ?? []),
219
213
  ]);
220
214
 
221
- await writeMigrationPackage(packageDir, manifest, []);
215
+ await writeMigrationPackage(packageDir, metadata, []);
222
216
  const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
223
217
  await copyFilesWithRename(packageDir, [
224
218
  { sourcePath: destinationArtifacts.jsonPath, destName: 'end-contract.json' },