@prisma-next/cli 0.11.0-dev.5 → 0.11.0-dev.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/dist/cli-errors-DFF1LlfU.mjs +215 -0
  2. package/dist/cli-errors-DFF1LlfU.mjs.map +1 -0
  3. package/dist/cli.mjs +9 -10
  4. package/dist/cli.mjs.map +1 -1
  5. package/dist/{client-oXO2WCPD.mjs → client-5uvDppD8.mjs} +23 -21
  6. package/dist/client-5uvDppD8.mjs.map +1 -0
  7. package/dist/{command-helpers-DtavI0wJ.mjs → command-helpers-4UNsRRc4.mjs} +427 -9
  8. package/dist/command-helpers-4UNsRRc4.mjs.map +1 -0
  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.map +1 -1
  12. package/dist/commands/contract-infer.mjs +1 -1
  13. package/dist/commands/db-init.d.mts.map +1 -1
  14. package/dist/commands/db-init.mjs +33 -7
  15. package/dist/commands/db-init.mjs.map +1 -1
  16. package/dist/commands/db-schema.d.mts.map +1 -1
  17. package/dist/commands/db-schema.mjs +3 -4
  18. package/dist/commands/db-schema.mjs.map +1 -1
  19. package/dist/commands/db-sign.d.mts.map +1 -1
  20. package/dist/commands/db-sign.mjs +6 -7
  21. package/dist/commands/db-sign.mjs.map +1 -1
  22. package/dist/commands/db-update.d.mts.map +1 -1
  23. package/dist/commands/db-update.mjs +36 -8
  24. package/dist/commands/db-update.mjs.map +1 -1
  25. package/dist/commands/db-verify.d.mts.map +1 -1
  26. package/dist/commands/db-verify.mjs +1 -1
  27. package/dist/commands/migrate.d.mts +5 -1
  28. package/dist/commands/migrate.d.mts.map +1 -1
  29. package/dist/commands/migrate.mjs +79 -39
  30. package/dist/commands/migrate.mjs.map +1 -1
  31. package/dist/commands/migration-check.d.mts +4 -3
  32. package/dist/commands/migration-check.d.mts.map +1 -1
  33. package/dist/commands/migration-check.mjs +1 -280
  34. package/dist/commands/migration-graph.d.mts +1 -1
  35. package/dist/commands/migration-graph.d.mts.map +1 -1
  36. package/dist/commands/migration-graph.mjs +3 -4
  37. package/dist/commands/migration-graph.mjs.map +1 -1
  38. package/dist/commands/migration-list.d.mts +63 -12
  39. package/dist/commands/migration-list.d.mts.map +1 -1
  40. package/dist/commands/migration-list.mjs +2 -103
  41. package/dist/commands/migration-log.d.mts.map +1 -1
  42. package/dist/commands/migration-log.mjs +3 -4
  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 +33 -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 +62 -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 +93 -67
  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 +34 -9
  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-emit-o-8VmdQX.mjs → contract-emit-C-CFGZsI.mjs} +9 -6
  65. package/dist/{contract-emit-o-8VmdQX.mjs.map → contract-emit-C-CFGZsI.mjs.map} +1 -1
  66. package/dist/{contract-emit-CmsklifJ.mjs → contract-emit-CuUzzM46.mjs} +5 -6
  67. package/dist/{contract-emit-CmsklifJ.mjs.map → contract-emit-CuUzzM46.mjs.map} +1 -1
  68. package/dist/{contract-enrichment-Dani0mMW.mjs → contract-enrichment-XmUPhmsS.mjs} +4 -25
  69. package/dist/contract-enrichment-XmUPhmsS.mjs.map +1 -0
  70. package/dist/{contract-infer-pKkiCt7C.mjs → contract-infer-C98ZaRhp.mjs} +3 -4
  71. package/dist/{contract-infer-pKkiCt7C.mjs.map → contract-infer-C98ZaRhp.mjs.map} +1 -1
  72. package/dist/contract-space-aggregate-loader-CVHGuA35.mjs +170 -0
  73. package/dist/contract-space-aggregate-loader-CVHGuA35.mjs.map +1 -0
  74. package/dist/{db-verify-AoIUriL4.mjs → db-verify-BWl1Yxi-.mjs} +6 -7
  75. package/dist/{db-verify-AoIUriL4.mjs.map → db-verify-BWl1Yxi-.mjs.map} +1 -1
  76. package/dist/exports/control-api.d.mts +1 -1
  77. package/dist/exports/control-api.d.mts.map +1 -1
  78. package/dist/exports/control-api.mjs +3 -3
  79. package/dist/exports/index.d.mts.map +1 -1
  80. package/dist/exports/index.mjs +1 -1
  81. package/dist/exports/index.mjs.map +1 -1
  82. package/dist/exports/init-output.d.mts.map +1 -1
  83. package/dist/exports/init-output.mjs +1 -1
  84. package/dist/extension-pack-inputs-BiY86HbQ.mjs +62 -0
  85. package/dist/extension-pack-inputs-BiY86HbQ.mjs.map +1 -0
  86. package/dist/{framework-components-65gOHkHB.mjs → framework-components-DTcjouhS.mjs} +2 -2
  87. package/dist/{framework-components-65gOHkHB.mjs.map → framework-components-DTcjouhS.mjs.map} +1 -1
  88. package/dist/{global-flags-CdE7M0d9.d.mts → global-flags-DWsQ6SSI.d.mts} +1 -1
  89. package/dist/global-flags-DWsQ6SSI.d.mts.map +1 -0
  90. package/dist/glyph-mode-CBB4emzO.d.mts +5 -0
  91. package/dist/glyph-mode-CBB4emzO.d.mts.map +1 -0
  92. package/dist/{graph-render-DJVv0_uf.mjs → graph-render-D2FnLpuK.mjs} +1 -1
  93. package/dist/{graph-render-DJVv0_uf.mjs.map → graph-render-D2FnLpuK.mjs.map} +1 -1
  94. package/dist/{init-Db5Itt5r.mjs → init-C7PvN163.mjs} +5 -5
  95. package/dist/{init-Db5Itt5r.mjs.map → init-C7PvN163.mjs.map} +1 -1
  96. package/dist/{inspect-live-schema-LeWvkZVz.mjs → inspect-live-schema-BRCWQ-Sr.mjs} +5 -5
  97. package/dist/{inspect-live-schema-LeWvkZVz.mjs.map → inspect-live-schema-BRCWQ-Sr.mjs.map} +1 -1
  98. package/dist/migration-check-DoskM1nB.mjs +341 -0
  99. package/dist/migration-check-DoskM1nB.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-CXLkoIJx.mjs} +5 -5
  104. package/dist/{migration-command-scaffold-BtkunvFQ.mjs.map → migration-command-scaffold-CXLkoIJx.mjs.map} +1 -1
  105. package/dist/migration-list-B2-iQ5Jd.mjs +646 -0
  106. package/dist/migration-list-B2-iQ5Jd.mjs.map +1 -0
  107. package/dist/{migration-plan-C2jeH1J5.mjs → migration-plan-BqmIKQpZ.mjs} +341 -88
  108. package/dist/migration-plan-BqmIKQpZ.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-BcVTutso.mjs} +12 -13
  112. package/dist/migrations-BcVTutso.mjs.map +1 -0
  113. package/dist/{output-BlsrGMEF.mjs → output-B60Gw5fu.mjs} +1 -1
  114. package/dist/{output-BlsrGMEF.mjs.map → output-B60Gw5fu.mjs.map} +1 -1
  115. package/dist/{progress-adapter-DFfvZcYL.mjs → progress-adapter-xASh41wr.mjs} +1 -1
  116. package/dist/{progress-adapter-DFfvZcYL.mjs.map → progress-adapter-xASh41wr.mjs.map} +1 -1
  117. package/dist/ref-advancement-DRh5Nquq.mjs +50 -0
  118. package/dist/ref-advancement-DRh5Nquq.mjs.map +1 -0
  119. package/dist/{types-C9FfXb1l.d.mts → types-CEtm6v6a.d.mts} +5 -11
  120. package/dist/types-CEtm6v6a.d.mts.map +1 -0
  121. package/dist/{verify-Bom75OYI.mjs → verify-DOHbbrub.mjs} +2 -2
  122. package/dist/{verify-Bom75OYI.mjs.map → verify-DOHbbrub.mjs.map} +1 -1
  123. package/package.json +20 -20
  124. package/src/commands/db-init.ts +48 -2
  125. package/src/commands/db-update.ts +45 -0
  126. package/src/commands/migrate.ts +120 -40
  127. package/src/commands/migration-check.ts +43 -83
  128. package/src/commands/migration-list.ts +173 -74
  129. package/src/commands/migration-new.ts +44 -48
  130. package/src/commands/migration-plan.ts +359 -128
  131. package/src/commands/migration-show.ts +65 -284
  132. package/src/commands/migration-status.ts +131 -99
  133. package/src/commands/ref.ts +46 -6
  134. package/src/control-api/client.ts +0 -1
  135. package/src/control-api/contract-enrichment.ts +6 -42
  136. package/src/control-api/operations/contract-emit.ts +7 -2
  137. package/src/control-api/operations/db-verify.ts +9 -5
  138. package/src/control-api/operations/migration-apply.ts +11 -19
  139. package/src/control-api/types.ts +0 -7
  140. package/src/migration-cli.ts +4 -4
  141. package/src/utils/cli-errors.ts +224 -0
  142. package/src/utils/command-helpers.ts +9 -4
  143. package/src/utils/contract-space-aggregate-loader.ts +221 -117
  144. package/src/utils/formatters/migration-list-data-column.ts +115 -0
  145. package/src/utils/formatters/migration-list-graph-layout.ts +268 -0
  146. package/src/utils/formatters/migration-list-graph-render.ts +314 -0
  147. package/src/utils/formatters/migration-list-render.ts +194 -0
  148. package/src/utils/formatters/migration-list-styler.ts +61 -0
  149. package/src/utils/formatters/migrations.ts +29 -38
  150. package/src/utils/glyph-mode.ts +22 -0
  151. package/src/utils/integrity-violation-to-check-failure.ts +130 -0
  152. package/src/utils/plan-resolution.ts +257 -0
  153. package/src/utils/ref-advancement.ts +68 -0
  154. package/src/utils/terminal-ui.ts +42 -1
  155. package/dist/cli-errors-Czmx92Zy.d.mts +0 -3
  156. package/dist/cli-errors-Djtz98Vm.mjs +0 -71
  157. package/dist/cli-errors-Djtz98Vm.mjs.map +0 -1
  158. package/dist/client-oXO2WCPD.mjs.map +0 -1
  159. package/dist/command-helpers-DtavI0wJ.mjs.map +0 -1
  160. package/dist/commands/migration-check.mjs.map +0 -1
  161. package/dist/commands/migration-list.mjs.map +0 -1
  162. package/dist/contract-enrichment-Dani0mMW.mjs.map +0 -1
  163. package/dist/contract-space-aggregate-loader-BmNQwlws.mjs +0 -160
  164. package/dist/contract-space-aggregate-loader-BmNQwlws.mjs.map +0 -1
  165. package/dist/global-flags-CdE7M0d9.d.mts.map +0 -1
  166. package/dist/migration-plan-C2jeH1J5.mjs.map +0 -1
  167. package/dist/migrations-CwZMa1Ck.mjs.map +0 -1
  168. package/dist/terminal-ui-BiB_8KNo.mjs +0 -379
  169. package/dist/terminal-ui-BiB_8KNo.mjs.map +0 -1
  170. 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 {
@@ -22,6 +26,7 @@ import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/packag
22
26
  import { parseContractRef } from '@prisma-next/migration-tools/ref-resolution';
23
27
  import type { RefEntry, Refs } from '@prisma-next/migration-tools/refs';
24
28
  import { readRefs } from '@prisma-next/migration-tools/refs';
29
+ import { blindCast } from '@prisma-next/utils/casts';
25
30
  import { ifDefined } from '@prisma-next/utils/defined';
26
31
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
27
32
  import { cyan, dim, magenta, yellow } from 'colorette';
@@ -39,7 +44,6 @@ import {
39
44
  import {
40
45
  addGlobalOptions,
41
46
  collectDeclaredInvariants,
42
- loadMigrationPackages,
43
47
  maskConnectionUrl,
44
48
  readContractEnvelope,
45
49
  resolveMigrationPaths,
@@ -50,9 +54,10 @@ import {
50
54
  toStructuralEdge,
51
55
  } from '../utils/command-helpers';
52
56
  import {
53
- type BuildAggregateInputs,
54
- buildContractSpaceAggregate,
57
+ refuseContractSpaceIntegrity,
58
+ refusePackageCorruptionOnAggregate,
55
59
  } from '../utils/contract-space-aggregate-loader';
60
+ import { toDeclaredExtensionsFromRaw } from '../utils/extension-pack-inputs';
56
61
  import {
57
62
  type EdgeStatus,
58
63
  type EdgeStatusKind,
@@ -433,46 +438,43 @@ function resolveDisplayChain(
433
438
  * cross-space totals.
434
439
  */
435
440
  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'];
441
+ readonly aggregate: ContractSpaceAggregate;
442
+ readonly extensionPacks: ReadonlyArray<unknown>;
441
443
  readonly markersBySpace: ReadonlyMap<string, ContractMarkerRecordLike> | null;
442
444
  }): 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.
445
+ const declaredExtensions = toDeclaredExtensionsFromRaw(args.extensionPacks);
446
+ if (
447
+ refuseContractSpaceIntegrity(args.aggregate, {
448
+ declaredExtensions,
449
+ checkContracts: true,
450
+ })
451
+ ) {
452
+ // Full integrity refusal (drift, layout violation, etc.) — surfacing
453
+ // it as a status diagnostic would duplicate `migration plan`'s job.
455
454
  // The single-space app pipeline still runs; extensions are simply
456
455
  // not enumerated.
457
456
  return [];
458
457
  }
459
- const aggregate = loaded.value;
458
+ const aggregate = args.aggregate;
460
459
 
461
460
  const orderedMembers = [...aggregate.extensions, aggregate.app];
462
461
  const rows: MigrationStatusSpaceEntry[] = [];
463
462
  for (const member of orderedMembers) {
464
463
  const liveMarker = args.markersBySpace?.get(member.spaceId) ?? null;
465
464
  const isApp = member.spaceId === aggregate.app.spaceId;
465
+ // The aggregate passed the integrity gate above, so every member has
466
+ // a resolved head ref (a missing one would have refused the load).
467
+ const headRef = requireHeadRef(member);
466
468
 
467
- if (member.migrations.graph.nodes.size === 0) {
469
+ if (member.graph().nodes.size === 0) {
468
470
  rows.push({
469
471
  spaceId: member.spaceId,
470
472
  kind: isApp ? 'app' : 'extension',
471
- headHash: member.headRef.hash,
473
+ headHash: headRef.hash,
472
474
  ...(args.markersBySpace !== null
473
475
  ? {
474
476
  markerHash: liveMarker?.storageHash ?? null,
475
- status: member.headRef.hash === EMPTY_CONTRACT_HASH ? 'up-to-date' : 'never-planned',
477
+ status: headRef.hash === EMPTY_CONTRACT_HASH ? 'up-to-date' : 'never-planned',
476
478
  pendingCount: 0,
477
479
  }
478
480
  : {}),
@@ -484,7 +486,7 @@ export async function loadAggregateStatusSpaces(args: {
484
486
  rows.push({
485
487
  spaceId: member.spaceId,
486
488
  kind: isApp ? 'app' : 'extension',
487
- headHash: member.headRef.hash,
489
+ headHash: headRef.hash,
488
490
  });
489
491
  continue;
490
492
  }
@@ -513,7 +515,7 @@ export async function loadAggregateStatusSpaces(args: {
513
515
  rows.push({
514
516
  spaceId: member.spaceId,
515
517
  kind: isApp ? 'app' : 'extension',
516
- headHash: member.headRef.hash,
518
+ headHash: headRef.hash,
517
519
  markerHash: liveMarker?.storageHash ?? null,
518
520
  pendingCount,
519
521
  ...(status ? { status } : {}),
@@ -528,6 +530,22 @@ export async function loadAggregateStatusSpaces(args: {
528
530
  * the existing `readContractEnvelope` path will report the same
529
531
  * problem via a status diagnostic, no need to double-surface.
530
532
  */
533
+
534
+ function appContractShellForAggregateLoad(args: {
535
+ readonly contractHash: string;
536
+ readonly targetId: string;
537
+ readonly targetFamily: string;
538
+ }): Contract {
539
+ return blindCast<Contract, 'status aggregate load without contract.json'>({
540
+ storage: { storageHash: args.contractHash },
541
+ schemaVersion: '0.0.0',
542
+ target: args.targetId,
543
+ targetFamily: args.targetFamily,
544
+ models: {},
545
+ profileHash: EMPTY_CONTRACT_HASH,
546
+ });
547
+ }
548
+
531
549
  async function loadContractRawSafely(config: {
532
550
  contract?: { output?: string };
533
551
  }): Promise<unknown | null> {
@@ -580,8 +598,10 @@ async function executeMigrationStatusCommand(
580
598
  ui: TerminalUI,
581
599
  ): Promise<Result<MigrationStatusResult, CliStructuredError>> {
582
600
  const config = await loadConfig(options.config);
583
- const { configPath, appMigrationsDir, appMigrationsRelative, migrationsDir, refsDir } =
584
- resolveMigrationPaths(options.config, config);
601
+ const { configPath, appMigrationsRelative, migrationsDir, refsDir } = resolveMigrationPaths(
602
+ options.config,
603
+ config,
604
+ );
585
605
 
586
606
  const dbConnection = options.db ?? config.db?.connection;
587
607
  const hasDriver = !!config.driver;
@@ -599,37 +619,92 @@ async function executeMigrationStatusCommand(
599
619
  throw error;
600
620
  }
601
621
 
602
- let fromOverrideHash: string | undefined;
622
+ const diagnostics: StatusDiagnostic[] = [];
623
+ let contractHash: string = EMPTY_CONTRACT_HASH;
624
+ try {
625
+ const envelope = await readContractEnvelope(config);
626
+ contractHash = envelope.storageHash;
627
+ } catch (error) {
628
+ diagnostics.push({
629
+ code: 'CONTRACT.UNREADABLE',
630
+ severity: 'warn',
631
+ message: `Could not read contract: ${error instanceof Error ? error.message : 'unknown error'}`,
632
+ hints: ["Run 'prisma-next contract emit' to generate a valid contract"],
633
+ });
634
+ }
603
635
 
604
- if (options.to || options.from) {
636
+ const contractRawForAggregate = await loadContractRawSafely(config);
637
+ const stack = createControlStack(config);
638
+ const familyInstance = config.family.create(stack);
639
+ const deserializeContract = (json: unknown): Contract => familyInstance.deserializeContract(json);
640
+ const appContractShell = appContractShellForAggregateLoad({
641
+ contractHash,
642
+ targetId: config.target.id,
643
+ targetFamily: config.target.familyId,
644
+ });
645
+ let appContractForLoad: Contract = appContractShell;
646
+ if (contractRawForAggregate !== null) {
605
647
  try {
606
- const { graph: earlyGraph } = await loadMigrationPackages(appMigrationsDir);
648
+ appContractForLoad = deserializeContract(contractRawForAggregate);
649
+ } catch (error) {
650
+ diagnostics.push({
651
+ code: 'CONTRACT.UNREADABLE',
652
+ severity: 'warn',
653
+ message: `Could not deserialize contract: ${error instanceof Error ? error.message : 'unknown error'}`,
654
+ hints: ["Run 'prisma-next contract emit' to generate a valid contract"],
655
+ });
656
+ }
657
+ }
607
658
 
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
- }
659
+ let aggregate: ContractSpaceAggregate;
660
+ try {
661
+ aggregate = await loadContractSpaceAggregate({
662
+ migrationsDir,
663
+ deserializeContract,
664
+ appContract: appContractForLoad,
665
+ });
666
+ } catch (error) {
667
+ if (MigrationToolsError.is(error)) {
668
+ return notOk(mapMigrationToolsError(error));
669
+ }
670
+ return notOk(
671
+ errorUnexpected(error instanceof Error ? error.message : String(error), {
672
+ why: `Failed to read migrations directory: ${error instanceof Error ? error.message : String(error)}`,
673
+ }),
674
+ );
675
+ }
620
676
 
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;
677
+ if (contractRawForAggregate !== null) {
678
+ const corruptionFailure = refusePackageCorruptionOnAggregate(aggregate);
679
+ if (corruptionFailure) {
680
+ return notOk(corruptionFailure);
681
+ }
682
+ }
683
+
684
+ const appGraph = aggregate.app.graph();
685
+
686
+ let fromOverrideHash: string | undefined;
687
+
688
+ if (options.to || options.from) {
689
+ if (options.to) {
690
+ const refResult = parseContractRef(options.to, { graph: appGraph, refs: allRefs });
691
+ if (!refResult.ok) {
692
+ return notOk(mapRefResolutionError(refResult.failure));
627
693
  }
628
- } catch (error) {
629
- if (MigrationToolsError.is(error)) {
630
- return notOk(mapMigrationToolsError(error));
694
+ activeRefHash = refResult.value.hash;
695
+ if (refResult.value.provenance.kind === 'ref') {
696
+ const resolvedRefName = refResult.value.provenance.refName;
697
+ activeRefName = resolvedRefName;
698
+ activeRefEntry = allRefs[resolvedRefName];
631
699
  }
632
- throw error;
700
+ }
701
+
702
+ if (options.from) {
703
+ const fromResult = parseContractRef(options.from, { graph: appGraph, refs: allRefs });
704
+ if (!fromResult.ok) {
705
+ return notOk(mapRefResolutionError(fromResult.failure));
706
+ }
707
+ fromOverrideHash = fromResult.value.hash;
633
708
  }
634
709
  }
635
710
 
@@ -670,34 +745,8 @@ async function executeMigrationStatusCommand(
670
745
  ui.stderr(header);
671
746
  }
672
747
 
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
- }
748
+ const bundles = aggregate.app.packages;
749
+ const graph = appGraph;
701
750
 
702
751
  if (bundles.length === 0) {
703
752
  if (dbConnection && hasDriver) {
@@ -808,32 +857,15 @@ async function executeMigrationStatusCommand(
808
857
  allMarkers = null;
809
858
  }
810
859
 
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
860
  let aggregateSpaces: readonly MigrationStatusSpaceEntry[] = [];
818
861
  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
862
  try {
826
863
  aggregateSpaces = await loadAggregateStatusSpaces({
827
- targetId: config.target.targetId,
828
- migrationsDir,
829
- appContractRaw: contractRawForAggregate,
864
+ aggregate,
830
865
  extensionPacks: config.extensionPacks ?? [],
831
- deserializeContract: (json: unknown) => familyInstance.deserializeContract(json),
832
866
  markersBySpace: allMarkers,
833
867
  });
834
868
  } catch {
835
- // Loader failure short-circuits silently — the existing
836
- // single-space app pipeline below still runs.
837
869
  aggregateSpaces = [];
838
870
  }
839
871
  }
@@ -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,
@@ -26,6 +34,7 @@ import {
26
34
  } from '../utils/command-helpers';
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
 
@@ -73,13 +82,13 @@ export async function executeRefSetCommand(
73
82
  try {
74
83
  const config = await loadConfig(options.config);
75
84
  const { appMigrationsDir, refsDir } = resolveMigrationPaths(options.config, config);
85
+ const { graph, bundles } = await loadMigrationPackages(appMigrationsDir);
86
+ const refs = await readRefs(refsDir);
76
87
 
77
88
  let resolvedHash: string;
78
89
  if (validateRefValue(contractInput)) {
79
90
  resolvedHash = contractInput;
80
91
  } else {
81
- const { graph } = await loadMigrationPackages(appMigrationsDir);
82
- const refs = await readRefs(refsDir);
83
92
  const refResult = parseContractRef(contractInput, { graph, refs });
84
93
  if (!refResult.ok) {
85
94
  return notOk(mapRefResolutionError(refResult.failure));
@@ -87,8 +96,39 @@ export async function executeRefSetCommand(
87
96
  resolvedHash = refResult.value.hash;
88
97
  }
89
98
 
99
+ if (resolvedHash === EMPTY_CONTRACT_HASH) {
100
+ return notOk(errorRefSetEmptySentinel(resolvedHash));
101
+ }
102
+ if (!isGraphNode(resolvedHash, graph)) {
103
+ const graphTip = findLatestMigration(graph)?.to ?? null;
104
+ return notOk(errorRefSetHashNotInGraph(resolvedHash, [...graph.nodes].sort(), graphTip));
105
+ }
106
+
107
+ const matchingBundle = bundles.find((bundle) => bundle.metadata.to === resolvedHash);
108
+ if (!matchingBundle) {
109
+ return notOk(errorRefSetBundleNotFound(resolvedHash));
110
+ }
111
+
112
+ const contractJsonPath = join(matchingBundle.dirPath, 'end-contract.json');
113
+ let contractJson: Record<string, unknown>;
114
+ try {
115
+ const raw = await readFile(contractJsonPath, 'utf-8');
116
+ contractJson = JSON.parse(raw) as Record<string, unknown>;
117
+ } catch (readError) {
118
+ if (readError instanceof Error && (readError as NodeJS.ErrnoException).code === 'ENOENT') {
119
+ return notOk(
120
+ errorFileNotFound(contractJsonPath, {
121
+ why: `Migration bundle for hash ${resolvedHash} is missing its end-contract snapshot at ${contractJsonPath}`,
122
+ fix: 'Run `pnpm fixtures:check`, or re-emit the migration so its end-contract.json is restored.',
123
+ }),
124
+ );
125
+ }
126
+ throw readError;
127
+ }
128
+
129
+ const contractIR = await readContractIR(contractJson, contractJsonPath);
90
130
  const entry: RefEntry = { hash: resolvedHash, invariants: [] };
91
- await writeRef(refsDir, name, entry);
131
+ await writeRefPaired(refsDir, name, entry, contractIR);
92
132
  return ok({ ok: true as const, ref: name, hash: resolvedHash, invariants: [] });
93
133
  } catch (error) {
94
134
  if (error instanceof CliStructuredError) return notOk(error);
@@ -103,7 +143,7 @@ export async function executeRefDeleteCommand(
103
143
  try {
104
144
  const config = await loadConfig(options.config);
105
145
  const { refsDir } = resolveMigrationPaths(options.config, config);
106
- await deleteRef(refsDir, name);
146
+ await deleteRefPaired(refsDir, name);
107
147
  return ok({ ok: true as const, ref: name, deleted: true as const });
108
148
  } catch (error) {
109
149
  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: {