@prisma-next/cli 0.11.0-dev.6 → 0.11.0-dev.61

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 (180) hide show
  1. package/README.md +13 -9
  2. package/dist/cli.mjs +9 -10
  3. package/dist/cli.mjs.map +1 -1
  4. package/dist/{client-oXO2WCPD.mjs → client-CD3om3R0.mjs} +44 -24
  5. package/dist/client-CD3om3R0.mjs.map +1 -0
  6. package/dist/{command-helpers-DtavI0wJ.mjs → command-helpers-CoceqqMl.mjs} +642 -45
  7. package/dist/command-helpers-CoceqqMl.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 +5 -1
  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 +11 -2
  34. package/dist/commands/migration-graph.d.mts.map +1 -1
  35. package/dist/commands/migration-graph.mjs +15 -30
  36. package/dist/commands/migration-graph.mjs.map +1 -1
  37. package/dist/commands/migration-list.d.mts +66 -4
  38. package/dist/commands/migration-list.d.mts.map +1 -1
  39. package/dist/commands/migration-list.mjs +2 -103
  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 +2 -1
  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 +5 -40
  55. package/dist/commands/migration-status.d.mts.map +1 -1
  56. package/dist/commands/migration-status.mjs +81 -76
  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-Bhf2jnkp.mjs +42 -0
  65. package/dist/contract-at-errors-Bhf2jnkp.mjs.map +1 -0
  66. package/dist/{contract-emit-CmsklifJ.mjs → contract-emit-C47r1loe.mjs} +4 -6
  67. package/dist/{contract-emit-CmsklifJ.mjs.map → contract-emit-C47r1loe.mjs.map} +1 -1
  68. package/dist/{contract-emit-o-8VmdQX.mjs → contract-emit-DxEfEc-M.mjs} +21 -7
  69. package/dist/{contract-emit-o-8VmdQX.mjs.map → contract-emit-DxEfEc-M.mjs.map} +1 -1
  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-pKkiCt7C.mjs → contract-infer-B5EhTqdR.mjs} +3 -4
  73. package/dist/{contract-infer-pKkiCt7C.mjs.map → contract-infer-B5EhTqdR.mjs.map} +1 -1
  74. package/dist/contract-space-aggregate-loader-lafgkTwG.mjs +247 -0
  75. package/dist/contract-space-aggregate-loader-lafgkTwG.mjs.map +1 -0
  76. package/dist/{db-verify-AoIUriL4.mjs → db-verify-B7fbkGnp.mjs} +5 -7
  77. package/dist/{db-verify-AoIUriL4.mjs.map → db-verify-B7fbkGnp.mjs.map} +1 -1
  78. package/dist/exports/control-api.d.mts +1 -1
  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-R_O3y5IW.mjs} +2 -2
  89. package/dist/{framework-components-65gOHkHB.mjs.map → framework-components-R_O3y5IW.mjs.map} +1 -1
  90. package/dist/global-flags-2SPgqWma.d.mts +34 -0
  91. package/dist/global-flags-2SPgqWma.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-Db5Itt5r.mjs → init-BQNpPozW.mjs} +4 -5
  95. package/dist/{init-Db5Itt5r.mjs.map → init-BQNpPozW.mjs.map} +1 -1
  96. package/dist/{inspect-live-schema-LeWvkZVz.mjs → inspect-live-schema-CQJnuPgD.mjs} +4 -5
  97. package/dist/{inspect-live-schema-LeWvkZVz.mjs.map → inspect-live-schema-CQJnuPgD.mjs.map} +1 -1
  98. package/dist/migration-check-CKfQlAWR.mjs +341 -0
  99. package/dist/migration-check-CKfQlAWR.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-BtkunvFQ.mjs → migration-command-scaffold-CE931d1-.mjs} +4 -5
  104. package/dist/{migration-command-scaffold-BtkunvFQ.mjs.map → migration-command-scaffold-CE931d1-.mjs.map} +1 -1
  105. package/dist/migration-list-A3bJ4j5e.mjs +780 -0
  106. package/dist/migration-list-A3bJ4j5e.mjs.map +1 -0
  107. package/dist/{migration-plan-C2jeH1J5.mjs → migration-plan-ZZm8C0s-.mjs} +372 -133
  108. package/dist/migration-plan-ZZm8C0s-.mjs.map +1 -0
  109. package/dist/{migration-types-BXWvz12q.d.mts → migration-types-q64xAI_J.d.mts} +1 -1
  110. package/dist/{migration-types-BXWvz12q.d.mts.map → migration-types-q64xAI_J.d.mts.map} +1 -1
  111. package/dist/{migrations-CwZMa1Ck.mjs → migrations-CjO1DsYe.mjs} +12 -13
  112. package/dist/migrations-CjO1DsYe.mjs.map +1 -0
  113. package/dist/{output-BlsrGMEF.mjs → output-DEg3SSnJ.mjs} +1 -1
  114. package/dist/{output-BlsrGMEF.mjs.map → output-DEg3SSnJ.mjs.map} +1 -1
  115. package/dist/{progress-adapter-DFfvZcYL.mjs → progress-adapter-C644QK8l.mjs} +1 -1
  116. package/dist/{progress-adapter-DFfvZcYL.mjs.map → progress-adapter-C644QK8l.mjs.map} +1 -1
  117. package/dist/ref-advancement-DUZqsue6.mjs +50 -0
  118. package/dist/ref-advancement-DUZqsue6.mjs.map +1 -0
  119. package/dist/terminal-ui-sLZt2cxc.d.mts +133 -0
  120. package/dist/terminal-ui-sLZt2cxc.d.mts.map +1 -0
  121. package/dist/{types-C9FfXb1l.d.mts → types-DK-ge7eR.d.mts} +5 -11
  122. package/dist/types-DK-ge7eR.d.mts.map +1 -0
  123. package/dist/{verify-Bom75OYI.mjs → verify-vl983Ed-.mjs} +2 -2
  124. package/dist/{verify-Bom75OYI.mjs.map → verify-vl983Ed-.mjs.map} +1 -1
  125. package/package.json +19 -19
  126. package/src/commands/db-init.ts +48 -2
  127. package/src/commands/db-sign.ts +9 -5
  128. package/src/commands/db-update.ts +54 -8
  129. package/src/commands/migrate.ts +123 -42
  130. package/src/commands/migration-check.ts +43 -83
  131. package/src/commands/migration-graph.ts +15 -41
  132. package/src/commands/migration-list.ts +231 -74
  133. package/src/commands/migration-log.ts +8 -14
  134. package/src/commands/migration-new.ts +44 -48
  135. package/src/commands/migration-plan.ts +411 -196
  136. package/src/commands/migration-show.ts +65 -284
  137. package/src/commands/migration-status.ts +116 -110
  138. package/src/commands/ref.ts +53 -8
  139. package/src/control-api/client.ts +0 -1
  140. package/src/control-api/contract-enrichment.ts +6 -42
  141. package/src/control-api/operations/contract-emit.ts +7 -2
  142. package/src/control-api/operations/db-verify.ts +9 -5
  143. package/src/control-api/operations/migration-apply.ts +36 -23
  144. package/src/control-api/types.ts +3 -10
  145. package/src/migration-cli.ts +4 -4
  146. package/src/utils/cli-errors.ts +234 -0
  147. package/src/utils/command-helpers.ts +1 -20
  148. package/src/utils/contract-at-errors.ts +96 -0
  149. package/src/utils/contract-space-aggregate-loader.ts +336 -117
  150. package/src/utils/formatters/migration-list-data-column.ts +115 -0
  151. package/src/utils/formatters/migration-list-graph-layout.ts +268 -0
  152. package/src/utils/formatters/migration-list-graph-render.ts +311 -0
  153. package/src/utils/formatters/migration-list-graph-topology.ts +158 -0
  154. package/src/utils/formatters/migration-list-render.ts +191 -0
  155. package/src/utils/formatters/migration-list-styler.ts +61 -0
  156. package/src/utils/formatters/migration-list-types.ts +21 -0
  157. package/src/utils/formatters/migrations.ts +29 -38
  158. package/src/utils/glyph-mode.ts +22 -0
  159. package/src/utils/integrity-violation-to-check-failure.ts +130 -0
  160. package/src/utils/plan-resolution.ts +258 -0
  161. package/src/utils/ref-advancement.ts +68 -0
  162. package/src/utils/terminal-ui.ts +42 -1
  163. package/dist/cli-errors-Czmx92Zy.d.mts +0 -3
  164. package/dist/cli-errors-Djtz98Vm.mjs +0 -71
  165. package/dist/cli-errors-Djtz98Vm.mjs.map +0 -1
  166. package/dist/client-oXO2WCPD.mjs.map +0 -1
  167. package/dist/command-helpers-DtavI0wJ.mjs.map +0 -1
  168. package/dist/commands/migration-check.mjs.map +0 -1
  169. package/dist/commands/migration-list.mjs.map +0 -1
  170. package/dist/contract-enrichment-Dani0mMW.mjs.map +0 -1
  171. package/dist/contract-space-aggregate-loader-BmNQwlws.mjs +0 -160
  172. package/dist/contract-space-aggregate-loader-BmNQwlws.mjs.map +0 -1
  173. package/dist/global-flags-CdE7M0d9.d.mts +0 -15
  174. package/dist/global-flags-CdE7M0d9.d.mts.map +0 -1
  175. package/dist/migration-plan-C2jeH1J5.mjs.map +0 -1
  176. package/dist/migrations-CwZMa1Ck.mjs.map +0 -1
  177. package/dist/rolldown-runtime-twds-ZHy.mjs +0 -14
  178. package/dist/terminal-ui-BiB_8KNo.mjs +0 -379
  179. package/dist/terminal-ui-BiB_8KNo.mjs.map +0 -1
  180. package/dist/types-C9FfXb1l.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,
@@ -433,46 +439,43 @@ function resolveDisplayChain(
433
439
  * cross-space totals.
434
440
  */
435
441
  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'];
442
+ readonly aggregate: ContractSpaceAggregate;
443
+ readonly extensionPacks: ReadonlyArray<unknown>;
441
444
  readonly markersBySpace: ReadonlyMap<string, ContractMarkerRecordLike> | null;
442
445
  }): 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.
446
+ const declaredExtensions = toDeclaredExtensionsFromRaw(args.extensionPacks);
447
+ if (
448
+ refuseContractSpaceIntegrity(args.aggregate, {
449
+ declaredExtensions,
450
+ checkContracts: true,
451
+ })
452
+ ) {
453
+ // Full integrity refusal (drift, layout violation, etc.) — surfacing
454
+ // it as a status diagnostic would duplicate `migration plan`'s job.
455
455
  // The single-space app pipeline still runs; extensions are simply
456
456
  // not enumerated.
457
457
  return [];
458
458
  }
459
- const aggregate = loaded.value;
459
+ const aggregate = args.aggregate;
460
460
 
461
461
  const orderedMembers = [...aggregate.extensions, aggregate.app];
462
462
  const rows: MigrationStatusSpaceEntry[] = [];
463
463
  for (const member of orderedMembers) {
464
464
  const liveMarker = args.markersBySpace?.get(member.spaceId) ?? null;
465
465
  const isApp = member.spaceId === aggregate.app.spaceId;
466
+ // The aggregate passed the integrity gate above, so every member has
467
+ // a resolved head ref (a missing one would have refused the load).
468
+ const headRef = requireHeadRef(member);
466
469
 
467
- if (member.migrations.graph.nodes.size === 0) {
470
+ if (member.graph().nodes.size === 0) {
468
471
  rows.push({
469
472
  spaceId: member.spaceId,
470
473
  kind: isApp ? 'app' : 'extension',
471
- headHash: member.headRef.hash,
474
+ headHash: headRef.hash,
472
475
  ...(args.markersBySpace !== null
473
476
  ? {
474
477
  markerHash: liveMarker?.storageHash ?? null,
475
- status: member.headRef.hash === EMPTY_CONTRACT_HASH ? 'up-to-date' : 'never-planned',
478
+ status: headRef.hash === EMPTY_CONTRACT_HASH ? 'up-to-date' : 'never-planned',
476
479
  pendingCount: 0,
477
480
  }
478
481
  : {}),
@@ -484,7 +487,7 @@ export async function loadAggregateStatusSpaces(args: {
484
487
  rows.push({
485
488
  spaceId: member.spaceId,
486
489
  kind: isApp ? 'app' : 'extension',
487
- headHash: member.headRef.hash,
490
+ headHash: headRef.hash,
488
491
  });
489
492
  continue;
490
493
  }
@@ -513,7 +516,7 @@ export async function loadAggregateStatusSpaces(args: {
513
516
  rows.push({
514
517
  spaceId: member.spaceId,
515
518
  kind: isApp ? 'app' : 'extension',
516
- headHash: member.headRef.hash,
519
+ headHash: headRef.hash,
517
520
  markerHash: liveMarker?.storageHash ?? null,
518
521
  pendingCount,
519
522
  ...(status ? { status } : {}),
@@ -528,17 +531,6 @@ export async function loadAggregateStatusSpaces(args: {
528
531
  * the existing `readContractEnvelope` path will report the same
529
532
  * problem via a status diagnostic, no need to double-surface.
530
533
  */
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
534
 
543
535
  async function validateOnlineMarkerRead(
544
536
  config: Awaited<ReturnType<typeof loadConfig>>,
@@ -580,8 +572,10 @@ async function executeMigrationStatusCommand(
580
572
  ui: TerminalUI,
581
573
  ): Promise<Result<MigrationStatusResult, CliStructuredError>> {
582
574
  const config = await loadConfig(options.config);
583
- const { configPath, appMigrationsDir, appMigrationsRelative, migrationsDir, refsDir } =
584
- resolveMigrationPaths(options.config, config);
575
+ const { configPath, appMigrationsRelative, migrationsDir, refsDir } = resolveMigrationPaths(
576
+ options.config,
577
+ config,
578
+ );
585
579
 
586
580
  const dbConnection = options.db ?? config.db?.connection;
587
581
  const hasDriver = !!config.driver;
@@ -599,37 +593,92 @@ async function executeMigrationStatusCommand(
599
593
  throw error;
600
594
  }
601
595
 
602
- let fromOverrideHash: string | undefined;
596
+ const diagnostics: StatusDiagnostic[] = [];
597
+ let contractHash: string = EMPTY_CONTRACT_HASH;
598
+ try {
599
+ const envelope = await readContractEnvelope(config);
600
+ contractHash = envelope.storageHash;
601
+ } catch (error) {
602
+ diagnostics.push({
603
+ code: 'CONTRACT.UNREADABLE',
604
+ severity: 'warn',
605
+ message: `Could not read contract: ${error instanceof Error ? error.message : 'unknown error'}`,
606
+ hints: ["Run 'prisma-next contract emit' to generate a valid contract"],
607
+ });
608
+ }
603
609
 
604
- if (options.to || options.from) {
610
+ const contractRawForAggregate = await loadContractRawSafely(config);
611
+ const stack = createControlStack(config);
612
+ const familyInstance = config.family.create(stack);
613
+ const deserializeContract = (json: unknown): Contract => familyInstance.deserializeContract(json);
614
+ const appContractStandIn = appContractStandInFromIdentity({
615
+ contractHash,
616
+ targetId: config.target.id,
617
+ targetFamily: config.target.familyId,
618
+ });
619
+ let appContractForLoad: Contract = appContractStandIn;
620
+ if (contractRawForAggregate !== null) {
605
621
  try {
606
- const { graph: earlyGraph } = await loadMigrationPackages(appMigrationsDir);
622
+ appContractForLoad = deserializeContract(contractRawForAggregate);
623
+ } catch (error) {
624
+ diagnostics.push({
625
+ code: 'CONTRACT.UNREADABLE',
626
+ severity: 'warn',
627
+ message: `Could not deserialize contract: ${error instanceof Error ? error.message : 'unknown error'}`,
628
+ hints: ["Run 'prisma-next contract emit' to generate a valid contract"],
629
+ });
630
+ }
631
+ }
607
632
 
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
- }
633
+ let aggregate: ContractSpaceAggregate;
634
+ try {
635
+ aggregate = await loadContractSpaceAggregate({
636
+ migrationsDir,
637
+ deserializeContract,
638
+ appContract: appContractForLoad,
639
+ });
640
+ } catch (error) {
641
+ if (MigrationToolsError.is(error)) {
642
+ return notOk(mapMigrationToolsError(error));
643
+ }
644
+ return notOk(
645
+ errorUnexpected(error instanceof Error ? error.message : String(error), {
646
+ why: `Failed to read migrations directory: ${error instanceof Error ? error.message : String(error)}`,
647
+ }),
648
+ );
649
+ }
620
650
 
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;
651
+ if (contractRawForAggregate !== null) {
652
+ const corruptionFailure = refusePackageCorruptionOnAggregate(aggregate);
653
+ if (corruptionFailure) {
654
+ return notOk(corruptionFailure);
655
+ }
656
+ }
657
+
658
+ const appGraph = aggregate.app.graph();
659
+
660
+ let fromOverrideHash: string | undefined;
661
+
662
+ if (options.to || options.from) {
663
+ if (options.to) {
664
+ const refResult = parseContractRef(options.to, { graph: appGraph, refs: allRefs });
665
+ if (!refResult.ok) {
666
+ return notOk(mapRefResolutionError(refResult.failure));
627
667
  }
628
- } catch (error) {
629
- if (MigrationToolsError.is(error)) {
630
- return notOk(mapMigrationToolsError(error));
668
+ activeRefHash = refResult.value.hash;
669
+ if (refResult.value.provenance.kind === 'ref') {
670
+ const resolvedRefName = refResult.value.provenance.refName;
671
+ activeRefName = resolvedRefName;
672
+ activeRefEntry = allRefs[resolvedRefName];
631
673
  }
632
- throw error;
674
+ }
675
+
676
+ if (options.from) {
677
+ const fromResult = parseContractRef(options.from, { graph: appGraph, refs: allRefs });
678
+ if (!fromResult.ok) {
679
+ return notOk(mapRefResolutionError(fromResult.failure));
680
+ }
681
+ fromOverrideHash = fromResult.value.hash;
633
682
  }
634
683
  }
635
684
 
@@ -670,34 +719,8 @@ async function executeMigrationStatusCommand(
670
719
  ui.stderr(header);
671
720
  }
672
721
 
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
- }
722
+ const bundles = aggregate.app.packages;
723
+ const graph = appGraph;
701
724
 
702
725
  if (bundles.length === 0) {
703
726
  if (dbConnection && hasDriver) {
@@ -808,32 +831,15 @@ async function executeMigrationStatusCommand(
808
831
  allMarkers = null;
809
832
  }
810
833
 
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
834
  let aggregateSpaces: readonly MigrationStatusSpaceEntry[] = [];
818
835
  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
836
  try {
826
837
  aggregateSpaces = await loadAggregateStatusSpaces({
827
- targetId: config.target.targetId,
828
- migrationsDir,
829
- appContractRaw: contractRawForAggregate,
838
+ aggregate,
830
839
  extensionPacks: config.extensionPacks ?? [],
831
- deserializeContract: (json: unknown) => familyInstance.deserializeContract(json),
832
840
  markersBySpace: allMarkers,
833
841
  });
834
842
  } catch {
835
- // Loader failure short-circuits silently — the existing
836
- // single-space app pipeline below still runs.
837
843
  aggregateSpaces = [];
838
844
  }
839
845
  }
@@ -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
  }
@@ -253,13 +253,18 @@ export async function executeContractEmit(
253
253
  // on-disk JSON envelope is constructed by target-owned code
254
254
  // rather than by walking the in-memory contract with
255
255
  // `Object.entries` (which would leak runtime-only class API
256
- // fields into the persisted shape).
256
+ // fields into the persisted shape). The optional `shouldPreserveEmpty`
257
+ // and `sortStorage` hooks let the family contribute storage-specific
258
+ // canonicalization rules without the framework importing family code.
259
+ const { contractSerializer } = config.target;
257
260
  const serializeContract = (c: Contract): JsonObject =>
258
- config.target.contractSerializer.serializeContract(c);
261
+ contractSerializer.serializeContract(c);
259
262
  emitResult = await unlessAborted(
260
263
  emit(enrichedIR, stack, config.family.emission, {
261
264
  outputJsonPath,
262
265
  serializeContract,
266
+ ...ifDefined('shouldPreserveEmpty', contractSerializer.shouldPreserveEmpty),
267
+ ...ifDefined('sortStorage', contractSerializer.sortStorage),
263
268
  }),
264
269
  );
265
270
  } catch (error) {
@@ -9,8 +9,10 @@ import type {
9
9
  import {
10
10
  type AggregateVerifierOutput,
11
11
  type ContractSpaceMember,
12
+ requireHeadRef,
12
13
  verifyAggregate,
13
14
  } from '@prisma-next/migration-tools/aggregate';
15
+ import { castAs } from '@prisma-next/utils/casts';
14
16
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
15
17
  import { CliStructuredError } from '../../utils/cli-errors';
16
18
  import {
@@ -162,7 +164,7 @@ async function runIntrospection<TFamilyId extends string, TTargetId extends stri
162
164
  * `ok` result so the verifier still runs the (cheap) schemaCheck loop
163
165
  * without invoking the family's verification path.
164
166
  */
165
- function createPerMemberVerifier<TFamilyId extends string, TTargetId extends string>(
167
+ export function createPerMemberVerifier<TFamilyId extends string, TTargetId extends string>(
166
168
  options: ExecuteDbVerifyOptions<TFamilyId, TTargetId>,
167
169
  ): (
168
170
  projectedSchema: unknown,
@@ -173,7 +175,7 @@ function createPerMemberVerifier<TFamilyId extends string, TTargetId extends str
173
175
  return (projectedSchema, member, verifyMode) => {
174
176
  if (skipSchema) return buildSkippedSchemaResult(member);
175
177
  return familyInstance.verifySchema({
176
- contract: member.contract,
178
+ contract: member.contract(),
177
179
  // The family's `TSchemaIR` is opaque to migration-tools; the
178
180
  // aggregate verifier passes through whatever we hand it. The
179
181
  // family expects its own IR shape on the way back.
@@ -247,15 +249,17 @@ function finaliseVerifyResult(args: {
247
249
  }
248
250
 
249
251
  function buildSkippedSchemaResult(member: ContractSpaceMember): VerifyDatabaseSchemaResult {
250
- const profileHash = (member.contract as { profileHash?: string }).profileHash;
252
+ const contract = member.contract();
253
+ const headRef = requireHeadRef(member);
254
+ const profileHash = castAs<{ profileHash?: string }>(contract).profileHash;
251
255
  return {
252
256
  ok: true,
253
257
  summary: 'Schema verification skipped',
254
258
  contract: {
255
- storageHash: member.headRef.hash,
259
+ storageHash: headRef.hash,
256
260
  ...(profileHash ? { profileHash } : {}),
257
261
  },
258
- target: { expected: member.contract.target },
262
+ target: { expected: contract.target },
259
263
  schema: {
260
264
  issues: [],
261
265
  root: {