@prisma-next/cli 0.11.0-dev.46 → 0.11.0-dev.48

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 (105) hide show
  1. package/dist/cli.mjs +9 -9
  2. package/dist/{client-a5NJce0-.mjs → client-5uvDppD8.mjs} +20 -18
  3. package/dist/client-5uvDppD8.mjs.map +1 -0
  4. package/dist/{command-helpers-yLuA78TP.mjs → command-helpers-CI8P5Xyd.mjs} +4 -4
  5. package/dist/{command-helpers-yLuA78TP.mjs.map → command-helpers-CI8P5Xyd.mjs.map} +1 -1
  6. package/dist/commands/contract-emit.mjs +1 -1
  7. package/dist/commands/contract-infer.mjs +1 -1
  8. package/dist/commands/db-init.mjs +5 -5
  9. package/dist/commands/db-schema.mjs +3 -3
  10. package/dist/commands/db-sign.mjs +3 -3
  11. package/dist/commands/db-update.mjs +5 -5
  12. package/dist/commands/db-verify.mjs +1 -1
  13. package/dist/commands/migrate.d.mts +1 -1
  14. package/dist/commands/migrate.d.mts.map +1 -1
  15. package/dist/commands/migrate.mjs +42 -37
  16. package/dist/commands/migrate.mjs.map +1 -1
  17. package/dist/commands/migration-check.d.mts +4 -3
  18. package/dist/commands/migration-check.d.mts.map +1 -1
  19. package/dist/commands/migration-check.mjs +1 -279
  20. package/dist/commands/migration-graph.d.mts +1 -1
  21. package/dist/commands/migration-graph.mjs +2 -2
  22. package/dist/commands/migration-list.d.mts +2 -2
  23. package/dist/commands/migration-list.mjs +1 -1
  24. package/dist/commands/migration-log.mjs +2 -2
  25. package/dist/commands/migration-new.d.mts.map +1 -1
  26. package/dist/commands/migration-new.mjs +32 -30
  27. package/dist/commands/migration-new.mjs.map +1 -1
  28. package/dist/commands/migration-plan.d.mts +1 -1
  29. package/dist/commands/migration-plan.mjs +1 -1
  30. package/dist/commands/migration-show.d.mts +4 -55
  31. package/dist/commands/migration-show.d.mts.map +1 -1
  32. package/dist/commands/migration-show.mjs +62 -152
  33. package/dist/commands/migration-show.mjs.map +1 -1
  34. package/dist/commands/migration-status.d.mts +5 -40
  35. package/dist/commands/migration-status.d.mts.map +1 -1
  36. package/dist/commands/migration-status.mjs +91 -64
  37. package/dist/commands/migration-status.mjs.map +1 -1
  38. package/dist/commands/ref.d.mts +1 -1
  39. package/dist/commands/ref.mjs +2 -2
  40. package/dist/{contract-emit-FtDVFs2Q.mjs → contract-emit-DmBG2Nnc.mjs} +2 -2
  41. package/dist/{contract-emit-FtDVFs2Q.mjs.map → contract-emit-DmBG2Nnc.mjs.map} +1 -1
  42. package/dist/{contract-infer-CVMuoJKk.mjs → contract-infer-BSWFKgI1.mjs} +3 -3
  43. package/dist/{contract-infer-CVMuoJKk.mjs.map → contract-infer-BSWFKgI1.mjs.map} +1 -1
  44. package/dist/contract-space-aggregate-loader-CVHGuA35.mjs +170 -0
  45. package/dist/contract-space-aggregate-loader-CVHGuA35.mjs.map +1 -0
  46. package/dist/{db-verify-B00o3LuC.mjs → db-verify-BzpwFyLg.mjs} +4 -4
  47. package/dist/{db-verify-B00o3LuC.mjs.map → db-verify-BzpwFyLg.mjs.map} +1 -1
  48. package/dist/exports/control-api.d.mts +1 -1
  49. package/dist/exports/control-api.mjs +1 -1
  50. package/dist/exports/index.mjs +1 -1
  51. package/dist/exports/init-output.mjs +1 -1
  52. package/dist/extension-pack-inputs-BiY86HbQ.mjs +62 -0
  53. package/dist/extension-pack-inputs-BiY86HbQ.mjs.map +1 -0
  54. package/dist/{global-flags-Dvibm2yu.d.mts → global-flags-DWsQ6SSI.d.mts} +1 -1
  55. package/dist/{global-flags-Dvibm2yu.d.mts.map → global-flags-DWsQ6SSI.d.mts.map} +1 -1
  56. package/dist/{graph-render-DJVv0_uf.mjs → graph-render-D2FnLpuK.mjs} +1 -1
  57. package/dist/{graph-render-DJVv0_uf.mjs.map → graph-render-D2FnLpuK.mjs.map} +1 -1
  58. package/dist/{init-BKgB6EKw.mjs → init-DEssiJ8j.mjs} +3 -3
  59. package/dist/{init-BKgB6EKw.mjs.map → init-DEssiJ8j.mjs.map} +1 -1
  60. package/dist/{inspect-live-schema-BXUd6RfS.mjs → inspect-live-schema-DlBM84nh.mjs} +3 -3
  61. package/dist/{inspect-live-schema-BXUd6RfS.mjs.map → inspect-live-schema-DlBM84nh.mjs.map} +1 -1
  62. package/dist/migration-check-CzLbAqIQ.mjs +341 -0
  63. package/dist/migration-check-CzLbAqIQ.mjs.map +1 -0
  64. package/dist/{migration-command-scaffold-3l3EdmSD.mjs → migration-command-scaffold-Bp8UHnvJ.mjs} +3 -3
  65. package/dist/{migration-command-scaffold-3l3EdmSD.mjs.map → migration-command-scaffold-Bp8UHnvJ.mjs.map} +1 -1
  66. package/dist/{migration-list-DopkAG7L.mjs → migration-list-C2xnaYsT.mjs} +2 -2
  67. package/dist/{migration-list-DopkAG7L.mjs.map → migration-list-C2xnaYsT.mjs.map} +1 -1
  68. package/dist/{migration-list-graph-render-C-daUZLU.d.mts → migration-list-graph-render-DKw1AT-e.d.mts} +1 -1
  69. package/dist/migration-list-graph-render-DKw1AT-e.d.mts.map +1 -0
  70. package/dist/{migration-plan-BHoeET4O.mjs → migration-plan-BLvOmNCu.mjs} +6 -5
  71. package/dist/{migration-plan-BHoeET4O.mjs.map → migration-plan-BLvOmNCu.mjs.map} +1 -1
  72. package/dist/{migration-types-BXWvz12q.d.mts → migration-types-q64xAI_J.d.mts} +1 -1
  73. package/dist/{migration-types-BXWvz12q.d.mts.map → migration-types-q64xAI_J.d.mts.map} +1 -1
  74. package/dist/{migrations-D-UCOGtk.mjs → migrations-vzQt9LI2.mjs} +3 -13
  75. package/dist/migrations-vzQt9LI2.mjs.map +1 -0
  76. package/dist/{output-CUIdfYo5.mjs → output-B60Gw5fu.mjs} +1 -1
  77. package/dist/{output-CUIdfYo5.mjs.map → output-B60Gw5fu.mjs.map} +1 -1
  78. package/dist/{ref-advancement-CHJ_8HxQ.mjs → ref-advancement-DRh5Nquq.mjs} +1 -1
  79. package/dist/{ref-advancement-CHJ_8HxQ.mjs.map → ref-advancement-DRh5Nquq.mjs.map} +1 -1
  80. package/dist/{types-UWB2-rrw.d.mts → types-CEtm6v6a.d.mts} +1 -8
  81. package/dist/types-CEtm6v6a.d.mts.map +1 -0
  82. package/dist/{verify-9gDJz6cm.mjs → verify-ktSRQvIS.mjs} +2 -2
  83. package/dist/{verify-9gDJz6cm.mjs.map → verify-ktSRQvIS.mjs.map} +1 -1
  84. package/package.json +18 -18
  85. package/src/commands/migrate.ts +52 -42
  86. package/src/commands/migration-check.ts +43 -83
  87. package/src/commands/migration-new.ts +44 -42
  88. package/src/commands/migration-plan.ts +2 -2
  89. package/src/commands/migration-show.ts +65 -284
  90. package/src/commands/migration-status.ts +131 -99
  91. package/src/control-api/client.ts +0 -1
  92. package/src/control-api/operations/db-verify.ts +9 -5
  93. package/src/control-api/operations/migration-apply.ts +11 -19
  94. package/src/control-api/types.ts +0 -7
  95. package/src/utils/command-helpers.ts +8 -3
  96. package/src/utils/contract-space-aggregate-loader.ts +221 -117
  97. package/src/utils/formatters/migrations.ts +4 -38
  98. package/src/utils/integrity-violation-to-check-failure.ts +130 -0
  99. package/dist/client-a5NJce0-.mjs.map +0 -1
  100. package/dist/commands/migration-check.mjs.map +0 -1
  101. package/dist/contract-space-aggregate-loader-EVU3n9YE.mjs +0 -160
  102. package/dist/contract-space-aggregate-loader-EVU3n9YE.mjs.map +0 -1
  103. package/dist/migration-list-graph-render-C-daUZLU.d.mts.map +0 -1
  104. package/dist/migrations-D-UCOGtk.mjs.map +0 -1
  105. package/dist/types-UWB2-rrw.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
  }
@@ -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),
@@ -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: {
@@ -12,11 +12,11 @@ import {
12
12
  type ContractSpaceAggregate,
13
13
  type ContractSpaceMember,
14
14
  graphWalkStrategy,
15
+ requireHeadRef,
15
16
  } from '@prisma-next/migration-tools/aggregate';
16
17
  import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
17
18
  import { errorNoInvariantPath } from '@prisma-next/migration-tools/errors';
18
19
  import { findPathWithDecision } from '@prisma-next/migration-tools/migration-graph';
19
- import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
20
20
  import { ifDefined } from '@prisma-next/utils/defined';
21
21
  import { notOk, ok } from '@prisma-next/utils/result';
22
22
  import {
@@ -56,15 +56,6 @@ export interface ExecuteMigrationApplyOptions<TFamilyId extends string, TTargetI
56
56
  readonly migrationsDir: string;
57
57
  readonly extensionPacks: ReadonlyArray<ControlExtensionDescriptor<TFamilyId, TTargetId>>;
58
58
  readonly targetId: TTargetId;
59
- /**
60
- * Already-loaded app-space migration packages. The CLI command
61
- * loads these via `loadMigrationPackages(appMigrationsDir)`; the
62
- * operation hydrates the app member's graph with them. Required
63
- * because the framework-neutral aggregate loader doesn't know how
64
- * to read the user's `migrations/` directory layout (it's family-
65
- * aware: ops.json shape, manifest keys, etc.).
66
- */
67
- readonly appMigrationPackages: ReadonlyArray<OnDiskMigrationPackage>;
68
59
  /**
69
60
  * Optional app-space ref override. When provided, the app member's
70
61
  * graph-walk targets this hash instead of `member.headRef.hash`.
@@ -125,7 +116,6 @@ export async function executeMigrationApply<TFamilyId extends string, TTargetId
125
116
  migrationsDir,
126
117
  extensionPacks,
127
118
  targetId,
128
- appMigrationPackages,
129
119
  refHash,
130
120
  refInvariants,
131
121
  refName,
@@ -138,7 +128,6 @@ export async function executeMigrationApply<TFamilyId extends string, TTargetId
138
128
  appContract: contract,
139
129
  extensionPacks,
140
130
  deserializeContract: (json) => familyInstance.deserializeContract(json),
141
- appMigrationPackages,
142
131
  };
143
132
  const loaded = await buildContractSpaceAggregate(loadInputs);
144
133
  if (!loaded.ok) {
@@ -161,12 +150,15 @@ export async function executeMigrationApply<TFamilyId extends string, TTargetId
161
150
  const atHeadResolutions = new Map<string, AggregatePerSpacePlan>();
162
151
  for (const member of allMembers) {
163
152
  const isAppMember = member.spaceId === aggregate.app.spaceId;
164
- const targetHash = isAppMember && refHash !== undefined ? refHash : member.headRef.hash;
153
+ // The aggregate passed the integrity gate, so every member's head ref
154
+ // is resolved (the app's is synthesised from the live contract).
155
+ const headRef = requireHeadRef(member);
156
+ const targetHash = isAppMember && refHash !== undefined ? refHash : headRef.hash;
165
157
  const liveMarker = markerRows.get(member.spaceId) ?? null;
166
158
 
167
159
  // Empty-graph members fail loudly: replay needs an on-disk path
168
160
  // and an empty graph means the user has never planned this space.
169
- if (member.migrations.graph.nodes.size === 0) {
161
+ if (member.graph().nodes.size === 0) {
170
162
  // Edge case: target == EMPTY (greenfield, nothing to do) or
171
163
  // the live marker already matches the target. Loader integrity
172
164
  // allows this for extensions whose head ref is the empty
@@ -197,9 +189,9 @@ export async function executeMigrationApply<TFamilyId extends string, TTargetId
197
189
  const targetInvariants =
198
190
  isAppMember && refHash !== undefined && refInvariants !== undefined
199
191
  ? refInvariants
200
- : member.headRef.invariants;
192
+ : headRef.invariants;
201
193
  const targetMember: ContractSpaceMember =
202
- targetHash === member.headRef.hash && targetInvariants === member.headRef.invariants
194
+ targetHash === headRef.hash && targetInvariants === headRef.invariants
203
195
  ? member
204
196
  : { ...member, headRef: { hash: targetHash, invariants: targetInvariants } };
205
197
 
@@ -224,7 +216,7 @@ export async function executeMigrationApply<TFamilyId extends string, TTargetId
224
216
  // is never a graph node, producing an empty `structuralPath` and
225
217
  // a less actionable diagnostic.
226
218
  const fromHash = liveMarker?.storageHash ?? EMPTY_CONTRACT_HASH;
227
- const structural = findPathWithDecision(targetMember.migrations.graph, fromHash, targetHash, {
219
+ const structural = findPathWithDecision(targetMember.graph(), fromHash, targetHash, {
228
220
  required: new Set<string>(),
229
221
  });
230
222
  const structuralPath =
@@ -369,7 +361,7 @@ function buildAtHeadResolution(args: {
369
361
  providedInvariants: [],
370
362
  },
371
363
  displayOps: [],
372
- destinationContract: member.contract,
364
+ destinationContract: member.contract(),
373
365
  strategy: 'graph-walk',
374
366
  migrationEdges: [],
375
367
  };
@@ -407,7 +399,7 @@ function buildSuccess(args: BuildSuccessArgs): MigrationApplySuccess {
407
399
  (r) => r.spaceId === args.aggregate.app.spaceId,
408
400
  );
409
401
  const appMarkerHash =
410
- appResolution?.entry.plan.destination.storageHash ?? args.aggregate.app.headRef.hash;
402
+ appResolution?.entry.plan.destination.storageHash ?? requireHeadRef(args.aggregate.app).hash;
411
403
 
412
404
  // Per-migration entries (one per authored edge) preserve the
413
405
  // single-space `migrationsApplied` count semantics for back-compat
@@ -18,7 +18,6 @@ import type {
18
18
  VerifyDatabaseSchemaResult,
19
19
  } from '@prisma-next/framework-components/control';
20
20
  import type { PslDocumentAst } from '@prisma-next/framework-components/psl-ast';
21
- import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
22
21
  import type { Result } from '@prisma-next/utils/result';
23
22
  import type { ExecuteDbVerifyResult } from './operations/db-verify';
24
23
 
@@ -548,12 +547,6 @@ export interface MigrationApplyOptions {
548
547
  readonly contract: unknown;
549
548
  /** Migrations root directory (`migrations/` under the project). */
550
549
  readonly migrationsDir: string;
551
- /**
552
- * Already-loaded app-space migration packages. The CLI loads these
553
- * via `loadMigrationPackages(appMigrationsDir)` before invoking
554
- * `migrationApply`.
555
- */
556
- readonly appMigrationPackages: ReadonlyArray<OnDiskMigrationPackage>;
557
550
  /**
558
551
  * Optional app-space ref override. When provided, the app member's
559
552
  * graph-walk targets this hash instead of `contract.storage.storageHash`.
@@ -242,9 +242,14 @@ export async function loadMigrationPackages(migrationsDir: string): Promise<{
242
242
  bundles: readonly OnDiskMigrationPackage[];
243
243
  graph: MigrationGraph;
244
244
  }> {
245
- const bundles = await readMigrationsDir(migrationsDir);
246
- const graph = reconstructGraph(bundles);
247
- return { bundles, graph };
245
+ // `readMigrationsDir` is tolerant: it retains hash-/invariant-mismatched
246
+ // packages and omits unparseable ones, reporting both via `problems`.
247
+ // This helper preserves its historical `{ bundles, graph }` contract by
248
+ // exposing the retained packages; callers that need to surface load
249
+ // problems read them from the tolerant primitive directly.
250
+ const { packages } = await readMigrationsDir(migrationsDir);
251
+ const graph = reconstructGraph(packages);
252
+ return { bundles: packages, graph };
248
253
  }
249
254
 
250
255
  /**