@prisma-next/cli 0.5.0-dev.8 → 0.5.0-dev.81

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 +56 -21
  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-CoDVPvQ4.mjs} +26 -35
  85. package/dist/init-CoDVPvQ4.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 +26 -24
  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 +12 -4
  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
@@ -0,0 +1,397 @@
1
+ import type { Contract } from '@prisma-next/contract/types';
2
+ import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
3
+ import type {
4
+ ControlDriverInstance,
5
+ ControlExtensionDescriptor,
6
+ ControlFamilyInstance,
7
+ MigrationOperationPolicy,
8
+ MigrationPlanOperation,
9
+ OperationPreview,
10
+ TargetMigrationsCapability,
11
+ } from '@prisma-next/framework-components/control';
12
+ import { hasOperationPreview } from '@prisma-next/framework-components/control';
13
+ import {
14
+ type AggregatePlannerError,
15
+ type ContractSpaceAggregate,
16
+ planAggregate,
17
+ } from '@prisma-next/migration-tools/aggregate';
18
+ import { ifDefined } from '@prisma-next/utils/defined';
19
+ import { notOk, ok } from '@prisma-next/utils/result';
20
+ import { CliStructuredError } from '../../utils/cli-errors';
21
+ import {
22
+ type BuildAggregateInputs,
23
+ buildContractSpaceAggregate,
24
+ } from '../../utils/contract-space-aggregate-loader';
25
+ import type {
26
+ AggregatePerSpaceExecutionEntry,
27
+ DbInitFailure,
28
+ DbInitResult,
29
+ DbInitSuccess,
30
+ DbUpdateFailure,
31
+ DbUpdateResult,
32
+ DbUpdateSuccess,
33
+ OnControlProgress,
34
+ } from '../types';
35
+ import { applyAggregate, buildPerSpaceBreakdown, collectOrdered } from './apply-aggregate';
36
+ import { stripOperations } from './migration-helpers';
37
+
38
+ /**
39
+ * Span IDs emitted via `onProgress` during the aggregate apply flow.
40
+ * Stable identifiers consumed by the structured-output renderer and by
41
+ * tests asserting on span ids. The `apply` span itself is owned by
42
+ * the {@link applyAggregate} primitive — only the introspect / plan
43
+ * spans are emitted directly here.
44
+ */
45
+ const SPAN_IDS = {
46
+ introspect: 'introspect',
47
+ plan: 'plan',
48
+ } as const;
49
+
50
+ /**
51
+ * Inputs shared by `db init` and `db update` aggregate apply flows.
52
+ *
53
+ * Accepts the already-validated app contract + descriptor list — the
54
+ * loader gathers the rest from disk + descriptors. The CLI is the
55
+ * descriptor-import boundary; everything downstream is descriptor-free.
56
+ */
57
+ export interface ExecuteAggregateApplyOptions<TFamilyId extends string, TTargetId extends string> {
58
+ readonly driver: ControlDriverInstance<TFamilyId, TTargetId>;
59
+ readonly familyInstance: ControlFamilyInstance<TFamilyId, unknown>;
60
+ readonly contract: Contract;
61
+ readonly mode: 'plan' | 'apply';
62
+ readonly migrations: TargetMigrationsCapability<
63
+ TFamilyId,
64
+ TTargetId,
65
+ ControlFamilyInstance<TFamilyId, unknown>
66
+ >;
67
+ readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, TTargetId>>;
68
+ readonly migrationsDir: string;
69
+ readonly extensionPacks: ReadonlyArray<ControlExtensionDescriptor<TFamilyId, TTargetId>>;
70
+ readonly targetId: TTargetId;
71
+ readonly policy: MigrationOperationPolicy;
72
+ readonly action: 'dbInit' | 'dbUpdate';
73
+ readonly onProgress?: OnControlProgress;
74
+ }
75
+
76
+ /**
77
+ * Loader → planner → runner pipeline shared by `db init` and `db update`.
78
+ *
79
+ * The pipeline:
80
+ *
81
+ * 1. **Load**: build a {@link ContractSpaceAggregate} from the descriptor
82
+ * set + on-disk on-disk artefacts. Any layout / drift / disjointness /
83
+ * integrity violation short-circuits with a structured error.
84
+ * 2. **Read DB state**: marker rows (`familyInstance.readAllMarkers`)
85
+ * + introspected schema (`familyInstance.introspect`).
86
+ * 3. **Plan**: {@link planAggregate} chooses graph-walk vs synth per
87
+ * member according to `callerPolicy.ignoreGraphFor`. The app member
88
+ * is forced through synth (today's daily-driver behaviour); every
89
+ * extension member walks its on-disk graph.
90
+ * 4. **Apply** (when `mode === 'apply'`): every per-space `MigrationPlan`
91
+ * feeds into the runner's `executeAcrossSpaces` — one outer
92
+ * transaction across every space; failure on any space rolls back
93
+ * every space's writes.
94
+ */
95
+ export async function executeAggregateApply<TFamilyId extends string, TTargetId extends string>(
96
+ options: ExecuteAggregateApplyOptions<TFamilyId, TTargetId>,
97
+ ): Promise<DbInitResult | DbUpdateResult> {
98
+ const {
99
+ driver,
100
+ familyInstance,
101
+ contract,
102
+ mode,
103
+ migrations,
104
+ frameworkComponents,
105
+ migrationsDir,
106
+ extensionPacks,
107
+ targetId,
108
+ policy,
109
+ action,
110
+ onProgress,
111
+ } = options;
112
+
113
+ // 1. Load aggregate from descriptors + on-disk state.
114
+ const loadInputs: BuildAggregateInputs<TFamilyId, TTargetId> = {
115
+ targetId,
116
+ migrationsDir,
117
+ appContract: contract,
118
+ extensionPacks,
119
+ validateContract: (json) => familyInstance.validateContract(json),
120
+ };
121
+ const loaded = await buildContractSpaceAggregate(loadInputs);
122
+ if (!loaded.ok) {
123
+ throw loaded.failure;
124
+ }
125
+ const aggregate = loaded.value;
126
+
127
+ // 2. Read live DB state (markers + schema).
128
+ const markerRows = await familyInstance.readAllMarkers({ driver });
129
+
130
+ // 2a. Orphan-marker pre-flight: refuse to apply when a marker row
131
+ // exists for a space that is not declared in the aggregate.
132
+ // Mirrors the M2 marker-check that `db init` / `db update` ran via
133
+ // `runContractSpaceVerifierMarkerCheck`. Runs before planning so a
134
+ // user with an orphaned marker (e.g. a retired extension whose
135
+ // migrations directory has been removed) is told to clean it up
136
+ // rather than silently advancing the app's marker.
137
+ const orphanMarkerError = detectOrphanMarkers(aggregate, markerRows);
138
+ if (orphanMarkerError !== null) {
139
+ throw orphanMarkerError;
140
+ }
141
+
142
+ onProgress?.({
143
+ action,
144
+ kind: 'spanStart',
145
+ spanId: SPAN_IDS.introspect,
146
+ label: 'Introspecting database schema',
147
+ });
148
+ const schemaIR = await familyInstance.introspect({ driver });
149
+ onProgress?.({ action, kind: 'spanEnd', spanId: SPAN_IDS.introspect, outcome: 'ok' });
150
+
151
+ // 3. Plan via aggregate planner. App is forced through synth (today's
152
+ // `db init` / `db update` daily-driver behaviour); extensions walk
153
+ // their on-disk migration graphs.
154
+ onProgress?.({
155
+ action,
156
+ kind: 'spanStart',
157
+ spanId: SPAN_IDS.plan,
158
+ label: 'Planning migration',
159
+ });
160
+ const planResult = await planAggregate<TFamilyId, TTargetId>({
161
+ aggregate,
162
+ currentDBState: { markersBySpaceId: markerRows, schemaIntrospection: schemaIR },
163
+ familyInstance,
164
+ migrations,
165
+ frameworkComponents,
166
+ callerPolicy: { ignoreGraphFor: new Set([aggregate.app.spaceId]) },
167
+ operationPolicy: policy,
168
+ });
169
+ if (!planResult.ok) {
170
+ onProgress?.({ action, kind: 'spanEnd', spanId: SPAN_IDS.plan, outcome: 'error' });
171
+ return mapPlannerError(planResult.failure);
172
+ }
173
+ onProgress?.({ action, kind: 'spanEnd', spanId: SPAN_IDS.plan, outcome: 'ok' });
174
+
175
+ const orderedResolutions = collectOrdered(planResult.value.applyOrder, planResult.value.perSpace);
176
+
177
+ // The destination's structural shape comes from the app's plan — its
178
+ // `destination` is the storage hash users see in CLI output.
179
+ const appResolution = orderedResolutions.find((r) => r.spaceId === aggregate.app.spaceId);
180
+ if (!appResolution) {
181
+ throw new Error(
182
+ 'Aggregate planner returned no plan for the app member — the planner is supposed to always emit one.',
183
+ );
184
+ }
185
+ const appPlan = appResolution.entry.plan;
186
+
187
+ // 4. Plan-mode: surface aggregate operations without applying.
188
+ if (mode === 'plan') {
189
+ const aggregateOps = orderedResolutions.flatMap((r) => r.entry.displayOps);
190
+ const preview = hasOperationPreview(familyInstance)
191
+ ? familyInstance.toOperationPreview(aggregateOps)
192
+ : undefined;
193
+ const perSpace = buildPerSpaceBreakdown(orderedResolutions, aggregate.app.spaceId, {
194
+ includeMarkers: false,
195
+ });
196
+ const summary = `Planned ${aggregateOps.length} operation(s) across ${orderedResolutions.length} space(s)`;
197
+ return wrapPlanResult({
198
+ operations: aggregateOps,
199
+ destination: appPlan.destination,
200
+ preview,
201
+ perSpace,
202
+ summary,
203
+ });
204
+ }
205
+
206
+ // 5. Apply mode: hand off to the shared `applyAggregate` primitive.
207
+ // The runner-driving tail is identical for `db init` / `db update` /
208
+ // `migration apply` — only how each caller produces `perSpacePlans`
209
+ // differs (synth + graph-walk via planAggregate here; graph-walk
210
+ // only for migration apply). See M6 sub-spec § Required changes 1.
211
+ const applied = await applyAggregate({
212
+ aggregate,
213
+ perSpacePlans: planResult.value.perSpace,
214
+ applyOrder: planResult.value.applyOrder,
215
+ driver,
216
+ familyInstance,
217
+ migrations,
218
+ frameworkComponents,
219
+ policy,
220
+ action,
221
+ ...ifDefined('onProgress', onProgress),
222
+ });
223
+ if (!applied.ok) {
224
+ return buildRunnerFailure({
225
+ summary: applied.failure.summary,
226
+ ...ifDefined('why', applied.failure.why),
227
+ meta: applied.failure.meta,
228
+ });
229
+ }
230
+
231
+ const aggregateOps = applied.value.orderedResolutions.flatMap((r) => r.entry.displayOps);
232
+ const summary =
233
+ action === 'dbInit'
234
+ ? `Applied ${applied.value.totalOpsExecuted} operation(s) across ${applied.value.orderedResolutions.length} space(s), database signed`
235
+ : applied.value.totalOpsExecuted === 0
236
+ ? `Database already matches contract across ${applied.value.orderedResolutions.length} space(s), signature updated`
237
+ : `Applied ${applied.value.totalOpsExecuted} operation(s) across ${applied.value.orderedResolutions.length} space(s), signature updated`;
238
+
239
+ return wrapApplyResult({
240
+ operations: aggregateOps,
241
+ destination: appPlan.destination,
242
+ operationsPlanned: applied.value.totalOpsPlanned,
243
+ operationsExecuted: applied.value.totalOpsExecuted,
244
+ perSpace: applied.value.perSpace,
245
+ summary,
246
+ });
247
+ }
248
+
249
+ /**
250
+ * Compare the live `_prisma_marker` rows against the aggregate's
251
+ * declared members. Any marker row whose `space` is not a member of
252
+ * the aggregate is an "orphan" — typically a marker left behind by
253
+ * an extension that was removed from `extensionPacks` without first
254
+ * cleaning up its on-disk migrations / database tables.
255
+ *
256
+ * Returns a {@link CliStructuredError} envelope (code `5002`,
257
+ * `kind: 'orphanMarker'`) for the first orphan it finds, or `null`
258
+ * when every marker row maps to a declared member. Mirrors the M2
259
+ * `runContractSpaceVerifierMarkerCheck` envelope so downstream
260
+ * tooling (integration tests, JSON consumers) keeps asserting on the
261
+ * same shape.
262
+ */
263
+ function detectOrphanMarkers(
264
+ aggregate: ContractSpaceAggregate,
265
+ markerRows: ReadonlyMap<string, unknown>,
266
+ ): CliStructuredError | null {
267
+ const memberSpaceIds = new Set<string>([
268
+ aggregate.app.spaceId,
269
+ ...aggregate.extensions.map((m) => m.spaceId),
270
+ ]);
271
+ const orphans: string[] = [];
272
+ for (const [spaceId, row] of markerRows) {
273
+ if (row !== null && row !== undefined && !memberSpaceIds.has(spaceId)) {
274
+ orphans.push(spaceId);
275
+ }
276
+ }
277
+ if (orphans.length === 0) return null;
278
+ orphans.sort((a, b) => a.localeCompare(b));
279
+ const summary =
280
+ orphans.length === 1
281
+ ? `Orphan contract-space marker detected for "${orphans[0]}"`
282
+ : `Orphan contract-space markers detected for ${orphans.length} spaces`;
283
+ return new CliStructuredError('5002', summary, {
284
+ domain: 'MIG',
285
+ why: `The database has \`_prisma_marker\` rows for spaces (${orphans
286
+ .map((s) => `"${s}"`)
287
+ .join(
288
+ ', ',
289
+ )}) that are not declared in the project's \`extensionPacks\`. The aggregate pipeline refuses to advance markers it cannot account for.`,
290
+ fix: 'Either re-declare the missing extension(s) in `extensionPacks` (so the aggregate owns them again), or remove the orphan marker row(s) from `_prisma_marker` once you have confirmed the corresponding tables can be safely retired.',
291
+ docsUrl: 'https://pris.ly/contract-spaces',
292
+ meta: {
293
+ violations: orphans.map((spaceId) => ({ kind: 'orphanMarker', spaceId })),
294
+ },
295
+ });
296
+ }
297
+
298
+ function mapPlannerError(error: AggregatePlannerError): DbInitResult | DbUpdateResult {
299
+ if (error.kind === 'appSynthFailure') {
300
+ const failure: DbInitFailure | DbUpdateFailure = {
301
+ code: 'PLANNING_FAILED',
302
+ summary: 'Migration planning failed due to conflicts',
303
+ conflicts: error.conflicts,
304
+ why: undefined,
305
+ meta: undefined,
306
+ };
307
+ return notOk(failure) as DbInitResult | DbUpdateResult;
308
+ }
309
+ if (error.kind === 'extensionPathUnreachable') {
310
+ return buildRunnerFailure({
311
+ summary: `Cannot resolve apply path for extension space "${error.spaceId}"`,
312
+ why: `No path in the on-disk migration graph for extension space "${error.spaceId}" reaches the on-disk head ref hash "${error.target}".`,
313
+ meta: { spaceId: error.spaceId, target: error.target },
314
+ });
315
+ }
316
+ if (error.kind === 'extensionPathUnsatisfiable') {
317
+ return buildRunnerFailure({
318
+ summary: `Cannot resolve apply path for extension space "${error.spaceId}"`,
319
+ why: `On-disk migration graph for extension space "${error.spaceId}" reaches the on-disk head ref but does not cover required invariants: ${error.missingInvariants.join(', ')}.`,
320
+ meta: { spaceId: error.spaceId, missingInvariants: error.missingInvariants },
321
+ });
322
+ }
323
+ // policyConflict — surfaces as a runner-style failure naming the
324
+ // space; conceptually a configuration bug, but mapping it onto the
325
+ // existing failure surface keeps callers untouched.
326
+ return buildRunnerFailure({
327
+ summary: `Aggregate planner policy conflict for space "${error.spaceId}"`,
328
+ why: error.detail,
329
+ meta: { spaceId: error.spaceId },
330
+ });
331
+ }
332
+
333
+ function wrapPlanResult(args: {
334
+ readonly operations: readonly MigrationPlanOperation[];
335
+ readonly destination: { readonly storageHash: string; readonly profileHash?: string };
336
+ readonly preview: OperationPreview | undefined;
337
+ readonly perSpace: readonly AggregatePerSpaceExecutionEntry[];
338
+ readonly summary: string;
339
+ }): DbInitResult | DbUpdateResult {
340
+ const success: DbInitSuccess | DbUpdateSuccess = {
341
+ mode: 'plan',
342
+ plan: {
343
+ operations: stripOperations(args.operations),
344
+ ...ifDefined('preview', args.preview),
345
+ },
346
+ destination: {
347
+ storageHash: args.destination.storageHash,
348
+ ...ifDefined('profileHash', args.destination.profileHash),
349
+ },
350
+ perSpace: args.perSpace,
351
+ summary: args.summary,
352
+ };
353
+ return ok(success);
354
+ }
355
+
356
+ function wrapApplyResult(args: {
357
+ readonly operations: readonly MigrationPlanOperation[];
358
+ readonly destination: { readonly storageHash: string; readonly profileHash?: string };
359
+ readonly operationsPlanned: number;
360
+ readonly operationsExecuted: number;
361
+ readonly perSpace: readonly AggregatePerSpaceExecutionEntry[];
362
+ readonly summary: string;
363
+ }): DbInitResult | DbUpdateResult {
364
+ const success: DbInitSuccess | DbUpdateSuccess = {
365
+ mode: 'apply',
366
+ plan: { operations: stripOperations(args.operations) },
367
+ destination: {
368
+ storageHash: args.destination.storageHash,
369
+ ...ifDefined('profileHash', args.destination.profileHash),
370
+ },
371
+ execution: {
372
+ operationsPlanned: args.operationsPlanned,
373
+ operationsExecuted: args.operationsExecuted,
374
+ },
375
+ marker: args.destination.profileHash
376
+ ? { storageHash: args.destination.storageHash, profileHash: args.destination.profileHash }
377
+ : { storageHash: args.destination.storageHash },
378
+ perSpace: args.perSpace,
379
+ summary: args.summary,
380
+ };
381
+ return ok(success);
382
+ }
383
+
384
+ function buildRunnerFailure(args: {
385
+ readonly summary: string;
386
+ readonly why?: string;
387
+ readonly meta: Record<string, unknown>;
388
+ }): DbInitResult | DbUpdateResult {
389
+ const failure: DbInitFailure | DbUpdateFailure = {
390
+ code: 'RUNNER_FAILED',
391
+ summary: args.summary,
392
+ why: args.why,
393
+ meta: args.meta,
394
+ conflicts: undefined,
395
+ };
396
+ return notOk(failure) as DbInitResult | DbUpdateResult;
397
+ }