@prisma-next/cli 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/README.md +13 -9
  2. package/dist/cli.mjs +259 -12
  3. package/dist/cli.mjs.map +1 -1
  4. package/dist/{client-oXO2WCPD.mjs → client-KgJorIvG.mjs} +72 -60
  5. package/dist/client-KgJorIvG.mjs.map +1 -0
  6. package/dist/{command-helpers-BSb0tRC8.mjs → command-helpers-Bbw1GbwL.mjs} +646 -46
  7. package/dist/command-helpers-Bbw1GbwL.mjs.map +1 -0
  8. package/dist/commands/contract-emit.d.mts.map +1 -1
  9. package/dist/commands/contract-emit.mjs +1 -1
  10. package/dist/commands/contract-infer.d.mts.map +1 -1
  11. package/dist/commands/contract-infer.mjs +1 -1
  12. package/dist/commands/db-init.d.mts.map +1 -1
  13. package/dist/commands/db-init.mjs +32 -7
  14. package/dist/commands/db-init.mjs.map +1 -1
  15. package/dist/commands/db-schema.d.mts.map +1 -1
  16. package/dist/commands/db-schema.mjs +3 -4
  17. package/dist/commands/db-schema.mjs.map +1 -1
  18. package/dist/commands/db-sign.d.mts.map +1 -1
  19. package/dist/commands/db-sign.mjs +12 -10
  20. package/dist/commands/db-sign.mjs.map +1 -1
  21. package/dist/commands/db-update.d.mts.map +1 -1
  22. package/dist/commands/db-update.mjs +41 -11
  23. package/dist/commands/db-update.mjs.map +1 -1
  24. package/dist/commands/db-verify.d.mts.map +1 -1
  25. package/dist/commands/db-verify.mjs +1 -1
  26. package/dist/commands/migrate.d.mts +6 -2
  27. package/dist/commands/migrate.d.mts.map +1 -1
  28. package/dist/commands/migrate.mjs +75 -40
  29. package/dist/commands/migrate.mjs.map +1 -1
  30. package/dist/commands/migration-check.d.mts +4 -3
  31. package/dist/commands/migration-check.d.mts.map +1 -1
  32. package/dist/commands/migration-check.mjs +1 -280
  33. package/dist/commands/migration-graph.d.mts +13 -2
  34. package/dist/commands/migration-graph.d.mts.map +1 -1
  35. package/dist/commands/migration-graph.mjs +2 -137
  36. package/dist/commands/migration-list.d.mts +64 -4
  37. package/dist/commands/migration-list.d.mts.map +1 -1
  38. package/dist/commands/migration-list.mjs +143 -56
  39. package/dist/commands/migration-list.mjs.map +1 -1
  40. package/dist/commands/migration-log.d.mts +10 -1
  41. package/dist/commands/migration-log.d.mts.map +1 -1
  42. package/dist/commands/migration-log.mjs +10 -15
  43. package/dist/commands/migration-log.mjs.map +1 -1
  44. package/dist/commands/migration-new.d.mts.map +1 -1
  45. package/dist/commands/migration-new.mjs +32 -38
  46. package/dist/commands/migration-new.mjs.map +1 -1
  47. package/dist/commands/migration-plan.d.mts +3 -2
  48. package/dist/commands/migration-plan.d.mts.map +1 -1
  49. package/dist/commands/migration-plan.mjs +1 -1
  50. package/dist/commands/migration-show.d.mts +4 -55
  51. package/dist/commands/migration-show.d.mts.map +1 -1
  52. package/dist/commands/migration-show.mjs +61 -153
  53. package/dist/commands/migration-show.mjs.map +1 -1
  54. package/dist/commands/migration-status.d.mts +12 -49
  55. package/dist/commands/migration-status.d.mts.map +1 -1
  56. package/dist/commands/migration-status.mjs +85 -81
  57. package/dist/commands/migration-status.mjs.map +1 -1
  58. package/dist/commands/ref.d.mts +1 -1
  59. package/dist/commands/ref.d.mts.map +1 -1
  60. package/dist/commands/ref.mjs +38 -10
  61. package/dist/commands/ref.mjs.map +1 -1
  62. package/dist/config-loader-B6sJjXTv.mjs.map +1 -1
  63. package/dist/config-loader.d.mts.map +1 -1
  64. package/dist/contract-at-errors-BxP-TOMl.mjs +42 -0
  65. package/dist/contract-at-errors-BxP-TOMl.mjs.map +1 -0
  66. package/dist/{contract-emit-bcrpT-wD.mjs → contract-emit-D-4jrNve.mjs} +25 -10
  67. package/dist/contract-emit-D-4jrNve.mjs.map +1 -0
  68. package/dist/{contract-emit-r4y8Zhf1.mjs → contract-emit-DxcGl4Uq.mjs} +19 -14
  69. package/dist/contract-emit-DxcGl4Uq.mjs.map +1 -0
  70. package/dist/{contract-enrichment-Dani0mMW.mjs → contract-enrichment-a0V5Y_mL.mjs} +4 -25
  71. package/dist/contract-enrichment-a0V5Y_mL.mjs.map +1 -0
  72. package/dist/{contract-infer-BmySmqVT.mjs → contract-infer-D8uEbJuu.mjs} +4 -5
  73. package/dist/{contract-infer-BmySmqVT.mjs.map → contract-infer-D8uEbJuu.mjs.map} +1 -1
  74. package/dist/contract-space-aggregate-loader-DvZwdkrr.mjs +247 -0
  75. package/dist/contract-space-aggregate-loader-DvZwdkrr.mjs.map +1 -0
  76. package/dist/{db-verify-BClPs3ph.mjs → db-verify-v_vUKXTU.mjs} +5 -7
  77. package/dist/{db-verify-BClPs3ph.mjs.map → db-verify-v_vUKXTU.mjs.map} +1 -1
  78. package/dist/exports/control-api.d.mts +3 -3
  79. package/dist/exports/control-api.d.mts.map +1 -1
  80. package/dist/exports/control-api.mjs +3 -3
  81. package/dist/exports/index.d.mts.map +1 -1
  82. package/dist/exports/index.mjs +1 -1
  83. package/dist/exports/index.mjs.map +1 -1
  84. package/dist/exports/init-output.d.mts.map +1 -1
  85. package/dist/exports/init-output.mjs +1 -1
  86. package/dist/extension-pack-inputs-IDvjRCi3.mjs +62 -0
  87. package/dist/extension-pack-inputs-IDvjRCi3.mjs.map +1 -0
  88. package/dist/{framework-components-65gOHkHB.mjs → framework-components-fYXjz_in.mjs} +2 -2
  89. package/dist/{framework-components-65gOHkHB.mjs.map → framework-components-fYXjz_in.mjs.map} +1 -1
  90. package/dist/global-flags-DEHjV8_s.d.mts +34 -0
  91. package/dist/global-flags-DEHjV8_s.d.mts.map +1 -0
  92. package/dist/{graph-render-DJVv0_uf.mjs → graph-render-rFAqZujX.mjs} +2 -2
  93. package/dist/{graph-render-DJVv0_uf.mjs.map → graph-render-rFAqZujX.mjs.map} +1 -1
  94. package/dist/{init-BCJZPWE1.mjs → init-Cv9UzWL5.mjs} +20 -269
  95. package/dist/init-Cv9UzWL5.mjs.map +1 -0
  96. package/dist/{inspect-live-schema-DSRbFoOL.mjs → inspect-live-schema-C6ohV_oQ.mjs} +4 -5
  97. package/dist/{inspect-live-schema-DSRbFoOL.mjs.map → inspect-live-schema-C6ohV_oQ.mjs.map} +1 -1
  98. package/dist/migration-check-BiBJoYYW.mjs +341 -0
  99. package/dist/migration-check-BiBJoYYW.mjs.map +1 -0
  100. package/dist/migration-cli.d.mts.map +1 -1
  101. package/dist/migration-cli.mjs +4 -4
  102. package/dist/migration-cli.mjs.map +1 -1
  103. package/dist/{migration-command-scaffold-Bzd9La5c.mjs → migration-command-scaffold-CjvwO6at.mjs} +4 -5
  104. package/dist/{migration-command-scaffold-Bzd9La5c.mjs.map → migration-command-scaffold-CjvwO6at.mjs.map} +1 -1
  105. package/dist/migration-graph-D7DVUElV.mjs +1232 -0
  106. package/dist/migration-graph-D7DVUElV.mjs.map +1 -0
  107. package/dist/migration-list-styler-BRwF4-gy.mjs +399 -0
  108. package/dist/migration-list-styler-BRwF4-gy.mjs.map +1 -0
  109. package/dist/{migration-plan-CFwqw3Gk.mjs → migration-plan-9DJ7q7_z.mjs} +372 -133
  110. package/dist/migration-plan-9DJ7q7_z.mjs.map +1 -0
  111. package/dist/{migration-types-BXWvz12q.d.mts → migration-types-D2FW63pr.d.mts} +1 -1
  112. package/dist/{migration-types-BXWvz12q.d.mts.map → migration-types-D2FW63pr.d.mts.map} +1 -1
  113. package/dist/{migrations-CwZMa1Ck.mjs → migrations-Cv2jxNNK.mjs} +12 -13
  114. package/dist/migrations-Cv2jxNNK.mjs.map +1 -0
  115. package/dist/{output-BlsrGMEF.mjs → output-B60Gw5fu.mjs} +1 -1
  116. package/dist/{output-BlsrGMEF.mjs.map → output-B60Gw5fu.mjs.map} +1 -1
  117. package/dist/{progress-adapter-DFfvZcYL.mjs → progress-adapter-C644QK8l.mjs} +1 -1
  118. package/dist/{progress-adapter-DFfvZcYL.mjs.map → progress-adapter-C644QK8l.mjs.map} +1 -1
  119. package/dist/ref-advancement-DUZqsue6.mjs +50 -0
  120. package/dist/ref-advancement-DUZqsue6.mjs.map +1 -0
  121. package/dist/terminal-ui-5Y6mrg93.d.mts +133 -0
  122. package/dist/terminal-ui-5Y6mrg93.d.mts.map +1 -0
  123. package/dist/{types--CqjMdk0.d.mts → types-Dt_SfqFm.d.mts} +28 -28
  124. package/dist/types-Dt_SfqFm.d.mts.map +1 -0
  125. package/dist/{verify-Bom75OYI.mjs → verify-DCA9Sldu.mjs} +2 -2
  126. package/dist/{verify-Bom75OYI.mjs.map → verify-DCA9Sldu.mjs.map} +1 -1
  127. package/package.json +35 -24
  128. package/src/commands/contract-emit.ts +19 -7
  129. package/src/commands/contract-infer.ts +1 -1
  130. package/src/commands/db-init.ts +48 -2
  131. package/src/commands/db-sign.ts +9 -5
  132. package/src/commands/db-update.ts +54 -8
  133. package/src/commands/init/hygiene-gitattributes.ts +2 -2
  134. package/src/commands/init/index.ts +2 -1
  135. package/src/commands/init/templates/code-templates.ts +4 -2
  136. package/src/commands/init/templates/env.ts +13 -14
  137. package/src/commands/migrate.ts +125 -44
  138. package/src/commands/migration-check.ts +43 -83
  139. package/src/commands/migration-graph.ts +75 -60
  140. package/src/commands/migration-list.ts +220 -74
  141. package/src/commands/migration-log.ts +8 -14
  142. package/src/commands/migration-new.ts +44 -48
  143. package/src/commands/migration-plan.ts +412 -197
  144. package/src/commands/migration-show.ts +65 -284
  145. package/src/commands/migration-status.ts +127 -124
  146. package/src/commands/ref.ts +53 -8
  147. package/src/control-api/client.ts +0 -1
  148. package/src/control-api/contract-enrichment.ts +6 -42
  149. package/src/control-api/operations/{apply-aggregate.ts → apply.ts} +44 -75
  150. package/src/control-api/operations/contract-emit.ts +14 -6
  151. package/src/control-api/operations/{db-apply-aggregate.ts → db-apply.ts} +19 -19
  152. package/src/control-api/operations/db-init.ts +4 -4
  153. package/src/control-api/operations/db-update.ts +4 -4
  154. package/src/control-api/operations/db-verify.ts +15 -11
  155. package/src/control-api/operations/migration-apply.ts +56 -47
  156. package/src/control-api/types.ts +26 -27
  157. package/src/migration-cli.ts +4 -4
  158. package/src/utils/cli-errors.ts +234 -0
  159. package/src/utils/command-helpers.ts +9 -24
  160. package/src/utils/contract-at-errors.ts +96 -0
  161. package/src/utils/contract-space-aggregate-loader.ts +336 -117
  162. package/src/utils/formatters/migration-graph-layout.ts +1119 -0
  163. package/src/utils/formatters/migration-graph-rows.ts +336 -0
  164. package/src/utils/formatters/migration-graph-tree-render.ts +459 -0
  165. package/src/utils/formatters/migration-list-data-column.ts +115 -0
  166. package/src/utils/formatters/migration-list-graph-topology.ts +368 -0
  167. package/src/utils/formatters/migration-list-render.ts +191 -0
  168. package/src/utils/formatters/migration-list-styler.ts +63 -0
  169. package/src/utils/formatters/migration-list-types.ts +21 -0
  170. package/src/utils/formatters/migrations.ts +37 -46
  171. package/src/utils/glyph-mode.ts +22 -0
  172. package/src/utils/integrity-violation-to-check-failure.ts +130 -0
  173. package/src/utils/plan-resolution.ts +258 -0
  174. package/src/utils/ref-advancement.ts +68 -0
  175. package/src/utils/terminal-ui.ts +42 -1
  176. package/dist/cli-errors-Czmx92Zy.d.mts +0 -3
  177. package/dist/cli-errors-Djtz98Vm.mjs +0 -71
  178. package/dist/cli-errors-Djtz98Vm.mjs.map +0 -1
  179. package/dist/client-oXO2WCPD.mjs.map +0 -1
  180. package/dist/command-helpers-BSb0tRC8.mjs.map +0 -1
  181. package/dist/commands/migration-check.mjs.map +0 -1
  182. package/dist/commands/migration-graph.mjs.map +0 -1
  183. package/dist/contract-emit-bcrpT-wD.mjs.map +0 -1
  184. package/dist/contract-emit-r4y8Zhf1.mjs.map +0 -1
  185. package/dist/contract-enrichment-Dani0mMW.mjs.map +0 -1
  186. package/dist/contract-space-aggregate-loader-BmNQwlws.mjs +0 -160
  187. package/dist/contract-space-aggregate-loader-BmNQwlws.mjs.map +0 -1
  188. package/dist/global-flags-CdE7M0d9.d.mts +0 -15
  189. package/dist/global-flags-CdE7M0d9.d.mts.map +0 -1
  190. package/dist/init-BCJZPWE1.mjs.map +0 -1
  191. package/dist/migration-plan-CFwqw3Gk.mjs.map +0 -1
  192. package/dist/migrations-CwZMa1Ck.mjs.map +0 -1
  193. package/dist/rolldown-runtime-twds-ZHy.mjs +0 -14
  194. package/dist/terminal-ui-BiB_8KNo.mjs +0 -379
  195. package/dist/terminal-ui-BiB_8KNo.mjs.map +0 -1
  196. package/dist/types--CqjMdk0.d.mts.map +0 -1
@@ -1,10 +1,14 @@
1
+ import type { Contract } from '@prisma-next/contract/types';
1
2
  import {
2
3
  createControlStack,
3
4
  type MigrationPlanOperation,
4
5
  } from '@prisma-next/framework-components/control';
5
6
  import {
6
7
  type ContractMarkerRecordLike,
8
+ type ContractSpaceAggregate,
7
9
  graphWalkStrategy,
10
+ loadContractSpaceAggregate,
11
+ requireHeadRef,
8
12
  } from '@prisma-next/migration-tools/aggregate';
9
13
  import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
10
14
  import {
@@ -39,7 +43,6 @@ import {
39
43
  import {
40
44
  addGlobalOptions,
41
45
  collectDeclaredInvariants,
42
- loadMigrationPackages,
43
46
  maskConnectionUrl,
44
47
  readContractEnvelope,
45
48
  resolveMigrationPaths,
@@ -50,9 +53,12 @@ import {
50
53
  toStructuralEdge,
51
54
  } from '../utils/command-helpers';
52
55
  import {
53
- type BuildAggregateInputs,
54
- buildContractSpaceAggregate,
56
+ appContractStandInFromIdentity,
57
+ loadContractRawSafely,
58
+ refuseContractSpaceIntegrity,
59
+ refusePackageCorruptionOnAggregate,
55
60
  } from '../utils/contract-space-aggregate-loader';
61
+ import { toDeclaredExtensionsFromRaw } from '../utils/extension-pack-inputs';
56
62
  import {
57
63
  type EdgeStatus,
58
64
  type EdgeStatusKind,
@@ -184,10 +190,9 @@ export interface MigrationStatusResult {
184
190
  * migrations directory) where the existing diagnostics already
185
191
  * surface the failure.
186
192
  *
187
- * The legacy top-level fields (`migrations`, `markerHash`,
188
- * `targetHash`, `pathDecision`, …) describe the **app member**
189
- * specifically back-compat with single-space callers. Per-space
190
- * detail for extension members lives only on this list.
193
+ * The top-level fields (`migrations`, `markerHash`, `targetHash`,
194
+ * `pathDecision`, …) describe the **app member** specifically.
195
+ * Per-space detail for extension members lives only on this list.
191
196
  */
192
197
  readonly spaces?: readonly MigrationStatusSpaceEntry[];
193
198
  /** Cross-space pending-migration total (sum of `spaces[].pendingCount`). Present when `spaces` is. */
@@ -424,55 +429,50 @@ function resolveDisplayChain(
424
429
  /**
425
430
  * Build the aggregate enumeration of contract spaces for the status
426
431
  * output. Loads the aggregate from disk (lossy on failure — extension
427
- * spaces are simply omitted, the existing single-space app behaviour
428
- * keeps working), reads per-space marker rows when online, and uses
432
+ * spaces are simply omitted, the app member's output keeps working),
433
+ * reads per-space marker rows when online, and uses
429
434
  * {@link graphWalkStrategy} to compute each space's pending count.
430
435
  *
431
- * Sub-spec § `migration status` semantics the aggregate-walking
432
- * version reports per-space marker + pending state alongside the
433
- * cross-space totals.
436
+ * The aggregate-walking status reports per-space marker + pending
437
+ * state alongside the cross-space totals.
434
438
  */
435
439
  export async function loadAggregateStatusSpaces(args: {
436
- readonly targetId: string;
437
- readonly migrationsDir: string;
438
- readonly appContractRaw: unknown;
439
- readonly extensionPacks: BuildAggregateInputs<string, string>['extensionPacks'];
440
- readonly deserializeContract: BuildAggregateInputs<string, string>['deserializeContract'];
440
+ readonly aggregate: ContractSpaceAggregate;
441
+ readonly extensionPacks: ReadonlyArray<unknown>;
441
442
  readonly markersBySpace: ReadonlyMap<string, ContractMarkerRecordLike> | null;
442
443
  }): Promise<readonly MigrationStatusSpaceEntry[]> {
443
- const loadInputs: BuildAggregateInputs<string, string> = {
444
- targetId: args.targetId,
445
- migrationsDir: args.migrationsDir,
446
- appContract: args.deserializeContract(args.appContractRaw),
447
- extensionPacks: args.extensionPacks,
448
- deserializeContract: args.deserializeContract,
449
- };
450
-
451
- const loaded = await buildContractSpaceAggregate(loadInputs);
452
- if (!loaded.ok) {
453
- // Loader failure (drift, layout violation, etc.) — surfacing it
454
- // as a status diagnostic would duplicate `migration plan`'s job.
455
- // The single-space app pipeline still runs; extensions are simply
456
- // not enumerated.
444
+ const declaredExtensions = toDeclaredExtensionsFromRaw(args.extensionPacks);
445
+ if (
446
+ refuseContractSpaceIntegrity(args.aggregate, {
447
+ declaredExtensions,
448
+ checkContracts: true,
449
+ })
450
+ ) {
451
+ // Full integrity refusal (drift, layout violation, etc.) — surfacing
452
+ // it as a status diagnostic would duplicate `migration plan`'s job.
453
+ // The app pipeline still runs; extensions are simply not enumerated.
457
454
  return [];
458
455
  }
459
- const aggregate = loaded.value;
456
+ const aggregate = args.aggregate;
460
457
 
461
458
  const orderedMembers = [...aggregate.extensions, aggregate.app];
462
459
  const rows: MigrationStatusSpaceEntry[] = [];
463
460
  for (const member of orderedMembers) {
464
461
  const liveMarker = args.markersBySpace?.get(member.spaceId) ?? null;
465
462
  const isApp = member.spaceId === aggregate.app.spaceId;
463
+ // The aggregate passed the integrity gate above, so every member has
464
+ // a resolved head ref (a missing one would have refused the load).
465
+ const headRef = requireHeadRef(member);
466
466
 
467
- if (member.migrations.graph.nodes.size === 0) {
467
+ if (member.graph().nodes.size === 0) {
468
468
  rows.push({
469
469
  spaceId: member.spaceId,
470
470
  kind: isApp ? 'app' : 'extension',
471
- headHash: member.headRef.hash,
471
+ headHash: headRef.hash,
472
472
  ...(args.markersBySpace !== null
473
473
  ? {
474
474
  markerHash: liveMarker?.storageHash ?? null,
475
- status: member.headRef.hash === EMPTY_CONTRACT_HASH ? 'up-to-date' : 'never-planned',
475
+ status: headRef.hash === EMPTY_CONTRACT_HASH ? 'up-to-date' : 'never-planned',
476
476
  pendingCount: 0,
477
477
  }
478
478
  : {}),
@@ -484,7 +484,7 @@ export async function loadAggregateStatusSpaces(args: {
484
484
  rows.push({
485
485
  spaceId: member.spaceId,
486
486
  kind: isApp ? 'app' : 'extension',
487
- headHash: member.headRef.hash,
487
+ headHash: headRef.hash,
488
488
  });
489
489
  continue;
490
490
  }
@@ -513,7 +513,7 @@ export async function loadAggregateStatusSpaces(args: {
513
513
  rows.push({
514
514
  spaceId: member.spaceId,
515
515
  kind: isApp ? 'app' : 'extension',
516
- headHash: member.headRef.hash,
516
+ headHash: headRef.hash,
517
517
  markerHash: liveMarker?.storageHash ?? null,
518
518
  pendingCount,
519
519
  ...(status ? { status } : {}),
@@ -528,17 +528,6 @@ export async function loadAggregateStatusSpaces(args: {
528
528
  * the existing `readContractEnvelope` path will report the same
529
529
  * problem via a status diagnostic, no need to double-surface.
530
530
  */
531
- async function loadContractRawSafely(config: {
532
- contract?: { output?: string };
533
- }): Promise<unknown | null> {
534
- try {
535
- const path = (await import('../utils/command-helpers')).resolveContractPath(config);
536
- const raw = await (await import('node:fs/promises')).readFile(path, 'utf-8');
537
- return JSON.parse(raw) as unknown;
538
- } catch {
539
- return null;
540
- }
541
- }
542
531
 
543
532
  async function validateOnlineMarkerRead(
544
533
  config: Awaited<ReturnType<typeof loadConfig>>,
@@ -580,8 +569,10 @@ async function executeMigrationStatusCommand(
580
569
  ui: TerminalUI,
581
570
  ): Promise<Result<MigrationStatusResult, CliStructuredError>> {
582
571
  const config = await loadConfig(options.config);
583
- const { configPath, appMigrationsDir, appMigrationsRelative, migrationsDir, refsDir } =
584
- resolveMigrationPaths(options.config, config);
572
+ const { configPath, appMigrationsRelative, migrationsDir, refsDir } = resolveMigrationPaths(
573
+ options.config,
574
+ config,
575
+ );
585
576
 
586
577
  const dbConnection = options.db ?? config.db?.connection;
587
578
  const hasDriver = !!config.driver;
@@ -599,37 +590,92 @@ async function executeMigrationStatusCommand(
599
590
  throw error;
600
591
  }
601
592
 
602
- let fromOverrideHash: string | undefined;
593
+ const diagnostics: StatusDiagnostic[] = [];
594
+ let contractHash: string = EMPTY_CONTRACT_HASH;
595
+ try {
596
+ const envelope = await readContractEnvelope(config);
597
+ contractHash = envelope.storageHash;
598
+ } catch (error) {
599
+ diagnostics.push({
600
+ code: 'CONTRACT.UNREADABLE',
601
+ severity: 'warn',
602
+ message: `Could not read contract: ${error instanceof Error ? error.message : 'unknown error'}`,
603
+ hints: ["Run 'prisma-next contract emit' to generate a valid contract"],
604
+ });
605
+ }
603
606
 
604
- if (options.to || options.from) {
607
+ const contractRawForAggregate = await loadContractRawSafely(config);
608
+ const stack = createControlStack(config);
609
+ const familyInstance = config.family.create(stack);
610
+ const deserializeContract = (json: unknown): Contract => familyInstance.deserializeContract(json);
611
+ const appContractStandIn = appContractStandInFromIdentity({
612
+ contractHash,
613
+ targetId: config.target.id,
614
+ targetFamily: config.target.familyId,
615
+ });
616
+ let appContractForLoad: Contract = appContractStandIn;
617
+ if (contractRawForAggregate !== null) {
605
618
  try {
606
- const { graph: earlyGraph } = await loadMigrationPackages(appMigrationsDir);
619
+ appContractForLoad = deserializeContract(contractRawForAggregate);
620
+ } catch (error) {
621
+ diagnostics.push({
622
+ code: 'CONTRACT.UNREADABLE',
623
+ severity: 'warn',
624
+ message: `Could not deserialize contract: ${error instanceof Error ? error.message : 'unknown error'}`,
625
+ hints: ["Run 'prisma-next contract emit' to generate a valid contract"],
626
+ });
627
+ }
628
+ }
607
629
 
608
- if (options.to) {
609
- const refResult = parseContractRef(options.to, { graph: earlyGraph, refs: allRefs });
610
- if (!refResult.ok) {
611
- return notOk(mapRefResolutionError(refResult.failure));
612
- }
613
- activeRefHash = refResult.value.hash;
614
- if (refResult.value.provenance.kind === 'ref') {
615
- const resolvedRefName = refResult.value.provenance.refName;
616
- activeRefName = resolvedRefName;
617
- activeRefEntry = allRefs[resolvedRefName];
618
- }
619
- }
630
+ let aggregate: ContractSpaceAggregate;
631
+ try {
632
+ aggregate = await loadContractSpaceAggregate({
633
+ migrationsDir,
634
+ deserializeContract,
635
+ appContract: appContractForLoad,
636
+ });
637
+ } catch (error) {
638
+ if (MigrationToolsError.is(error)) {
639
+ return notOk(mapMigrationToolsError(error));
640
+ }
641
+ return notOk(
642
+ errorUnexpected(error instanceof Error ? error.message : String(error), {
643
+ why: `Failed to read migrations directory: ${error instanceof Error ? error.message : String(error)}`,
644
+ }),
645
+ );
646
+ }
620
647
 
621
- if (options.from) {
622
- const fromResult = parseContractRef(options.from, { graph: earlyGraph, refs: allRefs });
623
- if (!fromResult.ok) {
624
- return notOk(mapRefResolutionError(fromResult.failure));
625
- }
626
- fromOverrideHash = fromResult.value.hash;
648
+ if (contractRawForAggregate !== null) {
649
+ const corruptionFailure = refusePackageCorruptionOnAggregate(aggregate);
650
+ if (corruptionFailure) {
651
+ return notOk(corruptionFailure);
652
+ }
653
+ }
654
+
655
+ const appGraph = aggregate.app.graph();
656
+
657
+ let fromOverrideHash: string | undefined;
658
+
659
+ if (options.to || options.from) {
660
+ if (options.to) {
661
+ const refResult = parseContractRef(options.to, { graph: appGraph, refs: allRefs });
662
+ if (!refResult.ok) {
663
+ return notOk(mapRefResolutionError(refResult.failure));
627
664
  }
628
- } catch (error) {
629
- if (MigrationToolsError.is(error)) {
630
- return notOk(mapMigrationToolsError(error));
665
+ activeRefHash = refResult.value.hash;
666
+ if (refResult.value.provenance.kind === 'ref') {
667
+ const resolvedRefName = refResult.value.provenance.refName;
668
+ activeRefName = resolvedRefName;
669
+ activeRefEntry = allRefs[resolvedRefName];
631
670
  }
632
- throw error;
671
+ }
672
+
673
+ if (options.from) {
674
+ const fromResult = parseContractRef(options.from, { graph: appGraph, refs: allRefs });
675
+ if (!fromResult.ok) {
676
+ return notOk(mapRefResolutionError(fromResult.failure));
677
+ }
678
+ fromOverrideHash = fromResult.value.hash;
633
679
  }
634
680
  }
635
681
 
@@ -670,34 +716,8 @@ async function executeMigrationStatusCommand(
670
716
  ui.stderr(header);
671
717
  }
672
718
 
673
- const diagnostics: StatusDiagnostic[] = [];
674
- let contractHash: string = EMPTY_CONTRACT_HASH;
675
- try {
676
- const envelope = await readContractEnvelope(config);
677
- contractHash = envelope.storageHash;
678
- } catch (error) {
679
- diagnostics.push({
680
- code: 'CONTRACT.UNREADABLE',
681
- severity: 'warn',
682
- message: `Could not read contract: ${error instanceof Error ? error.message : 'unknown error'}`,
683
- hints: ["Run 'prisma-next contract emit' to generate a valid contract"],
684
- });
685
- }
686
-
687
- let bundles: readonly OnDiskMigrationPackage[];
688
- let graph: MigrationGraph;
689
- try {
690
- ({ bundles, graph } = await loadMigrationPackages(appMigrationsDir));
691
- } catch (error) {
692
- if (MigrationToolsError.is(error)) {
693
- return notOk(mapMigrationToolsError(error));
694
- }
695
- return notOk(
696
- errorUnexpected(error instanceof Error ? error.message : String(error), {
697
- why: `Failed to read migrations directory: ${error instanceof Error ? error.message : String(error)}`,
698
- }),
699
- );
700
- }
719
+ const bundles = aggregate.app.packages;
720
+ const graph = appGraph;
701
721
 
702
722
  if (bundles.length === 0) {
703
723
  if (dbConnection && hasDriver) {
@@ -772,8 +792,8 @@ async function executeMigrationStatusCommand(
772
792
  mode = 'online';
773
793
  // Read every space's marker so the aggregate enumeration can
774
794
  // surface per-space marker state. `readAllMarkers` mirrors what
775
- // `db init` / `db update` already use to drive the multi-space
776
- // planner; here it powers the aggregate status output.
795
+ // `db init` / `db update` already use to drive the planner;
796
+ // here it powers the aggregate status output.
777
797
  //
778
798
  // Probe for the method first so we only swallow the
779
799
  // unsupported-method case: older family instances may not
@@ -808,32 +828,15 @@ async function executeMigrationStatusCommand(
808
828
  allMarkers = null;
809
829
  }
810
830
 
811
- // Build the aggregate enumeration of contract spaces. Lossy on
812
- // failure (extensions are simply omitted) so the existing
813
- // single-space app pipeline below still runs even if extensions
814
- // can't be loaded — a strict failure here would degrade the
815
- // load-bearing app-space output for unrelated reasons.
816
- const contractRawForAggregate = await loadContractRawSafely(config);
817
831
  let aggregateSpaces: readonly MigrationStatusSpaceEntry[] = [];
818
832
  if (contractRawForAggregate !== null) {
819
- // The aggregate loader needs a typed-Contract producer. Build a
820
- // real control stack so `deserializeContract` runs against a fully
821
- // composed family instance — descriptors that read stack members
822
- // during construction (e.g. codec lookups) get a consistent view.
823
- const stack = createControlStack(config);
824
- const familyInstance = config.family.create(stack);
825
833
  try {
826
834
  aggregateSpaces = await loadAggregateStatusSpaces({
827
- targetId: config.target.targetId,
828
- migrationsDir,
829
- appContractRaw: contractRawForAggregate,
835
+ aggregate,
830
836
  extensionPacks: config.extensionPacks ?? [],
831
- deserializeContract: (json: unknown) => familyInstance.deserializeContract(json),
832
837
  markersBySpace: allMarkers,
833
838
  });
834
839
  } catch {
835
- // Loader failure short-circuits silently — the existing
836
- // single-space app pipeline below still runs.
837
840
  aggregateSpaces = [];
838
841
  }
839
842
  }
@@ -1252,7 +1255,7 @@ export function formatStatusSummary(result: MigrationStatusResult, colorize: boo
1252
1255
  }
1253
1256
 
1254
1257
  // Per-space section. Suppressed when there's no extension space —
1255
- // the legacy single-space output already covers the app member.
1258
+ // the top-level output already covers the app member.
1256
1259
  // When extensions exist, render every space (including the app)
1257
1260
  // for consistency, plus a cross-space pending total + apply hint.
1258
1261
  if (result.spaces?.some((s) => s.kind === 'extension')) {
@@ -1,18 +1,26 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
1
3
  import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
4
+ import { findLatestMigration, isGraphNode } from '@prisma-next/migration-tools/migration-graph';
2
5
  import { parseContractRef } from '@prisma-next/migration-tools/ref-resolution';
3
6
  import type { RefEntry } from '@prisma-next/migration-tools/refs';
4
7
  import {
5
- deleteRef,
8
+ deleteRefPaired,
6
9
  readRefs,
7
10
  validateRefName,
8
11
  validateRefValue,
9
- writeRef,
12
+ writeRefPaired,
10
13
  } from '@prisma-next/migration-tools/refs';
11
14
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
12
15
  import { Command } from 'commander';
16
+ import { join } from 'pathe';
13
17
  import { loadConfig } from '../config-loader';
14
18
  import {
15
19
  CliStructuredError,
20
+ errorFileNotFound,
21
+ errorRefSetBundleNotFound,
22
+ errorRefSetEmptySentinel,
23
+ errorRefSetHashNotInGraph,
16
24
  errorRuntime,
17
25
  errorUnexpected,
18
26
  mapMigrationToolsError,
@@ -20,12 +28,13 @@ import {
20
28
  } from '../utils/cli-errors';
21
29
  import {
22
30
  addGlobalOptions,
23
- loadMigrationPackages,
24
31
  resolveMigrationPaths,
25
32
  setCommandDescriptions,
26
33
  } from '../utils/command-helpers';
34
+ import { buildReadAggregate } from '../utils/contract-space-aggregate-loader';
27
35
  import { formatCommandHelp } from '../utils/formatters/help';
28
36
  import { parseGlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';
37
+ import { readContractIR } from '../utils/ref-advancement';
29
38
  import { handleResult } from '../utils/result-handler';
30
39
  import { createTerminalUI } from '../utils/terminal-ui';
31
40
 
@@ -72,14 +81,19 @@ export async function executeRefSetCommand(
72
81
 
73
82
  try {
74
83
  const config = await loadConfig(options.config);
75
- const { appMigrationsDir, refsDir } = resolveMigrationPaths(options.config, config);
84
+ const { migrationsDir, refsDir } = resolveMigrationPaths(options.config, config);
85
+ const loaded = await buildReadAggregate(config, { migrationsDir });
86
+ if (!loaded.ok) {
87
+ return notOk(loaded.failure);
88
+ }
89
+ const graph = loaded.value.aggregate.app.graph();
90
+ const bundles = loaded.value.aggregate.app.packages;
91
+ const refs = loaded.value.aggregate.app.refs;
76
92
 
77
93
  let resolvedHash: string;
78
94
  if (validateRefValue(contractInput)) {
79
95
  resolvedHash = contractInput;
80
96
  } else {
81
- const { graph } = await loadMigrationPackages(appMigrationsDir);
82
- const refs = await readRefs(refsDir);
83
97
  const refResult = parseContractRef(contractInput, { graph, refs });
84
98
  if (!refResult.ok) {
85
99
  return notOk(mapRefResolutionError(refResult.failure));
@@ -87,8 +101,39 @@ export async function executeRefSetCommand(
87
101
  resolvedHash = refResult.value.hash;
88
102
  }
89
103
 
104
+ if (resolvedHash === EMPTY_CONTRACT_HASH) {
105
+ return notOk(errorRefSetEmptySentinel(resolvedHash));
106
+ }
107
+ if (!isGraphNode(resolvedHash, graph)) {
108
+ const graphTip = findLatestMigration(graph)?.to ?? null;
109
+ return notOk(errorRefSetHashNotInGraph(resolvedHash, [...graph.nodes].sort(), graphTip));
110
+ }
111
+
112
+ const matchingBundle = bundles.find((bundle) => bundle.metadata.to === resolvedHash);
113
+ if (!matchingBundle) {
114
+ return notOk(errorRefSetBundleNotFound(resolvedHash));
115
+ }
116
+
117
+ const contractJsonPath = join(matchingBundle.dirPath, 'end-contract.json');
118
+ let contractJson: Record<string, unknown>;
119
+ try {
120
+ const raw = await readFile(contractJsonPath, 'utf-8');
121
+ contractJson = JSON.parse(raw) as Record<string, unknown>;
122
+ } catch (readError) {
123
+ if (readError instanceof Error && (readError as NodeJS.ErrnoException).code === 'ENOENT') {
124
+ return notOk(
125
+ errorFileNotFound(contractJsonPath, {
126
+ why: `Migration bundle for hash ${resolvedHash} is missing its end-contract snapshot at ${contractJsonPath}`,
127
+ fix: 'Run `pnpm fixtures:check`, or re-emit the migration so its end-contract.json is restored.',
128
+ }),
129
+ );
130
+ }
131
+ throw readError;
132
+ }
133
+
134
+ const contractIR = await readContractIR(contractJson, contractJsonPath);
90
135
  const entry: RefEntry = { hash: resolvedHash, invariants: [] };
91
- await writeRef(refsDir, name, entry);
136
+ await writeRefPaired(refsDir, name, entry, contractIR);
92
137
  return ok({ ok: true as const, ref: name, hash: resolvedHash, invariants: [] });
93
138
  } catch (error) {
94
139
  if (error instanceof CliStructuredError) return notOk(error);
@@ -103,7 +148,7 @@ export async function executeRefDeleteCommand(
103
148
  try {
104
149
  const config = await loadConfig(options.config);
105
150
  const { refsDir } = resolveMigrationPaths(options.config, config);
106
- await deleteRef(refsDir, name);
151
+ await deleteRefPaired(refsDir, name);
107
152
  return ok({ ok: true as const, ref: name, deleted: true as const });
108
153
  } catch (error) {
109
154
  if (error instanceof CliStructuredError) return notOk(error);
@@ -473,7 +473,6 @@ class ControlClientImpl implements ControlClient {
473
473
  migrationsDir: options.migrationsDir,
474
474
  extensionPacks: this.options.extensionPacks ?? [],
475
475
  targetId: this.options.target.targetId,
476
- appMigrationPackages: options.appMigrationPackages,
477
476
  ...ifDefined('refHash', options.refHash),
478
477
  ...ifDefined('refInvariants', options.refInvariants),
479
478
  ...ifDefined('refName', options.refName),
@@ -1,7 +1,8 @@
1
1
  import type { Contract } from '@prisma-next/contract/types';
2
- import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
3
-
4
- type CapabilityMatrix = Record<string, Record<string, boolean>>;
2
+ import {
3
+ mergeCapabilityMatrices,
4
+ type TargetBoundComponentDescriptor,
5
+ } from '@prisma-next/framework-components/components';
5
6
 
6
7
  function isPlainObject(value: unknown): value is Record<string, unknown> {
7
8
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -26,37 +27,6 @@ function sortDeepTyped<T>(value: T): T {
26
27
  return sortDeep(value) as T;
27
28
  }
28
29
 
29
- function extractCapabilityMatrix(value: unknown): CapabilityMatrix {
30
- if (!isPlainObject(value)) return {};
31
-
32
- const out: CapabilityMatrix = {};
33
- for (const [namespace, maybeCaps] of Object.entries(value)) {
34
- if (!isPlainObject(maybeCaps)) continue;
35
- const caps: Record<string, boolean> = {};
36
- for (const [key, flag] of Object.entries(maybeCaps)) {
37
- if (typeof flag === 'boolean') {
38
- caps[key] = flag;
39
- }
40
- }
41
- if (Object.keys(caps).length > 0) {
42
- out[namespace] = caps;
43
- }
44
- }
45
-
46
- return out;
47
- }
48
-
49
- function mergeCapabilities(left: CapabilityMatrix, right: CapabilityMatrix): CapabilityMatrix {
50
- const next: CapabilityMatrix = { ...left };
51
- for (const [namespace, capabilities] of Object.entries(right)) {
52
- next[namespace] = {
53
- ...(left[namespace] ?? {}),
54
- ...capabilities,
55
- };
56
- }
57
- return next;
58
- }
59
-
60
30
  function extractExtensionPackMeta(
61
31
  component: TargetBoundComponentDescriptor<string, string>,
62
32
  ): Record<string, unknown> {
@@ -93,16 +63,10 @@ export function enrichContract(
93
63
  ir: Contract,
94
64
  components: ReadonlyArray<TargetBoundComponentDescriptor<string, string>>,
95
65
  ): Contract {
96
- let mergedCapabilities = ir.capabilities;
97
- const extensionPacksMeta: Record<string, unknown> = {};
66
+ const mergedCapabilities = mergeCapabilityMatrices(ir.capabilities, components);
98
67
 
68
+ const extensionPacksMeta: Record<string, unknown> = {};
99
69
  for (const component of components) {
100
- if (component.capabilities) {
101
- mergedCapabilities = mergeCapabilities(
102
- mergedCapabilities,
103
- extractCapabilityMatrix(component.capabilities),
104
- );
105
- }
106
70
  if (component.kind === 'extension') {
107
71
  extensionPacksMeta[component.id] = extractExtensionPackMeta(component);
108
72
  }