@prisma-next/cli 0.15.0 → 0.16.0-dev.10

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 (177) hide show
  1. package/README.md +3 -4
  2. package/dist/cli.d.mts +1 -1
  3. package/dist/cli.mjs +13 -13
  4. package/dist/{client-DigVI8zX.mjs → client-2kwLMBSf.mjs} +67 -36
  5. package/dist/client-2kwLMBSf.mjs.map +1 -0
  6. package/dist/{command-helpers-CUoKYTlG.mjs → command-helpers-CVn3U9Uz.mjs} +32 -8
  7. package/dist/command-helpers-CVn3U9Uz.mjs.map +1 -0
  8. package/dist/commands/contract-emit.d.mts +0 -1
  9. package/dist/commands/contract-emit.d.mts.map +1 -1
  10. package/dist/commands/contract-emit.mjs +1 -1
  11. package/dist/commands/contract-infer.d.mts +0 -1
  12. package/dist/commands/contract-infer.d.mts.map +1 -1
  13. package/dist/commands/contract-infer.mjs +1 -1
  14. package/dist/commands/db-init.d.mts +0 -1
  15. package/dist/commands/db-init.d.mts.map +1 -1
  16. package/dist/commands/db-init.mjs +8 -7
  17. package/dist/commands/db-init.mjs.map +1 -1
  18. package/dist/commands/db-schema.d.mts +0 -1
  19. package/dist/commands/db-schema.d.mts.map +1 -1
  20. package/dist/commands/db-schema.mjs +3 -3
  21. package/dist/commands/db-sign.d.mts +0 -1
  22. package/dist/commands/db-sign.d.mts.map +1 -1
  23. package/dist/commands/db-sign.mjs +8 -10
  24. package/dist/commands/db-sign.mjs.map +1 -1
  25. package/dist/commands/db-update.d.mts +0 -1
  26. package/dist/commands/db-update.d.mts.map +1 -1
  27. package/dist/commands/db-update.mjs +12 -14
  28. package/dist/commands/db-update.mjs.map +1 -1
  29. package/dist/commands/db-verify.d.mts +0 -1
  30. package/dist/commands/db-verify.d.mts.map +1 -1
  31. package/dist/commands/db-verify.mjs +1 -1
  32. package/dist/commands/migrate.d.mts +1 -2
  33. package/dist/commands/migrate.d.mts.map +1 -1
  34. package/dist/commands/migrate.mjs +7 -7
  35. package/dist/commands/migrate.mjs.map +1 -1
  36. package/dist/commands/migration-check.d.mts +4 -3
  37. package/dist/commands/migration-check.d.mts.map +1 -1
  38. package/dist/commands/migration-check.mjs +1 -1
  39. package/dist/commands/migration-graph.d.mts +2 -3
  40. package/dist/commands/migration-graph.d.mts.map +1 -1
  41. package/dist/commands/migration-graph.mjs +3 -3
  42. package/dist/commands/migration-list.d.mts +2 -3
  43. package/dist/commands/migration-list.d.mts.map +1 -1
  44. package/dist/commands/migration-list.mjs +1 -1
  45. package/dist/commands/migration-log.d.mts +2 -3
  46. package/dist/commands/migration-log.d.mts.map +1 -1
  47. package/dist/commands/migration-log.mjs +1 -1
  48. package/dist/commands/migration-new.d.mts +0 -1
  49. package/dist/commands/migration-new.d.mts.map +1 -1
  50. package/dist/commands/migration-new.mjs +14 -38
  51. package/dist/commands/migration-new.mjs.map +1 -1
  52. package/dist/commands/migration-plan.d.mts +0 -1
  53. package/dist/commands/migration-plan.d.mts.map +1 -1
  54. package/dist/commands/migration-plan.mjs +1 -1
  55. package/dist/commands/migration-show.d.mts +0 -1
  56. package/dist/commands/migration-show.d.mts.map +1 -1
  57. package/dist/commands/migration-show.mjs +3 -3
  58. package/dist/commands/migration-status.d.mts +2 -3
  59. package/dist/commands/migration-status.d.mts.map +1 -1
  60. package/dist/commands/migration-status.mjs +1 -1
  61. package/dist/commands/ref.d.mts +1 -2
  62. package/dist/commands/ref.d.mts.map +1 -1
  63. package/dist/commands/ref.mjs +13 -18
  64. package/dist/commands/ref.mjs.map +1 -1
  65. package/dist/commands/telemetry/index.d.mts +0 -1
  66. package/dist/commands/telemetry/index.d.mts.map +1 -1
  67. package/dist/commands/telemetry/index.mjs +1 -1
  68. package/dist/contract-at-errors-C-l4-JEY.mjs +36 -0
  69. package/dist/contract-at-errors-C-l4-JEY.mjs.map +1 -0
  70. package/dist/{contract-emit-wgqrOXUI.mjs → contract-emit-DzOkTs11.mjs} +3 -3
  71. package/dist/{contract-emit-wgqrOXUI.mjs.map → contract-emit-DzOkTs11.mjs.map} +1 -1
  72. package/dist/{contract-emit-sYrqzsdN.mjs → contract-emit-iSMJVs26.mjs} +3 -3
  73. package/dist/{contract-emit-sYrqzsdN.mjs.map → contract-emit-iSMJVs26.mjs.map} +1 -1
  74. package/dist/{contract-infer-6SQnnVj7.mjs → contract-infer-BVqworBB.mjs} +3 -3
  75. package/dist/{contract-infer-6SQnnVj7.mjs.map → contract-infer-BVqworBB.mjs.map} +1 -1
  76. package/dist/{contract-space-aggregate-loader-DpB4x97a.mjs → contract-space-aggregate-loader-hPVymNLw.mjs} +14 -15
  77. package/dist/contract-space-aggregate-loader-hPVymNLw.mjs.map +1 -0
  78. package/dist/{db-verify-CEsG5Gek.mjs → db-verify-BpwCMyWJ.mjs} +6 -7
  79. package/dist/db-verify-BpwCMyWJ.mjs.map +1 -0
  80. package/dist/exports/control-api.d.mts +1 -2
  81. package/dist/exports/control-api.d.mts.map +1 -1
  82. package/dist/exports/control-api.mjs +2 -2
  83. package/dist/exports/index.d.mts +0 -1
  84. package/dist/exports/index.d.mts.map +1 -1
  85. package/dist/exports/index.mjs +2 -2
  86. package/dist/exports/init-output.d.mts +1 -0
  87. package/dist/exports/init-output.d.mts.map +1 -1
  88. package/dist/exports/init-output.mjs +1 -1
  89. package/dist/{format-CjongNb1.mjs → format-CFLZv_zU.mjs} +6 -5
  90. package/dist/format-CFLZv_zU.mjs.map +1 -0
  91. package/dist/{framework-components-BbnM1RYm.mjs → framework-components-VMQJbAwl.mjs} +2 -2
  92. package/dist/{framework-components-BbnM1RYm.mjs.map → framework-components-VMQJbAwl.mjs.map} +1 -1
  93. package/dist/global-flags-BpoOYtNZ.d.mts.map +1 -1
  94. package/dist/{init-D8YFfaD6.mjs → init-BSgPxVXA.mjs} +85 -90
  95. package/dist/init-BSgPxVXA.mjs.map +1 -0
  96. package/dist/{inspect-live-schema-CEt_ztBJ.mjs → inspect-live-schema-DxsD_a4d.mjs} +3 -3
  97. package/dist/{inspect-live-schema-CEt_ztBJ.mjs.map → inspect-live-schema-DxsD_a4d.mjs.map} +1 -1
  98. package/dist/{migration-check-ByeQHOhK.mjs → migration-check-DMxPzbHl.mjs} +70 -44
  99. package/dist/migration-check-DMxPzbHl.mjs.map +1 -0
  100. package/dist/migration-cli.d.mts +0 -1
  101. package/dist/migration-cli.d.mts.map +1 -1
  102. package/dist/migration-cli.mjs +3 -3
  103. package/dist/migration-cli.mjs.map +1 -1
  104. package/dist/{migration-command-scaffold-CJO3tjiR.mjs → migration-command-scaffold-DB_i9nBC.mjs} +3 -3
  105. package/dist/{migration-command-scaffold-CJO3tjiR.mjs.map → migration-command-scaffold-DB_i9nBC.mjs.map} +1 -1
  106. package/dist/{migration-list-DTYM8xvm.mjs → migration-list-cnSKilYH.mjs} +5 -5
  107. package/dist/migration-list-cnSKilYH.mjs.map +1 -0
  108. package/dist/{migration-log-qZ8LpQIl.mjs → migration-log-CEqFiez9.mjs} +3 -3
  109. package/dist/{migration-log-qZ8LpQIl.mjs.map → migration-log-CEqFiez9.mjs.map} +1 -1
  110. package/dist/{migration-path-target-DmNwsDgs.mjs → migration-path-target-C0TQuV5n.mjs} +2 -2
  111. package/dist/{migration-path-target-DmNwsDgs.mjs.map → migration-path-target-C0TQuV5n.mjs.map} +1 -1
  112. package/dist/{migration-plan-CfPrB80t.mjs → migration-plan-BJHulFZC.mjs} +54 -62
  113. package/dist/migration-plan-BJHulFZC.mjs.map +1 -0
  114. package/dist/{migration-status-DRuS-rP5.mjs → migration-status-ugWwGk1V.mjs} +5 -5
  115. package/dist/{migration-status-DRuS-rP5.mjs.map → migration-status-ugWwGk1V.mjs.map} +1 -1
  116. package/dist/{output-mEQ74_nd.mjs → output-DWNYqZGc.mjs} +5 -6
  117. package/dist/output-DWNYqZGc.mjs.map +1 -0
  118. package/dist/{ref-advancement-BkXlikCA.mjs → ref-advancement-BCz7X7qJ.mjs} +13 -6
  119. package/dist/ref-advancement-BCz7X7qJ.mjs.map +1 -0
  120. package/dist/schemas-CeGMYFYX.d.mts.map +1 -1
  121. package/dist/{telemetry-DHBBq5wT.mjs → telemetry-BEyNMfco.mjs} +2 -2
  122. package/dist/{telemetry-DHBBq5wT.mjs.map → telemetry-BEyNMfco.mjs.map} +1 -1
  123. package/dist/{terminal-ui-DGRNFWna.d.mts → terminal-ui-BN4QOsm5.d.mts} +1 -2
  124. package/dist/terminal-ui-BN4QOsm5.d.mts.map +1 -0
  125. package/dist/{types-DFoqNIcQ.d.mts → types-D4r_HI9-.d.mts} +8 -4
  126. package/dist/types-D4r_HI9-.d.mts.map +1 -0
  127. package/dist/{verify-DDCb0R2r.mjs → verify-C4RazDE1.mjs} +9 -8
  128. package/dist/verify-C4RazDE1.mjs.map +1 -0
  129. package/package.json +25 -25
  130. package/src/commands/db-init.ts +4 -3
  131. package/src/commands/db-sign.ts +6 -4
  132. package/src/commands/db-update.ts +15 -7
  133. package/src/commands/db-verify.ts +2 -3
  134. package/src/commands/init/errors.ts +62 -58
  135. package/src/commands/init/hygiene-gitattributes.ts +37 -21
  136. package/src/commands/init/init.ts +35 -32
  137. package/src/commands/init/output.ts +4 -5
  138. package/src/commands/init/reinit-cleanup.ts +16 -17
  139. package/src/commands/migrate.ts +1 -0
  140. package/src/commands/migration-check.ts +75 -38
  141. package/src/commands/migration-list.ts +1 -5
  142. package/src/commands/migration-new.ts +15 -39
  143. package/src/commands/migration-plan.ts +56 -78
  144. package/src/commands/ref.ts +19 -14
  145. package/src/control-api/operations/db-run.ts +4 -4
  146. package/src/control-api/operations/db-verify.ts +12 -9
  147. package/src/control-api/operations/format.ts +4 -3
  148. package/src/control-api/operations/migrate.ts +70 -26
  149. package/src/control-api/types.ts +4 -1
  150. package/src/migration-cli.ts +5 -5
  151. package/src/utils/cli-errors.ts +28 -7
  152. package/src/utils/combine-verify-results.ts +1 -1
  153. package/src/utils/contract-at-errors.ts +13 -22
  154. package/src/utils/contract-space-aggregate-loader.ts +12 -13
  155. package/src/utils/contract-space-seed-phase.ts +8 -6
  156. package/src/utils/formatters/errors.ts +24 -3
  157. package/src/utils/formatters/verify.ts +7 -6
  158. package/src/utils/integrity-violation-to-check-failure.ts +14 -14
  159. package/src/utils/plan-resolution.ts +6 -9
  160. package/src/utils/ref-advancement.ts +22 -3
  161. package/src/utils/result-handler.ts +4 -2
  162. package/dist/client-DigVI8zX.mjs.map +0 -1
  163. package/dist/command-helpers-CUoKYTlG.mjs.map +0 -1
  164. package/dist/contract-at-errors-D8CNDD7g.mjs +0 -42
  165. package/dist/contract-at-errors-D8CNDD7g.mjs.map +0 -1
  166. package/dist/contract-space-aggregate-loader-DpB4x97a.mjs.map +0 -1
  167. package/dist/db-verify-CEsG5Gek.mjs.map +0 -1
  168. package/dist/format-CjongNb1.mjs.map +0 -1
  169. package/dist/init-D8YFfaD6.mjs.map +0 -1
  170. package/dist/migration-check-ByeQHOhK.mjs.map +0 -1
  171. package/dist/migration-list-DTYM8xvm.mjs.map +0 -1
  172. package/dist/migration-plan-CfPrB80t.mjs.map +0 -1
  173. package/dist/output-mEQ74_nd.mjs.map +0 -1
  174. package/dist/ref-advancement-BkXlikCA.mjs.map +0 -1
  175. package/dist/terminal-ui-DGRNFWna.d.mts.map +0 -1
  176. package/dist/types-DFoqNIcQ.d.mts.map +0 -1
  177. package/dist/verify-DDCb0R2r.mjs.map +0 -1
@@ -12,6 +12,7 @@ import type {
12
12
  } from '@prisma-next/framework-components/control';
13
13
  import {
14
14
  type AggregateContractSpace,
15
+ allStorageElementsExternal,
15
16
  buildFabricatedMigrationEdge,
16
17
  type ContractMarkerRecordLike,
17
18
  type ContractSpaceAggregate,
@@ -98,13 +99,17 @@ export interface ExecuteMigrateOptions<TFamilyId extends string, TTargetId exten
98
99
  * 2. Read live marker rows per space (`familyInstance.readAllMarkers`).
99
100
  * 3. Per space: `resolveRecordedPath` plots the path from the live
100
101
  * marker to `space.headRef.hash` (or `refHash` for the app
101
- * space when provided). Empty-graph spaces fail loudly a
102
- * "never planned" space is a user-error condition for replay.
102
+ * space when provided). An empty-graph space whose elements are ALL
103
+ * externally managed resolves declaratively (marker to head, zero
104
+ * ops), mirroring the db-init aggregate planner: such a space ships
105
+ * no DDL and has nothing to author. Every other empty-graph space
106
+ * fails loudly — "never planned" is a user-error condition for
107
+ * replay.
103
108
  * 4. Hand off to {@link runMigration} (the runner-driving tail
104
109
  * shared with `db init` / `db update`). Marker advancement is
105
110
  * inside the per-space transaction.
106
111
  *
107
- * Encodes the replay-only contract: every contract space must have an
112
+ * Encodes the replay-only contract: the app contract space must have an
108
113
  * authored migration graph on disk before this operation can advance it.
109
114
  */
110
115
  export async function executeMigrate<TFamilyId extends string, TTargetId extends string>(
@@ -230,8 +235,13 @@ export async function executeMigrate<TFamilyId extends string, TTargetId extends
230
235
  // plans). Surfaces every loaded space — including at-head empty-
231
236
  // graph extensions — in `perSpace[]` so the result reflects the
232
237
  // full aggregate, not just the spaces the runner would have touched.
233
- const totalPlannedOps = sumPlannedOps(applyOrder, perSpacePlans);
234
- if (totalPlannedOps === 0) {
238
+ // A zero-op plan still counts as pending when it advances a marker
239
+ // (declared-state resolution for an all-external extension space).
240
+ const hasPendingWork = applyOrder.some((spaceId) => {
241
+ const entry = perSpacePlans.get(spaceId);
242
+ return entry !== undefined && planRequiresExecution(entry);
243
+ });
244
+ if (!hasPendingWork) {
235
245
  const ordered = canonicalOrder
236
246
  .filter((spaceId) => perSpacePlans.has(spaceId) || atHeadResolutions.has(spaceId))
237
247
  .map((spaceId) => {
@@ -397,6 +407,39 @@ export function planSpacePath({
397
407
  }),
398
408
  };
399
409
  }
410
+ // Empty-graph extension space not yet at head: the space ships no
411
+ // migration packages at all. Advancing the marker without migrations
412
+ // (the db-init aggregate planner's declared-state strategy, mirrored
413
+ // here) is valid exclusively when every element the space declares is
414
+ // externally managed — nothing Prisma Next owns exists in such a space
415
+ // (e.g. Supabase's auth/storage), so its declared state needs no
416
+ // migration to be true, and there is no command that could author an
417
+ // edge for it. A space that declares a managed element but ships no
418
+ // migration graph is an authoring bug: it falls through to the
419
+ // never-planned failure, like an app space with no authored graph.
420
+ if (!isAppSpace && allStorageElementsExternal(space.contract())) {
421
+ if (headRef.invariants.length > 0) {
422
+ return {
423
+ kind: 'unsatisfiable',
424
+ spaceId: space.spaceId,
425
+ isAppSpace,
426
+ missing: [...headRef.invariants].sort(),
427
+ targetInvariants: headRef.invariants,
428
+ targetSpace: space,
429
+ liveHash: liveHash ?? EMPTY_CONTRACT_HASH,
430
+ refName: undefined,
431
+ };
432
+ }
433
+ return {
434
+ kind: 'ok',
435
+ plan: buildAtHeadResolution({
436
+ aggregateTargetId: aggregate.targetId,
437
+ space,
438
+ targetHash,
439
+ liveMarker,
440
+ }),
441
+ };
442
+ }
400
443
  return { kind: 'never-planned', spaceId: space.spaceId, targetHash };
401
444
  }
402
445
 
@@ -434,11 +477,13 @@ export function planSpacePath({
434
477
  }
435
478
 
436
479
  /**
437
- * Build a zero-op {@link PerSpacePlan} for an empty-graph
438
- * space whose live marker already matches the target. Lets the apply
439
- * pipeline thread the space through `perSpacePlans` -> `applyOrder`
440
- * -> the success envelope's `perSpace[]` block so the result reflects
441
- * every loaded space, even when there is nothing to execute.
480
+ * Build a zero-op {@link PerSpacePlan} for an empty-graph space —
481
+ * either one whose live marker already matches the target (at-head), or
482
+ * an all-external extension space whose marker must advance to the head
483
+ * ref with no DDL (declared-state). Lets the apply pipeline thread the
484
+ * space through `perSpacePlans` -> `applyOrder` -> the success
485
+ * envelope's `perSpace[]` block so the result reflects every loaded
486
+ * space, even when there is nothing to execute.
442
487
  */
443
488
  function buildAtHeadResolution(args: {
444
489
  readonly aggregateTargetId: string;
@@ -469,17 +514,14 @@ function buildAtHeadResolution(args: {
469
514
  };
470
515
  }
471
516
 
472
- function sumPlannedOps(
473
- applyOrder: readonly string[],
474
- perSpacePlans: ReadonlyMap<string, PerSpacePlan>,
475
- ): number {
476
- let total = 0;
477
- for (const spaceId of applyOrder) {
478
- const entry = perSpacePlans.get(spaceId);
479
- if (!entry) continue;
480
- total += entry.plan.operations.length;
481
- }
482
- return total;
517
+ /**
518
+ * A plan needs the runner when it executes operations or advances the
519
+ * space's marker (a declared-state resolution has zero operations but a
520
+ * destination hash the live marker doesn't carry yet).
521
+ */
522
+ function planRequiresExecution(entry: PerSpacePlan): boolean {
523
+ if (entry.plan.operations.length > 0) return true;
524
+ return entry.plan.origin?.storageHash !== entry.plan.destination.storageHash;
483
525
  }
484
526
 
485
527
  interface BuildSuccessArgs {
@@ -552,10 +594,12 @@ function buildSuccess(args: BuildSuccessArgs): MigrateSuccess {
552
594
  }
553
595
 
554
596
  /**
555
- * Build the `neverPlanned` failure raised when a contract space has no on-disk
556
- * migration graph but migrate was asked to reach a target hash. The `why`
557
- * states only the condition; the recovery sequence is composed by
558
- * `errorPathUnreachable`'s `fix`.
597
+ * Build the `neverPlanned` failure raised when a contract space that
598
+ * declares managed storage elements has no on-disk migration graph but
599
+ * migrate was asked to reach a target hash. All-external spaces never reach
600
+ * this: they resolve declaratively (marker advances to the head ref with
601
+ * zero operations). The `why` states only the condition; the recovery
602
+ * sequence is composed by `errorPathUnreachable`'s `fix`.
559
603
  *
560
604
  * @internal Exported for testing only.
561
605
  */
@@ -563,7 +607,7 @@ export function buildNeverPlannedFailure(spaceId: string, targetHash: string): M
563
607
  return {
564
608
  code: 'MIGRATION_PATH_NOT_FOUND',
565
609
  summary: `No on-disk migrations for contract space "${spaceId}"`,
566
- why: `migrate is replay-only: every contract space must have an authored migration graph on disk. Space "${spaceId}" has no migrations under \`migrations/${spaceId}/\` but its head ref targets "${targetHash}".`,
610
+ why: `migrate is replay-only: a contract space that declares managed storage elements must have an authored migration graph on disk. Space "${spaceId}" has no migrations under \`migrations/${spaceId}/\` but its head ref targets "${targetHash}".`,
567
611
  meta: { spaceId, target: targetHash, kind: 'neverPlanned' },
568
612
  };
569
613
  }
@@ -402,7 +402,10 @@ export interface DbInitSuccess {
402
402
  /**
403
403
  * Failure codes for dbInit operation.
404
404
  */
405
- export type DbInitFailureCode = 'PLANNING_FAILED' | 'MARKER_ORIGIN_MISMATCH' | 'RUNNER_FAILED';
405
+ export type DbInitFailureCode =
406
+ | 'PLANNING_FAILED'
407
+ | 'MIGRATION.MARKER_ORIGIN_MISMATCH'
408
+ | 'RUNNER_FAILED';
406
409
 
407
410
  /**
408
411
  * Failure details for dbInit operation.
@@ -21,7 +21,7 @@
21
21
  * the CLI commands use, walking up from the migration file's directory.
22
22
  * 4. Probe-instantiates the migration class without a stack so it can read
23
23
  * `targetId` and verify it matches `config.target.targetId`
24
- * (`PN-MIG-2006` on mismatch) before any stack-driven adapter
24
+ * (`MIGRATION.TARGET_MISMATCH` on mismatch) before any stack-driven adapter
25
25
  * construction runs.
26
26
  * 5. Assembles a `ControlStack` from the loaded config descriptors and
27
27
  * constructs the migration with that stack.
@@ -269,7 +269,7 @@ async function orchestrate(
269
269
 
270
270
  // Pre-scan for malformed `--config` (no value, or value-shaped-as-flag)
271
271
  // before delegating to clipanion. The legacy parser surfaced both as
272
- // `errorMigrationCliInvalidConfigArg` (`PN-CLI-4012`); pre-scanning
272
+ // `errorMigrationCliInvalidConfigArg` (`CLI.CONFIG_ARG_MISSING_PATH`); pre-scanning
273
273
  // here keeps that contract independent of how clipanion classifies
274
274
  // the error internally (it variably throws `UnknownSyntaxError` or
275
275
  // accepts the flag-shaped token as the value depending on what other
@@ -337,7 +337,7 @@ async function orchestrate(
337
337
  * - `--config <empty>` where the value is the empty string. Shells
338
338
  * expand `--config ""` (or `--config "$UNSET_VAR"`) into a real
339
339
  * empty argv token; treating that as a usage error here surfaces
340
- * `PN-CLI-4012` instead of a less actionable loader error on an
340
+ * `CLI.CONFIG_ARG_MISSING_PATH` instead of a less actionable loader error on an
341
341
  * empty path.
342
342
  * - `--config=` (the equals form with an empty value). Same shape as
343
343
  * the empty-string case above; the user expressed intent to override
@@ -425,7 +425,7 @@ function isUnknownSyntaxError(err: unknown): err is Error {
425
425
  /**
426
426
  * Best-effort: pull the first input token that doesn't match a known
427
427
  * flag. Falls back to the first token when we can't pinpoint it. The
428
- * returned name is rendered into the user-visible PN-CLI-4013 envelope
428
+ * returned name is rendered into the user-visible CLI.UNKNOWN_FLAG envelope
429
429
  * (`Unknown flag \`<name>\``) and round-tripped via `meta.flag` so
430
430
  * agent consumers can render their own "did you mean" suggestions.
431
431
  */
@@ -556,7 +556,7 @@ async function runMigration(
556
556
  // subclasses are required to accept the no-arg form; the abstract
557
557
  // `Migration` constructor declares `stack?` and target subclasses
558
558
  // (Postgres, Mongo) propagate that optionality. This makes the
559
- // target-mismatch guard fail fast with `PN-MIG-2006` before any
559
+ // target-mismatch guard fail fast with `MIGRATION.TARGET_MISMATCH` before any
560
560
  // stack-driven adapter construction begins, even if the wrong-target
561
561
  // adapter's `create` would otherwise succeed and silently misshapen
562
562
  // the stored adapter cast.
@@ -215,22 +215,30 @@ export function errorPlanForgotTheFlag(
215
215
  });
216
216
  }
217
217
 
218
+ /**
219
+ * `viaRef: true` (the default) mirrors migration-tools' `errorRefNotResolvable`:
220
+ * a ref name with no pointer file, where the fallback hash isn't a graph
221
+ * node either — there's nothing to materialize a contract from.
222
+ * `viaRef: false` is a distinct, ref-independent case: an explicit `--from
223
+ * <hash>` that doesn't name a ref, on an empty migration graph, so there is
224
+ * no graph node and no ref to resolve a contract through.
225
+ */
218
226
  export function errorSnapshotMissing(
219
227
  identifier: string,
220
228
  options?: { readonly viaRef?: boolean },
221
229
  ): CliStructuredError {
222
230
  const viaRef = options?.viaRef !== false;
223
231
  const fix = viaRef
224
- ? `Run "prisma-next db update --advance-ref ${identifier}" to repopulate the snapshot, or "prisma-next ref delete ${identifier}" to clear the orphan pointer.`
225
- : `No contract source exists for hash "${identifier}" on an empty migration graph. Use --from with a ref name that has a paired snapshot, or run db update first.`;
232
+ ? `Create the ref with "prisma-next ref set ${identifier} <hash>" (or advance it via "prisma-next db update --advance-ref ${identifier}"), or pass a hash that is a node in the migration graph.`
233
+ : `No contract source exists for hash "${identifier}" on an empty migration graph. Use --from with a ref name (its contract resolves through the snapshot store), or run db update first.`;
226
234
  return errorRuntime(
227
235
  viaRef
228
- ? `Ref "${identifier}" has no paired contract snapshot`
236
+ ? `Ref "${identifier}" is not resolvable`
229
237
  : `No contract source for from-hash "${identifier}"`,
230
238
  {
231
239
  why: viaRef
232
- ? `Ref "${identifier}" exists but its paired snapshot files are missing.`
233
- : `Hash "${identifier}" is not a graph node and no paired ref snapshot supplies a contract.`,
240
+ ? `Ref "${identifier}" has no pointer file, and the hash being resolved is not a node in the migration graph either.`
241
+ : `Hash "${identifier}" is not a node in the migration graph (the graph is empty), and it does not name a ref either.`,
234
242
  fix,
235
243
  meta: {
236
244
  code: 'MIGRATION.SNAPSHOT_MISSING',
@@ -287,7 +295,18 @@ export function errorPathUnreachable(failure: MigrateFailure): CliStructuredErro
287
295
  // Plan-then-apply recovery. The planner destination is the missing edge's
288
296
  // target; `migration plan --to` (built for arbitrary targets) makes this a
289
297
  // real command, so the diagnostic that sends you here is now honest.
298
+ //
299
+ // Never-planned spaces have an EMPTY migration graph, and contract-ref
300
+ // resolution only resolves full hashes against graph nodes — a
301
+ // `--to <hash>` remediation would reject its own input. `migration plan`
302
+ // without `--to` targets the working contract (the same contract the app
303
+ // space's synthesized head ref carries), so the bare form is the one that
304
+ // runs verbatim.
305
+ const neverPlanned = meta['kind'] === 'neverPlanned';
290
306
  const planCommand = (() => {
307
+ if (neverPlanned) {
308
+ return 'prisma-next migration plan --name <slug>';
309
+ }
291
310
  if (planFromHash !== null && targetHash !== null) {
292
311
  return `prisma-next migration plan --from ${planFromHash} --to ${targetHash} --name <slug>`;
293
312
  }
@@ -300,7 +319,9 @@ export function errorPathUnreachable(failure: MigrateFailure): CliStructuredErro
300
319
  return 'prisma-next migration plan';
301
320
  })();
302
321
  const applyCommand =
303
- targetHash !== null ? `prisma-next migrate --to ${targetHash}` : 'prisma-next migrate';
322
+ targetHash !== null && !neverPlanned
323
+ ? `prisma-next migrate --to ${targetHash}`
324
+ : 'prisma-next migrate';
304
325
  return errorRuntime(failure.summary, {
305
326
  why:
306
327
  failure.why ??
@@ -347,7 +368,7 @@ export function mapMigrationToolsError(error: MigrationToolsError): CliStructure
347
368
  * Shared "needs a live database" precondition for read verbs that consult the
348
369
  * marker/ledger (`migration log`, `migration status`). A command needs both a
349
370
  * connection string and a control-plane driver; either missing yields the same
350
- * `PN-CLI-4005` envelope with `meta.missingFlags` (canonical long-form flags
371
+ * `CONFIG.DB_CONNECTION_REQUIRED` envelope with `meta.missingFlags` (canonical long-form flags
351
372
  * per CLI Style Guide §Errors) so callers can react programmatically. Returns
352
373
  * `null` when both are present.
353
374
  */
@@ -78,7 +78,7 @@ export function combineVerifyResults(
78
78
  return {
79
79
  result: {
80
80
  ok,
81
- ...(ok ? {} : { code: appResult.code ?? 'PN-RUN-3010' }),
81
+ ...(ok ? {} : { code: appResult.code ?? 'CONTRACT.MARKER_REQUIRED' }),
82
82
  summary,
83
83
  contract: appResult.contract,
84
84
  target: appResult.target,
@@ -1,6 +1,5 @@
1
1
  import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
2
2
  import { notOk, type Result } from '@prisma-next/utils/result';
3
- import { join } from 'pathe';
4
3
  import {
5
4
  CliStructuredError,
6
5
  errorContractValidationFailed,
@@ -16,7 +15,7 @@ export function mapContractAtError(
16
15
  ): Result<never, CliStructuredError> {
17
16
  if (MigrationToolsError.is(error)) {
18
17
  switch (error.code) {
19
- case 'MIGRATION.SNAPSHOT_MISSING': {
18
+ case 'MIGRATION.REF_NOT_RESOLVABLE': {
20
19
  const refName =
21
20
  typeof error.details?.['refName'] === 'string'
22
21
  ? error.details['refName']
@@ -27,18 +26,13 @@ export function mapContractAtError(
27
26
  }
28
27
  case 'MIGRATION.CONTRACT_DESERIALIZATION_FAILED': {
29
28
  const filePath =
30
- typeof error.details?.['filePath'] === 'string'
31
- ? error.details['filePath']
32
- : 'ref-snapshot';
29
+ typeof error.details?.['filePath'] === 'string' ? error.details['filePath'] : 'unknown';
33
30
  const message =
34
31
  typeof error.details?.['message'] === 'string' ? error.details['message'] : error.message;
35
- const isRefSnapshot = filePath.endsWith('.contract.json');
36
32
  return notOk(
37
33
  errorContractValidationFailed(
38
- isRefSnapshot
39
- ? `Ref snapshot contract failed to deserialize: ${message}`
40
- : `Predecessor contract at ${filePath} failed to deserialize: ${message}`,
41
- { where: { path: isRefSnapshot ? 'ref-snapshot' : filePath } },
34
+ `Predecessor contract at ${filePath} failed to deserialize: ${message}`,
35
+ { where: { path: filePath } },
42
36
  ),
43
37
  );
44
38
  }
@@ -66,22 +60,19 @@ export function mapContractAtError(
66
60
  fix: error.fix,
67
61
  }),
68
62
  );
69
- case 'MIGRATION.FILE_MISSING': {
70
- const file =
71
- typeof error.details?.['file'] === 'string' ? error.details['file'] : 'end-contract.json';
72
- const dir = typeof error.details?.['dir'] === 'string' ? error.details['dir'] : '';
73
- const jsonPath = dir ? join(dir, 'end-contract.json') : file;
63
+ case 'MIGRATION.CONTRACT_SNAPSHOT_MISSING': {
64
+ const expectedPath =
65
+ typeof error.details?.['expectedPath'] === 'string'
66
+ ? error.details['expectedPath']
67
+ : 'migrations/snapshots/';
74
68
  const role = options?.artifactRole ?? 'from';
75
69
  return notOk(
76
- errorFileNotFound(jsonPath, {
70
+ errorFileNotFound(expectedPath, {
77
71
  why:
78
72
  role === 'to'
79
- ? `Target migration is missing its destination contract snapshot at ${jsonPath}`
80
- : `Predecessor migration is missing its destination contract snapshot at ${jsonPath}`,
81
- fix:
82
- role === 'to'
83
- ? 'Re-emit the target migration so its sibling `end-contract.json` / `end-contract.d.ts` are restored, then re-run this command.'
84
- : 'Re-emit the predecessor migration (`prisma-next migration plan` from its source) so its sibling `end-contract.json` is restored, then re-run this command.',
73
+ ? `Target migration is missing its contract snapshot at ${expectedPath}`
74
+ : `Predecessor migration is missing its contract snapshot at ${expectedPath}`,
75
+ fix: 'Restore migrations/snapshots/ from version control, or re-run the command that produced this migration to regenerate its snapshot.',
85
76
  }),
86
77
  );
87
78
  }
@@ -20,7 +20,7 @@ import { toDeclaredExtensionsFromRaw } from './extension-pack-inputs';
20
20
 
21
21
  const CONTRACT_SPACES_DOCS_URL = 'https://pris.ly/contract-spaces';
22
22
 
23
- function contractSpaceError5002(
23
+ function contractSpaceViolationError(
24
24
  summary: string,
25
25
  options: {
26
26
  readonly why: string;
@@ -28,8 +28,7 @@ function contractSpaceError5002(
28
28
  readonly violations: readonly IntegrityViolation[];
29
29
  },
30
30
  ): CliStructuredError {
31
- return new CliStructuredError('5002', summary, {
32
- domain: 'MIG',
31
+ return new CliStructuredError('MIGRATION.CONTRACT_SPACE_VIOLATION', summary, {
33
32
  why: options.why,
34
33
  fix: options.fix,
35
34
  docsUrl: CONTRACT_SPACES_DOCS_URL,
@@ -38,7 +37,7 @@ function contractSpaceError5002(
38
37
  }
39
38
 
40
39
  /**
41
- * Build the `5002` structured-error envelope for a contract-space
40
+ * Build the `MIGRATION.CONTRACT_SPACE_VIOLATION` structured-error envelope for a contract-space
42
41
  * target mismatch. Shared between the declared-extension precheck (the
43
42
  * descriptor's configured target disagrees with the project target) and
44
43
  * the on-disk-contract check surfaced by `checkIntegrity`.
@@ -48,7 +47,7 @@ function targetMismatchError(
48
47
  expected: string,
49
48
  actual: string,
50
49
  ): CliStructuredError {
51
- return contractSpaceError5002(`Contract-space target mismatch for "${spaceId}"`, {
50
+ return contractSpaceViolationError(`Contract-space target mismatch for "${spaceId}"`, {
52
51
  why: `Space "${spaceId}" targets "${actual}" but the project's adapter targets "${expected}".`,
53
52
  fix: 'Update the extension descriptor to target the configured database, or change the project adapter.',
54
53
  violations: [{ kind: 'targetMismatch', spaceId, expected, actual }],
@@ -89,9 +88,10 @@ function describeIntegrityViolation(violation: IntegrityViolation): string {
89
88
  /**
90
89
  * Map the integrity violations `checkIntegrity` reports into a single
91
90
  * CLI structured-error envelope, preserving the error codes the prior
92
- * throw-on-load loader emitted: `5001` (layout drift, bundled) and
93
- * `5002` (target / disjointness / contract-validation / structural
94
- * integrity). Returns `null` when there is nothing to refuse on.
91
+ * throw-on-load loader emitted: `MIGRATION.CONTRACT_SPACE_LAYOUT_VIOLATION`
92
+ * (layout drift, bundled) and `MIGRATION.CONTRACT_SPACE_VIOLATION` (target /
93
+ * disjointness / contract-validation / structural integrity). Returns
94
+ * `null` when there is nothing to refuse on.
95
95
  *
96
96
  * Precedence reproduces the prior loader's first-failure ordering:
97
97
  * layout drift first (every offence bundled into one envelope), then
@@ -113,8 +113,7 @@ export function mapIntegrityViolations(
113
113
  layout.length === 1
114
114
  ? 'Contract-space layout violation detected'
115
115
  : `Contract-space layout violations detected (${layout.length})`;
116
- return new CliStructuredError('5001', summary, {
117
- domain: 'MIG',
116
+ return new CliStructuredError('MIGRATION.CONTRACT_SPACE_LAYOUT_VIOLATION', summary, {
118
117
  why: `The on-disk \`migrations/\` directory and your \`extensionPacks\` declaration are not in agreement.\n${lines.join('\n')}`,
119
118
  fix: 'Declare the extension in `extensionPacks` and re-emit its contract-space artefacts, or remove the orphan `migrations/<space>` directory.',
120
119
  docsUrl: CONTRACT_SPACES_DOCS_URL,
@@ -133,7 +132,7 @@ export function mapIntegrityViolations(
133
132
 
134
133
  const disjointness = violations.find((v) => v.kind === 'disjointness');
135
134
  if (disjointness && disjointness.kind === 'disjointness') {
136
- return contractSpaceError5002(
135
+ return contractSpaceViolationError(
137
136
  `Contract-space disjointness violation: storage element "${disjointness.element}" claimed by multiple spaces`,
138
137
  {
139
138
  why: `Spaces ${disjointness.claimedBy.map((s) => `"${s}"`).join(', ')} all claim the storage element "${disjointness.element}". Each storage element must be owned by exactly one contract space.`,
@@ -145,7 +144,7 @@ export function mapIntegrityViolations(
145
144
 
146
145
  const contractUnreadable = violations.find((v) => v.kind === 'contractUnreadable');
147
146
  if (contractUnreadable && contractUnreadable.kind === 'contractUnreadable') {
148
- return contractSpaceError5002(
147
+ return contractSpaceViolationError(
149
148
  `Contract-space contract validation failed for "${contractUnreadable.spaceId}"`,
150
149
  {
151
150
  why: contractUnreadable.detail,
@@ -160,7 +159,7 @@ export function mapIntegrityViolations(
160
159
  // is still computed; the gate just renders one envelope).
161
160
  const structural = violations[0]!;
162
161
  const spaceId = 'spaceId' in structural ? structural.spaceId : '*';
163
- return contractSpaceError5002(`Contract-space integrity failure for "${spaceId}"`, {
162
+ return contractSpaceViolationError(`Contract-space integrity failure for "${spaceId}"`, {
164
163
  why: describeIntegrityViolation(structural),
165
164
  fix: 'Re-emit the affected migration package(s) or restore the on-disk `migrations/` directory from version control.',
166
165
  violations: [structural],
@@ -2,7 +2,7 @@ import { materialiseExtensionMigrationPackageIfMissing } from '@prisma-next/migr
2
2
  import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
3
3
  import type { MigrationOps } from '@prisma-next/migration-tools/package';
4
4
  import {
5
- emitContractSpaceArtefacts,
5
+ emitContractSpaceArtifacts,
6
6
  planAllSpaces,
7
7
  readContractSpaceHeadRef,
8
8
  type SpacePlanOutput,
@@ -53,7 +53,7 @@ export interface ContractSpaceSeedPhaseInputs {
53
53
  * - `action: 'unchanged'` — the on-disk head already matched the
54
54
  * descriptor and no new migration packages needed to be written.
55
55
  *
56
- * Either way, the artefacts (`contract.json`, `contract.d.ts`,
56
+ * Either way, the artifacts (`contract.json`, `contract.d.ts`,
57
57
  * `refs/head.json`) are re-emitted: the framework owns those files and
58
58
  * makes the re-emit observably idempotent at the byte level.
59
59
  */
@@ -75,9 +75,11 @@ export interface ContractSpaceSeedPhaseResult {
75
75
  * For every extension that exposes a `contractSpace`:
76
76
  *
77
77
  * 1. Read the on-disk head ref (returns `null` on first emit).
78
- * 2. Re-emit `contract.json` / `contract.d.ts` / `refs/head.json`
79
- * unconditionally via {@link emitContractSpaceArtefacts}. The
80
- * framework owns these files; re-emit is the contract.
78
+ * 2. Write the head contract into the migrations-root snapshot store
79
+ * (write-if-absent, keyed by hash) and unconditionally re-emit
80
+ * `refs/head.json`, via {@link emitContractSpaceArtifacts}. The
81
+ * framework owns `refs/head.json`; re-emit is the contract for that
82
+ * file. The snapshot itself is only written once per distinct hash.
81
83
  * 3. Materialise any descriptor-shipped migration packages not yet on
82
84
  * disk via {@link materialiseExtensionMigrationPackageIfMissing}.
83
85
  * Existing packages are left untouched (by-existence skip).
@@ -144,7 +146,7 @@ export async function runContractSpaceSeedPhase(
144
146
  const onDiskHeadRef = await readContractSpaceHeadRef(inputs.migrationsDir, space.spaceId);
145
147
  const priorHash = onDiskHeadRef?.hash ?? null;
146
148
 
147
- await emitContractSpaceArtefacts(inputs.migrationsDir, space.spaceId, {
149
+ await emitContractSpaceArtifacts(inputs.migrationsDir, space.spaceId, {
148
150
  contract: descriptor.contractJson,
149
151
  contractDts: buildPlaceholderContractDts(space.spaceId),
150
152
  headRef: { hash: descriptor.headRef.hash, invariants: descriptor.headRef.invariants },
@@ -7,6 +7,25 @@ import type { GlobalFlags } from '../global-flags';
7
7
  import { createColorFormatter, formatDim, isVerbose } from './helpers';
8
8
  import { formatPlannerWarningsBlock } from './migrations';
9
9
 
10
+ /**
11
+ * The display label for a schema-diff issue in the shared error envelope,
12
+ * derived from which sides are present: expected-only is a missing object,
13
+ * actual-only an extra one, both a mismatch. `undefined` when the entry is not
14
+ * a schema-diff issue (it carries neither side) so the caller can fall through
15
+ * to its generic label.
16
+ */
17
+ function schemaDiffIssueLabel(issue: {
18
+ readonly expected?: unknown;
19
+ readonly actual?: unknown;
20
+ }): 'missing' | 'extra' | 'mismatch' | undefined {
21
+ const hasExpected = issue.expected !== undefined;
22
+ const hasActual = issue.actual !== undefined;
23
+ if (hasExpected && hasActual) return 'mismatch';
24
+ if (hasExpected) return 'missing';
25
+ if (hasActual) return 'extra';
26
+ return undefined;
27
+ }
28
+
10
29
  /**
11
30
  * Formats error output for human-readable display.
12
31
  */
@@ -51,13 +70,15 @@ export function formatErrorOutput(error: CliErrorEnvelope, flags: GlobalFlags):
51
70
  // Show issues list if present (always show a short list; show full list when verbose).
52
71
  // `issues` is a shared error-envelope field: PSL interpretation diagnostics stamp
53
72
  // `kind` and `message` (their diagnostic code and prose); schema-diff issues
54
- // (`SchemaDiffIssue`) carry no `message` and stamp `reason` and `path` instead.
73
+ // (`SchemaDiffIssue`) carry no `message` and stamp `path` plus the
74
+ // `expected`/`actual` presence a label is derived from.
55
75
  if (error.meta?.['issues']) {
56
76
  const issues = error.meta['issues'] as readonly {
57
77
  kind?: string;
58
- reason?: string;
59
78
  message?: string;
60
79
  path?: readonly string[];
80
+ expected?: unknown;
81
+ actual?: unknown;
61
82
  }[];
62
83
  if (issues.length > 0) {
63
84
  const maxToShow = isVerbose(flags, 1) ? issues.length : Math.min(3, issues.length);
@@ -66,7 +87,7 @@ export function formatErrorOutput(error: CliErrorEnvelope, flags: GlobalFlags):
66
87
  : ` Issues (showing ${maxToShow} of ${issues.length}):`;
67
88
  lines.push(`${formatDimText(header)}`);
68
89
  for (const issue of issues.slice(0, maxToShow)) {
69
- const label = issue.kind ?? issue.reason ?? 'issue';
90
+ const label = issue.kind ?? schemaDiffIssueLabel(issue) ?? 'issue';
70
91
  const message = issue.message ?? issue.path?.join('/') ?? '';
71
92
  lines.push(`${formatDimText(` - [${label}] ${message}`)}`);
72
93
  }
@@ -8,13 +8,14 @@ import type {
8
8
  VerifyDatabaseResult,
9
9
  VerifyDatabaseSchemaResult,
10
10
  } from '@prisma-next/framework-components/control';
11
+ import { issueOutcome } from '@prisma-next/framework-components/control';
11
12
  import { ifDefined } from '@prisma-next/utils/defined';
12
13
  import { bold, cyan, dim, green, magenta, red, yellow } from 'colorette';
13
14
  import type { GlobalFlags } from '../global-flags';
14
15
  import { createColorFormatter, formatDim, isVerbose } from './helpers';
15
16
 
16
- /** Human-readable label for each failure reason, prefixed onto an issue's message for display. */
17
- const REASON_LABEL: Record<ExpectationFailureReason, string> = {
17
+ /** Human-readable label for each outcome, prefixed onto an issue's message for display. */
18
+ const OUTCOME_LABEL: Record<ExpectationFailureReason, string> = {
18
19
  'not-found': 'missing',
19
20
  'not-expected': 'extra',
20
21
  'not-equal': 'mismatch',
@@ -22,12 +23,12 @@ const REASON_LABEL: Record<ExpectationFailureReason, string> = {
22
23
 
23
24
  /**
24
25
  * The issue's display text: its own path, prefixed with a human label for
25
- * why it's flagged. Turning `reason` (and the path) into prose is this
26
- * formatter's job, not the differ's — the differ's issue is data (`path` +
27
- * `reason` + nodes), not prose.
26
+ * why it's flagged. Turning the presence-derived outcome (and the path) into
27
+ * prose is this formatter's job, not the differ's — the differ's issue is
28
+ * data (`path` + nodes), not prose.
28
29
  */
29
30
  function formatIssueMessage(issue: SchemaDiffIssue): string {
30
- return `${REASON_LABEL[issue.reason]}: ${issue.path.join('/')}`;
31
+ return `${OUTCOME_LABEL[issueOutcome(issue)]}: ${issue.path.join('/')}`;
31
32
  }
32
33
 
33
34
  // ============================================================================