@prisma-next/cli 0.5.0-dev.9 → 0.6.0-dev.1

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 (186) hide show
  1. package/README.md +61 -26
  2. package/dist/cli-errors-B9OBbled.d.mts +3 -0
  3. package/dist/cli-errors-D3_sMh2K.mjs +33 -0
  4. package/dist/cli-errors-D3_sMh2K.mjs.map +1 -0
  5. package/dist/cli.mjs +16 -78
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/client-qVH-rEgd.mjs +1595 -0
  8. package/dist/client-qVH-rEgd.mjs.map +1 -0
  9. package/dist/{result-handler-Ba3zWQsI.mjs → command-helpers-BeZHkxV8.mjs} +70 -47
  10. package/dist/command-helpers-BeZHkxV8.mjs.map +1 -0
  11. package/dist/commands/contract-emit.d.mts.map +1 -1
  12. package/dist/commands/contract-emit.mjs +2 -4
  13. package/dist/commands/contract-infer.d.mts.map +1 -1
  14. package/dist/commands/contract-infer.mjs +2 -4
  15. package/dist/commands/db-init.d.mts.map +1 -1
  16. package/dist/commands/db-init.mjs +16 -13
  17. package/dist/commands/db-init.mjs.map +1 -1
  18. package/dist/commands/db-schema.d.mts.map +1 -1
  19. package/dist/commands/db-schema.mjs +6 -7
  20. package/dist/commands/db-schema.mjs.map +1 -1
  21. package/dist/commands/db-sign.d.mts.map +1 -1
  22. package/dist/commands/db-sign.mjs +9 -9
  23. package/dist/commands/db-sign.mjs.map +1 -1
  24. package/dist/commands/db-update.d.mts.map +1 -1
  25. package/dist/commands/db-update.mjs +15 -13
  26. package/dist/commands/db-update.mjs.map +1 -1
  27. package/dist/commands/db-verify.d.mts.map +1 -1
  28. package/dist/commands/db-verify.mjs +1 -321
  29. package/dist/commands/migration-apply.d.mts +28 -13
  30. package/dist/commands/migration-apply.d.mts.map +1 -1
  31. package/dist/commands/migration-apply.mjs +55 -151
  32. package/dist/commands/migration-apply.mjs.map +1 -1
  33. package/dist/commands/migration-new.d.mts +0 -1
  34. package/dist/commands/migration-new.d.mts.map +1 -1
  35. package/dist/commands/migration-new.mjs +34 -40
  36. package/dist/commands/migration-new.mjs.map +1 -1
  37. package/dist/commands/migration-plan.d.mts +33 -6
  38. package/dist/commands/migration-plan.d.mts.map +1 -1
  39. package/dist/commands/migration-plan.mjs +2 -348
  40. package/dist/commands/migration-ref.d.mts +1 -1
  41. package/dist/commands/migration-ref.d.mts.map +1 -1
  42. package/dist/commands/migration-ref.mjs +8 -12
  43. package/dist/commands/migration-ref.mjs.map +1 -1
  44. package/dist/commands/migration-show.d.mts +13 -7
  45. package/dist/commands/migration-show.d.mts.map +1 -1
  46. package/dist/commands/migration-show.mjs +35 -36
  47. package/dist/commands/migration-show.mjs.map +1 -1
  48. package/dist/commands/migration-status.d.mts +126 -5
  49. package/dist/commands/migration-status.d.mts.map +1 -1
  50. package/dist/commands/migration-status.mjs +2 -4
  51. package/dist/{config-loader-C25b63rJ.mjs → config-loader-B6sJjXTv.mjs} +3 -5
  52. package/dist/config-loader-B6sJjXTv.mjs.map +1 -0
  53. package/dist/config-loader.d.mts +0 -1
  54. package/dist/config-loader.d.mts.map +1 -1
  55. package/dist/config-loader.mjs +2 -3
  56. package/dist/contract-emit-9DBda5Ou.mjs +150 -0
  57. package/dist/contract-emit-9DBda5Ou.mjs.map +1 -0
  58. package/dist/contract-emit-B77TsJqf.mjs +327 -0
  59. package/dist/contract-emit-B77TsJqf.mjs.map +1 -0
  60. package/dist/{contract-enrichment-CAOELa-H.mjs → contract-enrichment-Dani0mMW.mjs} +4 -6
  61. package/dist/contract-enrichment-Dani0mMW.mjs.map +1 -0
  62. package/dist/{contract-infer-D9cC3rJm.mjs → contract-infer-BK9YFGEG.mjs} +13 -22
  63. package/dist/contract-infer-BK9YFGEG.mjs.map +1 -0
  64. package/dist/db-verify-C0y1PCO2.mjs +404 -0
  65. package/dist/db-verify-C0y1PCO2.mjs.map +1 -0
  66. package/dist/exports/config-types.mjs +1 -2
  67. package/dist/exports/control-api.d.mts +101 -586
  68. package/dist/exports/control-api.d.mts.map +1 -1
  69. package/dist/exports/control-api.mjs +4 -6
  70. package/dist/exports/index.d.mts.map +1 -1
  71. package/dist/exports/index.mjs +28 -30
  72. package/dist/exports/index.mjs.map +1 -1
  73. package/dist/exports/init-output.d.mts +2 -4
  74. package/dist/exports/init-output.d.mts.map +1 -1
  75. package/dist/exports/init-output.mjs +2 -3
  76. package/dist/extension-pack-inputs-C7xgE-vv.mjs +74 -0
  77. package/dist/extension-pack-inputs-C7xgE-vv.mjs.map +1 -0
  78. package/dist/{framework-components-Cr--XBKy.mjs → framework-components-ChqVUxR-.mjs} +3 -4
  79. package/dist/{framework-components-Cr--XBKy.mjs.map → framework-components-ChqVUxR-.mjs.map} +1 -1
  80. package/dist/global-flags-Icqpxk23.d.mts +12 -0
  81. package/dist/global-flags-Icqpxk23.d.mts.map +1 -0
  82. package/dist/helpers-eqdN8tH6.mjs +25 -0
  83. package/dist/helpers-eqdN8tH6.mjs.map +1 -0
  84. package/dist/{init-C5220SY9.mjs → init-DETSgw3h.mjs} +40 -49
  85. package/dist/init-DETSgw3h.mjs.map +1 -0
  86. package/dist/{inspect-live-schema-yrHAvG71.mjs → inspect-live-schema-CWYxGKlb.mjs} +10 -11
  87. package/dist/inspect-live-schema-CWYxGKlb.mjs.map +1 -0
  88. package/dist/migration-cli.d.mts +41 -12
  89. package/dist/migration-cli.d.mts.map +1 -1
  90. package/dist/migration-cli.mjs +309 -86
  91. package/dist/migration-cli.mjs.map +1 -1
  92. package/dist/{migration-command-scaffold-B3B09et6.mjs → migration-command-scaffold-B5dORFEv.mjs} +8 -9
  93. package/dist/migration-command-scaffold-B5dORFEv.mjs.map +1 -0
  94. package/dist/migration-plan-C6lVaHsO.mjs +554 -0
  95. package/dist/migration-plan-C6lVaHsO.mjs.map +1 -0
  96. package/dist/{migration-status-DUMiH8_G.mjs → migration-status-CZ-D5k7k.mjs} +272 -65
  97. package/dist/migration-status-CZ-D5k7k.mjs.map +1 -0
  98. package/dist/migrations-D_UJnpuW.mjs +216 -0
  99. package/dist/migrations-D_UJnpuW.mjs.map +1 -0
  100. package/dist/{output-BpcQrnnq.mjs → output-B16Kefzx.mjs} +9 -3
  101. package/dist/output-B16Kefzx.mjs.map +1 -0
  102. package/dist/{progress-adapter-DvQWB1nK.mjs → progress-adapter-DFfvZcYL.mjs} +2 -2
  103. package/dist/{progress-adapter-DvQWB1nK.mjs.map → progress-adapter-DFfvZcYL.mjs.map} +1 -1
  104. package/dist/result-handler-rmPVKIP2.mjs +25 -0
  105. package/dist/result-handler-rmPVKIP2.mjs.map +1 -0
  106. package/dist/rolldown-runtime-twds-ZHy.mjs +14 -0
  107. package/dist/{terminal-ui-C3ZLwQxK.mjs → terminal-ui-C_hFNbAn.mjs} +4 -28
  108. package/dist/terminal-ui-C_hFNbAn.mjs.map +1 -0
  109. package/dist/types-D7x-IFLO.d.mts +858 -0
  110. package/dist/types-D7x-IFLO.d.mts.map +1 -0
  111. package/dist/{verify-Bkycc-Tf.mjs → verify-CiwNWM9N.mjs} +3 -4
  112. package/dist/verify-CiwNWM9N.mjs.map +1 -0
  113. package/package.json +28 -26
  114. package/src/cli.ts +32 -6
  115. package/src/commands/contract-emit.ts +67 -163
  116. package/src/commands/contract-infer.ts +7 -20
  117. package/src/commands/db-init.ts +15 -3
  118. package/src/commands/db-update.ts +9 -4
  119. package/src/commands/db-verify.ts +47 -15
  120. package/src/commands/init/index.ts +1 -1
  121. package/src/commands/init/init.ts +2 -2
  122. package/src/commands/init/templates/code-templates.ts +26 -18
  123. package/src/commands/inspect-live-schema.ts +10 -5
  124. package/src/commands/migration-apply.ts +114 -212
  125. package/src/commands/migration-new.ts +42 -45
  126. package/src/commands/migration-plan.ts +212 -72
  127. package/src/commands/migration-ref.ts +8 -7
  128. package/src/commands/migration-show.ts +60 -41
  129. package/src/commands/migration-status.ts +483 -64
  130. package/src/config-path-validation.ts +0 -1
  131. package/src/control-api/client.ts +85 -5
  132. package/src/control-api/contract-enrichment.ts +6 -4
  133. package/src/control-api/operations/apply-aggregate.ts +290 -0
  134. package/src/control-api/operations/contract-emit.ts +198 -115
  135. package/src/control-api/operations/db-apply-aggregate.ts +397 -0
  136. package/src/control-api/operations/db-init.ts +51 -253
  137. package/src/control-api/operations/db-update.ts +66 -183
  138. package/src/control-api/operations/db-verify.ts +342 -0
  139. package/src/control-api/operations/migration-apply.ts +424 -131
  140. package/src/control-api/types.ts +280 -29
  141. package/src/exports/control-api.ts +15 -3
  142. package/src/load-ts-contract.ts +28 -26
  143. package/src/migration-cli.ts +445 -122
  144. package/src/utils/cli-errors.ts +49 -2
  145. package/src/utils/combine-schema-results.ts +84 -0
  146. package/src/utils/command-helpers.ts +69 -25
  147. package/src/utils/contract-space-aggregate-loader.ts +204 -0
  148. package/src/utils/contract-space-extension-migrations-pass.ts +120 -0
  149. package/src/utils/contract-space-migrate-pass.ts +156 -0
  150. package/src/utils/emit-queue.ts +26 -0
  151. package/src/utils/extension-pack-inputs.ts +170 -0
  152. package/src/utils/formatters/graph-migration-mapper.ts +7 -3
  153. package/src/utils/formatters/migrations.ts +197 -61
  154. package/src/utils/publish-contract-artifact-pair.ts +134 -0
  155. package/dist/cli-errors-BFYgBH3L.d.mts +0 -4
  156. package/dist/cli-errors-Cd79vmTH.mjs +0 -5
  157. package/dist/client-CrsnY58k.mjs +0 -997
  158. package/dist/client-CrsnY58k.mjs.map +0 -1
  159. package/dist/commands/db-verify.mjs.map +0 -1
  160. package/dist/commands/migration-plan.mjs.map +0 -1
  161. package/dist/config-loader-C25b63rJ.mjs.map +0 -1
  162. package/dist/contract-emit--feXyNd7.mjs +0 -4
  163. package/dist/contract-emit-NJ01hiiv.mjs +0 -195
  164. package/dist/contract-emit-NJ01hiiv.mjs.map +0 -1
  165. package/dist/contract-emit-V5SSitUT.mjs +0 -122
  166. package/dist/contract-emit-V5SSitUT.mjs.map +0 -1
  167. package/dist/contract-enrichment-CAOELa-H.mjs.map +0 -1
  168. package/dist/contract-infer-D9cC3rJm.mjs.map +0 -1
  169. package/dist/extract-operation-statements-DsFfxXVZ.mjs +0 -13
  170. package/dist/extract-operation-statements-DsFfxXVZ.mjs.map +0 -1
  171. package/dist/extract-sql-ddl-D9UbZDyz.mjs +0 -26
  172. package/dist/extract-sql-ddl-D9UbZDyz.mjs.map +0 -1
  173. package/dist/init-C5220SY9.mjs.map +0 -1
  174. package/dist/inspect-live-schema-yrHAvG71.mjs.map +0 -1
  175. package/dist/migration-command-scaffold-B3B09et6.mjs.map +0 -1
  176. package/dist/migration-status-DUMiH8_G.mjs.map +0 -1
  177. package/dist/migrations-Bo5WtTla.mjs +0 -153
  178. package/dist/migrations-Bo5WtTla.mjs.map +0 -1
  179. package/dist/output-BpcQrnnq.mjs.map +0 -1
  180. package/dist/result-handler-Ba3zWQsI.mjs.map +0 -1
  181. package/dist/terminal-ui-C3ZLwQxK.mjs.map +0 -1
  182. package/dist/validate-contract-deps-B_Cs29TL.mjs +0 -37
  183. package/dist/validate-contract-deps-B_Cs29TL.mjs.map +0 -1
  184. package/dist/verify-Bkycc-Tf.mjs.map +0 -1
  185. package/src/control-api/operations/extract-operation-statements.ts +0 -14
  186. package/src/control-api/operations/extract-sql-ddl.ts +0 -47
@@ -1,191 +1,484 @@
1
+ import type { Contract } from '@prisma-next/contract/types';
1
2
  import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
2
3
  import type {
3
4
  ControlDriverInstance,
5
+ ControlExtensionDescriptor,
4
6
  ControlFamilyInstance,
5
- MigrationRunnerResult,
6
7
  TargetMigrationsCapability,
7
8
  } from '@prisma-next/framework-components/control';
9
+ import {
10
+ type AggregatePerSpacePlan,
11
+ type ContractMarkerRecordLike,
12
+ type ContractSpaceAggregate,
13
+ type ContractSpaceMember,
14
+ graphWalkStrategy,
15
+ } from '@prisma-next/migration-tools/aggregate';
8
16
  import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
17
+ import { errorNoInvariantPath } from '@prisma-next/migration-tools/errors';
18
+ import { findPathWithDecision } from '@prisma-next/migration-tools/migration-graph';
19
+ import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
20
+ import { ifDefined } from '@prisma-next/utils/defined';
9
21
  import { notOk, ok } from '@prisma-next/utils/result';
22
+ import {
23
+ type BuildAggregateInputs,
24
+ buildContractSpaceAggregate,
25
+ } from '../../utils/contract-space-aggregate-loader';
10
26
  import type {
11
- MigrationApplyAppliedEntry,
27
+ AggregatePerSpaceExecutionEntry,
28
+ MigrationApplyFailure,
29
+ MigrationApplyPathDecision,
12
30
  MigrationApplyResult,
13
- MigrationApplyStep,
31
+ MigrationApplySuccess,
14
32
  OnControlProgress,
15
33
  } from '../types';
34
+ import { applyAggregate, buildPerSpaceBreakdown } from './apply-aggregate';
16
35
 
36
+ /**
37
+ * Inputs for the aggregate-walking `migration apply` control-api
38
+ * operation.
39
+ *
40
+ * The CLI command resolves the descriptor surface (config, refs,
41
+ * contract envelope) and hands a flat input through. The operation
42
+ * is the single descriptor-free seam between the CLI and the
43
+ * aggregate runtime.
44
+ */
17
45
  export interface ExecuteMigrationApplyOptions<TFamilyId extends string, TTargetId extends string> {
18
46
  readonly driver: ControlDriverInstance<TFamilyId, TTargetId>;
19
47
  readonly familyInstance: ControlFamilyInstance<TFamilyId, unknown>;
20
- readonly originHash: string;
21
- readonly destinationHash: string;
22
- readonly pendingMigrations: readonly MigrationApplyStep[];
48
+ /** Already-validated app contract (the canonical "where we are heading" hash). */
49
+ readonly contract: Contract;
23
50
  readonly migrations: TargetMigrationsCapability<
24
51
  TFamilyId,
25
52
  TTargetId,
26
53
  ControlFamilyInstance<TFamilyId, unknown>
27
54
  >;
28
55
  readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, TTargetId>>;
29
- readonly targetId: string;
56
+ readonly migrationsDir: string;
57
+ readonly extensionPacks: ReadonlyArray<ControlExtensionDescriptor<TFamilyId, TTargetId>>;
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
+ /**
69
+ * Optional app-space ref override. When provided, the app member's
70
+ * graph-walk targets this hash instead of `member.headRef.hash`.
71
+ * Extensions are unaffected — they always walk to their own head.
72
+ *
73
+ * Sub-spec § `--ref <hash>` semantics under multi-space.
74
+ */
75
+ readonly refHash?: string;
76
+ /**
77
+ * Required invariants attached to the user-supplied app-space ref.
78
+ * Threaded into the graph-walk's `required` calculation so the
79
+ * planner picks an invariant-bearing path and surfaces the
80
+ * required/satisfied set on the success envelope. When `refHash`
81
+ * is absent the file's `member.headRef.invariants` are used.
82
+ */
83
+ readonly refInvariants?: readonly string[];
84
+ /**
85
+ * Resolved name of the user-supplied app-space ref. Surfaces in
86
+ * `pathDecision.refName` and in `MIGRATION.NO_INVARIANT_PATH`
87
+ * error envelopes so diagnostics name what the user actually
88
+ * passed (`--ref prod`) instead of a synthetic placeholder.
89
+ * Ignored when `refHash` is absent.
90
+ */
91
+ readonly refName?: string;
30
92
  readonly onProgress?: OnControlProgress;
31
93
  }
32
94
 
95
+ /**
96
+ * Apply pending migrations across every contract space (app +
97
+ * extensions). Replay-only: graph-walk against the on-disk graph for
98
+ * every member; no synth, no introspection.
99
+ *
100
+ * Pipeline:
101
+ *
102
+ * 1. Load aggregate from disk (loader hydrates extension graphs;
103
+ * caller provides app-space packages).
104
+ * 2. Read live marker rows per space (`familyInstance.readAllMarkers`).
105
+ * 3. Per member: `graphWalkStrategy` plots the path from the live
106
+ * marker to `member.headRef.hash` (or `refHash` for the app
107
+ * member when provided). Empty-graph members fail loudly — a
108
+ * "never planned" space is a user-error condition for replay.
109
+ * 4. Hand off to {@link applyAggregate} (the runner-driving tail
110
+ * shared with `db init` / `db update`). Marker advancement is
111
+ * inside the per-space transaction.
112
+ *
113
+ * Sub-spec § `migration apply` semantics + § Required changes 1.
114
+ */
33
115
  export async function executeMigrationApply<TFamilyId extends string, TTargetId extends string>(
34
116
  options: ExecuteMigrationApplyOptions<TFamilyId, TTargetId>,
35
117
  ): Promise<MigrationApplyResult> {
36
118
  const {
37
119
  driver,
38
120
  familyInstance,
39
- originHash,
40
- destinationHash,
41
- pendingMigrations,
121
+ contract,
42
122
  migrations,
43
123
  frameworkComponents,
124
+ migrationsDir,
125
+ extensionPacks,
44
126
  targetId,
127
+ appMigrationPackages,
128
+ refHash,
129
+ refInvariants,
130
+ refName,
45
131
  onProgress,
46
132
  } = options;
47
133
 
48
- if (pendingMigrations.length === 0) {
49
- if (originHash !== destinationHash) {
50
- return notOk({
51
- code: 'MIGRATION_PATH_NOT_FOUND' as const,
52
- summary: 'No migrations provided for requested origin and destination',
53
- why: `Requested ${originHash} -> ${destinationHash} but pendingMigrations is empty`,
54
- meta: { originHash, destinationHash },
55
- });
56
- }
57
- return ok({
58
- migrationsApplied: 0,
59
- markerHash: originHash,
60
- applied: [],
61
- summary: 'Already up to date',
62
- });
134
+ const loadInputs: BuildAggregateInputs<TFamilyId, TTargetId> = {
135
+ targetId,
136
+ migrationsDir,
137
+ appContract: contract,
138
+ extensionPacks,
139
+ validateContract: (json) => familyInstance.validateContract(json),
140
+ appMigrationPackages,
141
+ };
142
+ const loaded = await buildContractSpaceAggregate(loadInputs);
143
+ if (!loaded.ok) {
144
+ throw loaded.failure;
63
145
  }
146
+ const aggregate = loaded.value;
64
147
 
65
- const firstMigration = pendingMigrations[0]!;
66
- const lastMigration = pendingMigrations[pendingMigrations.length - 1]!;
67
- if (firstMigration.from !== originHash || lastMigration.to !== destinationHash) {
68
- return notOk({
69
- code: 'MIGRATION_PATH_NOT_FOUND' as const,
70
- summary: 'Migration apply path does not match requested origin and destination',
71
- why: `Path resolved as ${firstMigration.from} -> ${lastMigration.to}, but requested ${originHash} -> ${destinationHash}`,
72
- meta: {
73
- originHash,
74
- destinationHash,
75
- pathOrigin: firstMigration.from,
76
- pathDestination: lastMigration.to,
77
- },
78
- });
79
- }
148
+ const markerRows = await familyInstance.readAllMarkers({ driver });
80
149
 
81
- for (let i = 1; i < pendingMigrations.length; i++) {
82
- const previous = pendingMigrations[i - 1]!;
83
- const current = pendingMigrations[i]!;
84
- if (previous.to !== current.from) {
85
- return notOk({
86
- code: 'MIGRATION_PATH_NOT_FOUND' as const,
87
- summary: 'Migration apply path contains a discontinuity between adjacent migrations',
88
- why: `Migration "${previous.dirName}" ends at ${previous.to}, but next migration "${current.dirName}" starts at ${current.from}`,
89
- meta: {
90
- originHash,
91
- destinationHash,
92
- previousDirName: previous.dirName,
93
- previousTo: previous.to,
94
- currentDirName: current.dirName,
95
- currentFrom: current.from,
96
- discontinuityIndex: i,
97
- },
150
+ // Plan every member via graph-walk. App member targets `refHash`
151
+ // when provided, otherwise its own head; extensions always walk
152
+ // to their own head ref.
153
+ const allMembers: ReadonlyArray<ContractSpaceMember> = [aggregate.app, ...aggregate.extensions];
154
+ const perSpacePlans = new Map<string, AggregatePerSpacePlan>();
155
+ // Already-at-head empty-graph members (typically extensions whose
156
+ // head ref is the empty sentinel, or whose live marker already
157
+ // matches the target). Kept out of the runner schedule so we don't
158
+ // write spurious markers for greenfield extensions, but merged back
159
+ // into the success envelope so every loaded member is represented.
160
+ const atHeadResolutions = new Map<string, AggregatePerSpacePlan>();
161
+ for (const member of allMembers) {
162
+ const isAppMember = member.spaceId === aggregate.app.spaceId;
163
+ const targetHash = isAppMember && refHash !== undefined ? refHash : member.headRef.hash;
164
+ const liveMarker = markerRows.get(member.spaceId) ?? null;
165
+
166
+ // Empty-graph members fail loudly: replay needs an on-disk path
167
+ // and an empty graph means the user has never planned this space.
168
+ if (member.migrations.graph.nodes.size === 0) {
169
+ // Edge case: target == EMPTY (greenfield, nothing to do) or
170
+ // the live marker already matches the target. Loader integrity
171
+ // allows this for extensions whose head ref is the empty
172
+ // sentinel. Record a zero-op resolution so the aggregate result
173
+ // still surfaces the member in `perSpace[]` as already-at-head;
174
+ // the runner is not invoked for these members because they have
175
+ // no authored ops and (for greenfield extensions) no marker to
176
+ // advance.
177
+ const liveHash = liveMarker?.storageHash;
178
+ if (
179
+ targetHash === liveHash ||
180
+ (liveHash === undefined && targetHash === EMPTY_CONTRACT_HASH)
181
+ ) {
182
+ atHeadResolutions.set(
183
+ member.spaceId,
184
+ buildAtHeadResolution({
185
+ aggregateTargetId: aggregate.targetId,
186
+ member,
187
+ targetHash,
188
+ liveMarker,
189
+ }),
190
+ );
191
+ continue;
192
+ }
193
+ return notOk(buildNeverPlannedFailure(member.spaceId, targetHash));
194
+ }
195
+
196
+ const targetInvariants =
197
+ isAppMember && refHash !== undefined && refInvariants !== undefined
198
+ ? refInvariants
199
+ : member.headRef.invariants;
200
+ const targetMember: ContractSpaceMember =
201
+ targetHash === member.headRef.hash && targetInvariants === member.headRef.invariants
202
+ ? member
203
+ : { ...member, headRef: { hash: targetHash, invariants: targetInvariants } };
204
+
205
+ const walked = graphWalkStrategy({
206
+ aggregateTargetId: aggregate.targetId,
207
+ member: targetMember,
208
+ currentMarker: liveMarker,
209
+ ...(isAppMember && refName !== undefined ? { refName } : {}),
210
+ });
211
+ if (walked.kind === 'unreachable') {
212
+ return notOk(buildPathNotFoundFailure(member.spaceId, liveMarker, targetHash));
213
+ }
214
+ if (walked.kind === 'unsatisfiable') {
215
+ // Surface the canonical MIGRATION.NO_INVARIANT_PATH envelope
216
+ // (the error rendering pipeline maps it to meta.code +
217
+ // meta.required + meta.missing + meta.structuralPath that the
218
+ // cli-journeys invariant suite asserts on).
219
+ const fromHash = liveMarker?.storageHash ?? '';
220
+ const structural = findPathWithDecision(targetMember.migrations.graph, fromHash, targetHash, {
221
+ required: new Set<string>(),
222
+ });
223
+ const structuralPath =
224
+ structural.kind === 'ok'
225
+ ? structural.decision.selectedPath.map((edge) => ({
226
+ dirName: edge.dirName,
227
+ migrationHash: edge.migrationHash,
228
+ from: edge.from,
229
+ to: edge.to,
230
+ invariants: edge.invariants,
231
+ }))
232
+ : [];
233
+ throw errorNoInvariantPath({
234
+ ...(isAppMember && refName !== undefined ? { refName } : {}),
235
+ required: targetInvariants,
236
+ missing: walked.missing,
237
+ structuralPath,
98
238
  });
99
239
  }
240
+
241
+ perSpacePlans.set(member.spaceId, walked.result);
100
242
  }
101
243
 
102
- const runner = migrations.createRunner(familyInstance);
103
- const applied: MigrationApplyAppliedEntry[] = [];
244
+ const canonicalOrder = [...aggregate.extensions.map((m) => m.spaceId), aggregate.app.spaceId];
245
+ const applyOrder = canonicalOrder.filter((spaceId) => perSpacePlans.has(spaceId));
104
246
 
105
- for (const migration of pendingMigrations) {
106
- const migrationSpanId = `migration:${migration.dirName}`;
107
- onProgress?.({
108
- action: 'migrationApply',
109
- kind: 'spanStart',
110
- spanId: migrationSpanId,
111
- label: `Applying ${migration.dirName}`,
247
+ // Short-circuit: nothing pending across any space (no runner-bound
248
+ // plans). Surfaces every loaded member — including at-head empty-
249
+ // graph extensions — in `perSpace[]` so the result reflects the
250
+ // full aggregate, not just the spaces the runner would have touched.
251
+ const totalPlannedOps = sumPlannedOps(applyOrder, perSpacePlans);
252
+ if (totalPlannedOps === 0) {
253
+ const ordered = canonicalOrder
254
+ .filter((spaceId) => perSpacePlans.has(spaceId) || atHeadResolutions.has(spaceId))
255
+ .map((spaceId) => {
256
+ const entry = perSpacePlans.get(spaceId) ?? atHeadResolutions.get(spaceId);
257
+ if (entry === undefined) {
258
+ throw new Error(`Unreachable: missing per-space plan for "${spaceId}"`);
259
+ }
260
+ return { spaceId, entry };
261
+ });
262
+ const perSpace = buildPerSpaceBreakdown(ordered, aggregate.app.spaceId, {
263
+ includeMarkers: true,
112
264
  });
265
+ const totalSpaces = ordered.length;
266
+ return ok(
267
+ buildSuccess({
268
+ aggregate,
269
+ orderedResolutions: ordered,
270
+ perSpace,
271
+ totalOpsExecuted: 0,
272
+ summary:
273
+ totalSpaces === 0
274
+ ? 'Already up to date — no contract spaces are loaded'
275
+ : totalSpaces === 1
276
+ ? 'Already up to date'
277
+ : `Already up to date across ${totalSpaces} space(s)`,
278
+ }),
279
+ );
280
+ }
113
281
 
114
- const { operations } = migration;
115
-
116
- // Allow all operation classes. The policy gate belongs at plan time, not
117
- // apply time — the planner already decided what to emit. Restricting here
118
- // would be a tautology (the allowed set would just mirror what's in ops).
119
- const policy = {
120
- allowedOperationClasses: ['additive', 'widening', 'destructive', 'data'] as const,
121
- };
282
+ const applied = await applyAggregate({
283
+ aggregate,
284
+ perSpacePlans,
285
+ applyOrder,
286
+ driver,
287
+ familyInstance,
288
+ migrations,
289
+ frameworkComponents,
290
+ policy: { allowedOperationClasses: ['additive', 'widening', 'destructive', 'data'] },
291
+ action: 'migrationApply',
292
+ ...ifDefined('onProgress', onProgress),
293
+ });
122
294
 
123
- // EMPTY_CONTRACT_HASH means "no prior state" — the runner expects origin: null
124
- // for a fresh database (no marker present).
125
- const plan = {
126
- targetId,
127
- origin: migration.from === EMPTY_CONTRACT_HASH ? null : { storageHash: migration.from },
128
- destination: { storageHash: migration.to },
129
- operations,
295
+ if (!applied.ok) {
296
+ const failure: MigrationApplyFailure = {
297
+ code: 'RUNNER_FAILED',
298
+ summary: applied.failure.summary,
299
+ why: applied.failure.why,
300
+ meta: applied.failure.meta,
130
301
  };
302
+ return notOk(failure);
303
+ }
131
304
 
132
- const destinationContract = familyInstance.validateContract(migration.toContract);
133
-
134
- const runnerResult: MigrationRunnerResult = await runner.execute({
135
- plan,
136
- driver,
137
- destinationContract,
138
- policy,
139
- executionChecks: {
140
- prechecks: true,
141
- postchecks: true,
142
- idempotencyChecks: true,
143
- },
144
- frameworkComponents,
305
+ // Merge at-head zero-op resolutions back into the canonical order
306
+ // so the success envelope surfaces every loaded member, not just
307
+ // those the runner executed.
308
+ const orderedAll = canonicalOrder
309
+ .filter((spaceId) => perSpacePlans.has(spaceId) || atHeadResolutions.has(spaceId))
310
+ .map((spaceId) => {
311
+ if (perSpacePlans.has(spaceId)) {
312
+ const fromRunner = applied.value.orderedResolutions.find((r) => r.spaceId === spaceId);
313
+ if (fromRunner !== undefined) return fromRunner;
314
+ }
315
+ const entry = atHeadResolutions.get(spaceId);
316
+ if (entry === undefined) {
317
+ throw new Error(`Unreachable: missing per-space plan for "${spaceId}"`);
318
+ }
319
+ return { spaceId, entry };
145
320
  });
321
+ const perSpaceAll = buildPerSpaceBreakdown(orderedAll, aggregate.app.spaceId, {
322
+ includeMarkers: true,
323
+ });
324
+ const totalMigrationsApplied = applied.value.orderedResolutions.reduce(
325
+ (sum, r) => sum + (r.entry.migrationEdges?.length ?? 0),
326
+ 0,
327
+ );
328
+ const summary = `Applied ${totalMigrationsApplied} migration(s) (${applied.value.totalOpsExecuted} operation(s)) across ${orderedAll.length} contract space(s)`;
146
329
 
147
- if (!runnerResult.ok) {
148
- onProgress?.({
149
- action: 'migrationApply',
150
- kind: 'spanEnd',
151
- spanId: migrationSpanId,
152
- outcome: 'error',
153
- });
154
- return notOk({
155
- code: 'RUNNER_FAILED' as const,
156
- summary: runnerResult.failure.summary,
157
- why: runnerResult.failure.why,
158
- meta: {
159
- migration: migration.dirName,
160
- from: migration.from,
161
- to: migration.to,
162
- ...(runnerResult.failure.meta ?? {}),
163
- },
164
- });
165
- }
330
+ return ok(
331
+ buildSuccess({
332
+ aggregate,
333
+ orderedResolutions: orderedAll,
334
+ perSpace: perSpaceAll,
335
+ totalOpsExecuted: applied.value.totalOpsExecuted,
336
+ summary,
337
+ }),
338
+ );
339
+ }
166
340
 
167
- onProgress?.({
168
- action: 'migrationApply',
169
- kind: 'spanEnd',
170
- spanId: migrationSpanId,
171
- outcome: 'ok',
172
- });
341
+ /**
342
+ * Build a zero-op {@link AggregatePerSpacePlan} for an empty-graph
343
+ * member whose live marker already matches the target. Lets the apply
344
+ * pipeline thread the member through `perSpacePlans` -> `applyOrder`
345
+ * -> the success envelope's `perSpace[]` block so the result reflects
346
+ * every loaded space, even when there is nothing to execute.
347
+ */
348
+ function buildAtHeadResolution(args: {
349
+ readonly aggregateTargetId: string;
350
+ readonly member: ContractSpaceMember;
351
+ readonly targetHash: string;
352
+ readonly liveMarker: ContractMarkerRecordLike | null;
353
+ }): AggregatePerSpacePlan {
354
+ const { aggregateTargetId, member, targetHash, liveMarker } = args;
355
+ return {
356
+ plan: {
357
+ targetId: aggregateTargetId,
358
+ spaceId: member.spaceId,
359
+ origin: liveMarker === null ? null : { storageHash: liveMarker.storageHash },
360
+ destination: { storageHash: targetHash },
361
+ operations: [],
362
+ providedInvariants: [],
363
+ },
364
+ displayOps: [],
365
+ destinationContract: member.contract,
366
+ strategy: 'graph-walk',
367
+ migrationEdges: [],
368
+ };
369
+ }
173
370
 
174
- applied.push({
175
- dirName: migration.dirName,
176
- from: migration.from,
177
- to: migration.to,
178
- operationsExecuted: runnerResult.value.operationsExecuted,
179
- });
371
+ function sumPlannedOps(
372
+ applyOrder: readonly string[],
373
+ perSpacePlans: ReadonlyMap<string, AggregatePerSpacePlan>,
374
+ ): number {
375
+ let total = 0;
376
+ for (const spaceId of applyOrder) {
377
+ const entry = perSpacePlans.get(spaceId);
378
+ if (!entry) continue;
379
+ total += entry.plan.operations.length;
180
380
  }
381
+ return total;
382
+ }
181
383
 
182
- const finalHash = pendingMigrations[pendingMigrations.length - 1]!.to;
183
- const totalOps = applied.reduce((sum, a) => sum + a.operationsExecuted, 0);
384
+ interface BuildSuccessArgs {
385
+ readonly aggregate: ContractSpaceAggregate;
386
+ readonly orderedResolutions: ReadonlyArray<{
387
+ readonly spaceId: string;
388
+ readonly entry: AggregatePerSpacePlan;
389
+ }>;
390
+ readonly perSpace: ReadonlyArray<AggregatePerSpaceExecutionEntry>;
391
+ readonly totalOpsExecuted: number;
392
+ readonly summary: string;
393
+ }
394
+
395
+ function buildSuccess(args: BuildSuccessArgs): MigrationApplySuccess {
396
+ // The marker hash surfaced at the top level is the **app member's**
397
+ // post-apply marker (today's single-space `markerHash` field).
398
+ // Per-space markers live on `perSpace[].marker.storageHash`.
399
+ const appResolution = args.orderedResolutions.find(
400
+ (r) => r.spaceId === args.aggregate.app.spaceId,
401
+ );
402
+ const appMarkerHash =
403
+ appResolution?.entry.plan.destination.storageHash ?? args.aggregate.app.headRef.hash;
404
+
405
+ // Per-migration entries (one per authored edge) preserve the
406
+ // single-space `migrationsApplied` count semantics for back-compat
407
+ // with existing JSON-shape consumers (e.g. `parsed.applied.length`
408
+ // in integration tests). The aggregate per-space breakdown lives on
409
+ // `perSpace[]`.
410
+ const applied = args.orderedResolutions.flatMap((r) => {
411
+ const edges = r.entry.migrationEdges ?? [];
412
+ return edges.map((edge) => ({
413
+ spaceId: r.spaceId,
414
+ dirName: edge.dirName,
415
+ migrationHash: edge.migrationHash,
416
+ from: edge.from,
417
+ to: edge.to,
418
+ operationsExecuted: edge.operationCount,
419
+ }));
420
+ });
421
+
422
+ const appPlan = appResolution?.entry;
423
+ const pathDecision: MigrationApplyPathDecision | undefined = appPlan?.pathDecision
424
+ ? {
425
+ fromHash: appPlan.pathDecision.fromHash,
426
+ toHash: appPlan.pathDecision.toHash,
427
+ alternativeCount: appPlan.pathDecision.alternativeCount,
428
+ tieBreakReasons: appPlan.pathDecision.tieBreakReasons,
429
+ ...(appPlan.pathDecision.refName !== undefined
430
+ ? { refName: appPlan.pathDecision.refName }
431
+ : {}),
432
+ requiredInvariants: appPlan.pathDecision.requiredInvariants ?? [],
433
+ satisfiedInvariants: appPlan.pathDecision.satisfiedInvariants ?? [],
434
+ selectedPath: appPlan.pathDecision.selectedPath.map((entry) => ({
435
+ dirName: entry.dirName,
436
+ migrationHash: entry.migrationHash,
437
+ from: entry.from,
438
+ to: entry.to,
439
+ invariants: entry.invariants,
440
+ })),
441
+ }
442
+ : undefined;
184
443
 
185
- return ok({
444
+ return {
186
445
  migrationsApplied: applied.length,
187
- markerHash: finalHash,
446
+ markerHash: appMarkerHash,
188
447
  applied,
189
- summary: `Applied ${applied.length} migration(s) (${totalOps} operation(s)), marker at ${finalHash}`,
190
- });
448
+ summary: args.summary,
449
+ perSpace: args.perSpace,
450
+ ...(pathDecision !== undefined ? { pathDecision } : {}),
451
+ };
452
+ }
453
+
454
+ function buildNeverPlannedFailure(spaceId: string, targetHash: string): MigrationApplyFailure {
455
+ return {
456
+ code: 'MIGRATION_PATH_NOT_FOUND',
457
+ summary: `No on-disk migrations for contract space "${spaceId}"`,
458
+ why: `migration apply is replay-only: every contract space must have an authored migration graph on disk. Space "${spaceId}" has no migrations under \`migrations/${spaceId}/\` but its head ref targets "${targetHash}". Run \`prisma-next migration plan\` first to materialise the path.`,
459
+ meta: { spaceId, target: targetHash, kind: 'neverPlanned' },
460
+ };
461
+ }
462
+
463
+ function buildPathNotFoundFailure(
464
+ spaceId: string,
465
+ marker: ContractMarkerRecordLike | null,
466
+ targetHash: string,
467
+ ): MigrationApplyFailure {
468
+ const fromHash = marker?.storageHash ?? '<empty>';
469
+ // The single-space-degenerate phrasing names the user-visible
470
+ // condition (a contract has been emitted that no on-disk
471
+ // migration reaches) so the error reads naturally for the
472
+ // single-space app case. Multi-space callers see the same
473
+ // condition expressed against the offending space.
474
+ const summary =
475
+ spaceId === 'app'
476
+ ? 'Current contract has no planned migration path'
477
+ : `Current contract has no planned migration path for contract space "${spaceId}"`;
478
+ return {
479
+ code: 'MIGRATION_PATH_NOT_FOUND',
480
+ summary,
481
+ why: `Cannot reach target "${targetHash}" from current marker "${fromHash}" in space "${spaceId}". The on-disk migration graph for this space does not connect the two states. Run \`prisma-next migration plan\` to materialise the path.`,
482
+ meta: { spaceId, fromHash, targetHash, kind: 'pathUnreachable' },
483
+ };
191
484
  }