@prisma-next/cli 0.5.0-dev.66 → 0.5.0-dev.68

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 (138) hide show
  1. package/dist/{cli-errors-By1iVE3z.mjs → cli-errors-D3_sMh2K.mjs} +2 -3
  2. package/dist/{cli-errors-By1iVE3z.mjs.map → cli-errors-D3_sMh2K.mjs.map} +1 -1
  3. package/dist/{cli-errors-DDeVsP2Y.d.mts → cli-errors-QH8kf-C2.d.mts} +0 -2
  4. package/dist/cli.mjs +12 -76
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/client-0ZX24FXF.mjs +1398 -0
  7. package/dist/client-0ZX24FXF.mjs.map +1 -0
  8. package/dist/commands/contract-emit.d.mts.map +1 -1
  9. package/dist/commands/contract-emit.mjs +2 -4
  10. package/dist/commands/contract-infer.d.mts.map +1 -1
  11. package/dist/commands/contract-infer.mjs +2 -4
  12. package/dist/commands/db-init.d.mts.map +1 -1
  13. package/dist/commands/db-init.mjs +11 -11
  14. package/dist/commands/db-init.mjs.map +1 -1
  15. package/dist/commands/db-schema.d.mts.map +1 -1
  16. package/dist/commands/db-schema.mjs +5 -7
  17. package/dist/commands/db-schema.mjs.map +1 -1
  18. package/dist/commands/db-sign.d.mts.map +1 -1
  19. package/dist/commands/db-sign.mjs +8 -9
  20. package/dist/commands/db-sign.mjs.map +1 -1
  21. package/dist/commands/db-update.d.mts.map +1 -1
  22. package/dist/commands/db-update.mjs +11 -11
  23. package/dist/commands/db-update.mjs.map +1 -1
  24. package/dist/commands/db-verify.d.mts.map +1 -1
  25. package/dist/commands/db-verify.mjs +1 -321
  26. package/dist/commands/migration-apply.d.mts.map +1 -1
  27. package/dist/commands/migration-apply.mjs +16 -17
  28. package/dist/commands/migration-apply.mjs.map +1 -1
  29. package/dist/commands/migration-new.d.mts +0 -1
  30. package/dist/commands/migration-new.d.mts.map +1 -1
  31. package/dist/commands/migration-new.mjs +10 -11
  32. package/dist/commands/migration-new.mjs.map +1 -1
  33. package/dist/commands/migration-plan.d.mts.map +1 -1
  34. package/dist/commands/migration-plan.mjs +1 -345
  35. package/dist/commands/migration-ref.d.mts +1 -1
  36. package/dist/commands/migration-ref.d.mts.map +1 -1
  37. package/dist/commands/migration-ref.mjs +5 -6
  38. package/dist/commands/migration-ref.mjs.map +1 -1
  39. package/dist/commands/migration-show.d.mts +1 -1
  40. package/dist/commands/migration-show.d.mts.map +1 -1
  41. package/dist/commands/migration-show.mjs +13 -13
  42. package/dist/commands/migration-show.mjs.map +1 -1
  43. package/dist/commands/migration-status.d.mts.map +1 -1
  44. package/dist/commands/migration-status.mjs +2 -4
  45. package/dist/{config-loader-ih8ViDb_.mjs → config-loader-B6sJjXTv.mjs} +2 -4
  46. package/dist/config-loader-B6sJjXTv.mjs.map +1 -0
  47. package/dist/config-loader.d.mts +0 -1
  48. package/dist/config-loader.d.mts.map +1 -1
  49. package/dist/config-loader.mjs +2 -3
  50. package/dist/{contract-emit-CnTXVVbF.mjs → contract-emit-B3ChISB_.mjs} +22 -13
  51. package/dist/contract-emit-B3ChISB_.mjs.map +1 -0
  52. package/dist/{contract-emit-CcZr3HS9.mjs → contract-emit-DkMqO7f2.mjs} +8 -10
  53. package/dist/contract-emit-DkMqO7f2.mjs.map +1 -0
  54. package/dist/{contract-enrichment-xDeJBC-o.mjs → contract-enrichment-CF6ogEJ_.mjs} +2 -2
  55. package/dist/contract-enrichment-CF6ogEJ_.mjs.map +1 -0
  56. package/dist/{contract-infer-sER84Le-.mjs → contract-infer-BDKAE0B0.mjs} +5 -7
  57. package/dist/{contract-infer-sER84Le-.mjs.map → contract-infer-BDKAE0B0.mjs.map} +1 -1
  58. package/dist/db-verify-B4TdDKOI.mjs +403 -0
  59. package/dist/db-verify-B4TdDKOI.mjs.map +1 -0
  60. package/dist/exports/config-types.mjs +1 -2
  61. package/dist/exports/control-api.d.mts +202 -7
  62. package/dist/exports/control-api.d.mts.map +1 -1
  63. package/dist/exports/control-api.mjs +4 -6
  64. package/dist/exports/index.d.mts.map +1 -1
  65. package/dist/exports/index.mjs +28 -30
  66. package/dist/exports/index.mjs.map +1 -1
  67. package/dist/exports/init-output.d.mts +2 -4
  68. package/dist/exports/init-output.d.mts.map +1 -1
  69. package/dist/exports/init-output.mjs +2 -3
  70. package/dist/{framework-components-Bgcre3Z6.mjs → framework-components-gwAHl7ml.mjs} +3 -4
  71. package/dist/{framework-components-Bgcre3Z6.mjs.map → framework-components-gwAHl7ml.mjs.map} +1 -1
  72. package/dist/{init-DC4sL4Rp.mjs → init-Deo7U8_U.mjs} +13 -30
  73. package/dist/init-Deo7U8_U.mjs.map +1 -0
  74. package/dist/{inspect-live-schema-BQN21nNO.mjs → inspect-live-schema-BAgQMYpD.mjs} +7 -8
  75. package/dist/inspect-live-schema-BAgQMYpD.mjs.map +1 -0
  76. package/dist/migration-cli.d.mts +0 -1
  77. package/dist/migration-cli.d.mts.map +1 -1
  78. package/dist/migration-cli.mjs +2 -3
  79. package/dist/migration-cli.mjs.map +1 -1
  80. package/dist/{migration-command-scaffold-DLmYGRug.mjs → migration-command-scaffold-B8J702Uh.mjs} +7 -8
  81. package/dist/migration-command-scaffold-B8J702Uh.mjs.map +1 -0
  82. package/dist/migration-plan-BcKNnTM7.mjs +530 -0
  83. package/dist/migration-plan-BcKNnTM7.mjs.map +1 -0
  84. package/dist/{migration-status-CDW4RDsO.mjs → migration-status-CjwB2of-.mjs} +10 -14
  85. package/dist/migration-status-CjwB2of-.mjs.map +1 -0
  86. package/dist/{migrations-MEoKMiV5.mjs → migrations-CIK94AJf.mjs} +3 -4
  87. package/dist/migrations-CIK94AJf.mjs.map +1 -0
  88. package/dist/{output-BpcQrnnq.mjs → output-DnjfCC_u.mjs} +9 -3
  89. package/dist/output-DnjfCC_u.mjs.map +1 -0
  90. package/dist/{progress-adapter-DgRGldpT.mjs → progress-adapter-xASh41wr.mjs} +2 -2
  91. package/dist/{progress-adapter-DgRGldpT.mjs.map → progress-adapter-xASh41wr.mjs.map} +1 -1
  92. package/dist/{result-handler-Ch6hVnOo.mjs → result-handler-DWb1rFS-.mjs} +20 -10
  93. package/dist/result-handler-DWb1rFS-.mjs.map +1 -0
  94. package/dist/{terminal-ui-u2YgKghu.mjs → terminal-ui-zaRDhJnP.mjs} +2 -6
  95. package/dist/{terminal-ui-u2YgKghu.mjs.map → terminal-ui-zaRDhJnP.mjs.map} +1 -1
  96. package/dist/{verify-BT9tgCOH.mjs → verify-BEIa9638.mjs} +3 -4
  97. package/dist/verify-BEIa9638.mjs.map +1 -0
  98. package/package.json +24 -24
  99. package/src/commands/db-init.ts +13 -3
  100. package/src/commands/db-update.ts +7 -3
  101. package/src/commands/db-verify.ts +47 -15
  102. package/src/commands/init/index.ts +1 -1
  103. package/src/commands/init/init.ts +2 -2
  104. package/src/commands/migration-apply.ts +9 -9
  105. package/src/commands/migration-new.ts +4 -4
  106. package/src/commands/migration-plan.ts +66 -9
  107. package/src/commands/migration-show.ts +7 -5
  108. package/src/commands/migration-status.ts +3 -3
  109. package/src/control-api/client.ts +42 -0
  110. package/src/control-api/operations/db-apply-aggregate.ts +446 -0
  111. package/src/control-api/operations/db-init.ts +51 -258
  112. package/src/control-api/operations/db-update.ts +66 -188
  113. package/src/control-api/operations/db-verify.ts +342 -0
  114. package/src/control-api/types.ts +56 -0
  115. package/src/exports/control-api.ts +13 -2
  116. package/src/load-ts-contract.ts +28 -26
  117. package/src/utils/combine-schema-results.ts +84 -0
  118. package/src/utils/command-helpers.ts +24 -2
  119. package/src/utils/contract-space-aggregate-loader.ts +236 -0
  120. package/src/utils/contract-space-extension-migrations-pass.ts +120 -0
  121. package/src/utils/contract-space-migrate-pass.ts +156 -0
  122. package/dist/client-hUCMXFE_.mjs +0 -1031
  123. package/dist/client-hUCMXFE_.mjs.map +0 -1
  124. package/dist/commands/db-verify.mjs.map +0 -1
  125. package/dist/commands/migration-plan.mjs.map +0 -1
  126. package/dist/config-loader-ih8ViDb_.mjs.map +0 -1
  127. package/dist/contract-emit-BkRH9lGt.mjs +0 -4
  128. package/dist/contract-emit-CcZr3HS9.mjs.map +0 -1
  129. package/dist/contract-emit-CnTXVVbF.mjs.map +0 -1
  130. package/dist/contract-enrichment-xDeJBC-o.mjs.map +0 -1
  131. package/dist/init-DC4sL4Rp.mjs.map +0 -1
  132. package/dist/inspect-live-schema-BQN21nNO.mjs.map +0 -1
  133. package/dist/migration-command-scaffold-DLmYGRug.mjs.map +0 -1
  134. package/dist/migration-status-CDW4RDsO.mjs.map +0 -1
  135. package/dist/migrations-MEoKMiV5.mjs.map +0 -1
  136. package/dist/output-BpcQrnnq.mjs.map +0 -1
  137. package/dist/result-handler-Ch6hVnOo.mjs.map +0 -1
  138. package/dist/verify-BT9tgCOH.mjs.map +0 -1
@@ -0,0 +1,446 @@
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
+ MultiSpaceCapableRunner,
10
+ MultiSpaceRunnerPerSpaceOptions,
11
+ OperationPreview,
12
+ TargetMigrationsCapability,
13
+ } from '@prisma-next/framework-components/control';
14
+ import {
15
+ hasMultiSpaceRunner,
16
+ hasOperationPreview,
17
+ } from '@prisma-next/framework-components/control';
18
+ import {
19
+ type AggregatePerSpacePlan,
20
+ type AggregatePlannerError,
21
+ type ContractSpaceAggregate,
22
+ planAggregate,
23
+ } from '@prisma-next/migration-tools/aggregate';
24
+ import { ifDefined } from '@prisma-next/utils/defined';
25
+ import { notOk, ok } from '@prisma-next/utils/result';
26
+ import { CliStructuredError, errorRunnerFailed } from '../../utils/cli-errors';
27
+ import {
28
+ type BuildAggregateInputs,
29
+ buildContractSpaceAggregate,
30
+ } from '../../utils/contract-space-aggregate-loader';
31
+ import type {
32
+ DbInitFailure,
33
+ DbInitResult,
34
+ DbInitSuccess,
35
+ DbUpdateFailure,
36
+ DbUpdateResult,
37
+ DbUpdateSuccess,
38
+ OnControlProgress,
39
+ } from '../types';
40
+ import { stripOperations } from './migration-helpers';
41
+
42
+ /**
43
+ * Span IDs emitted via `onProgress` during the aggregate apply flow.
44
+ * Stable identifiers consumed by the structured-output renderer and by
45
+ * tests asserting on span ids.
46
+ */
47
+ const SPAN_IDS = {
48
+ introspect: 'introspect',
49
+ plan: 'plan',
50
+ apply: 'apply',
51
+ } as const;
52
+
53
+ /**
54
+ * Inputs shared by `db init` and `db update` aggregate apply flows.
55
+ *
56
+ * Accepts the already-validated app contract + descriptor list — the
57
+ * loader gathers the rest from disk + descriptors. The CLI is the
58
+ * descriptor-import boundary; everything downstream is descriptor-free.
59
+ */
60
+ export interface ExecuteAggregateApplyOptions<TFamilyId extends string, TTargetId extends string> {
61
+ readonly driver: ControlDriverInstance<TFamilyId, TTargetId>;
62
+ readonly familyInstance: ControlFamilyInstance<TFamilyId, unknown>;
63
+ readonly contract: Contract;
64
+ readonly mode: 'plan' | 'apply';
65
+ readonly migrations: TargetMigrationsCapability<
66
+ TFamilyId,
67
+ TTargetId,
68
+ ControlFamilyInstance<TFamilyId, unknown>
69
+ >;
70
+ readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, TTargetId>>;
71
+ readonly migrationsDir: string;
72
+ readonly extensionPacks: ReadonlyArray<ControlExtensionDescriptor<TFamilyId, TTargetId>>;
73
+ readonly targetId: TTargetId;
74
+ readonly policy: MigrationOperationPolicy;
75
+ readonly action: 'dbInit' | 'dbUpdate';
76
+ readonly onProgress?: OnControlProgress;
77
+ }
78
+
79
+ /**
80
+ * Loader → planner → runner pipeline shared by `db init` and `db update`.
81
+ *
82
+ * The pipeline:
83
+ *
84
+ * 1. **Load**: build a {@link ContractSpaceAggregate} from the descriptor
85
+ * set + on-disk on-disk artefacts. Any layout / drift / disjointness /
86
+ * integrity violation short-circuits with a structured error.
87
+ * 2. **Read DB state**: marker rows (`familyInstance.readAllMarkers`)
88
+ * + introspected schema (`familyInstance.introspect`).
89
+ * 3. **Plan**: {@link planAggregate} chooses graph-walk vs synth per
90
+ * member according to `callerPolicy.ignoreGraphFor`. The app member
91
+ * is forced through synth (today's daily-driver behaviour); every
92
+ * extension member walks its on-disk graph.
93
+ * 4. **Apply** (when `mode === 'apply'`): every per-space `MigrationPlan`
94
+ * feeds into the runner's `executeAcrossSpaces` — one outer
95
+ * transaction across every space; failure on any space rolls back
96
+ * every space's writes.
97
+ */
98
+ export async function executeAggregateApply<TFamilyId extends string, TTargetId extends string>(
99
+ options: ExecuteAggregateApplyOptions<TFamilyId, TTargetId>,
100
+ ): Promise<DbInitResult | DbUpdateResult> {
101
+ const {
102
+ driver,
103
+ familyInstance,
104
+ contract,
105
+ mode,
106
+ migrations,
107
+ frameworkComponents,
108
+ migrationsDir,
109
+ extensionPacks,
110
+ targetId,
111
+ policy,
112
+ action,
113
+ onProgress,
114
+ } = options;
115
+
116
+ // 1. Load aggregate from descriptors + on-disk state.
117
+ const loadInputs: BuildAggregateInputs<TFamilyId, TTargetId> = {
118
+ targetId,
119
+ migrationsDir,
120
+ appContract: contract,
121
+ extensionPacks,
122
+ validateContract: (json) => familyInstance.validateContract(json),
123
+ };
124
+ const loaded = await buildContractSpaceAggregate(loadInputs);
125
+ if (!loaded.ok) {
126
+ throw loaded.failure;
127
+ }
128
+ const aggregate = loaded.value;
129
+
130
+ // 2. Read live DB state (markers + schema).
131
+ const markerRows = await familyInstance.readAllMarkers({ driver });
132
+
133
+ // 2a. Orphan-marker pre-flight: refuse to apply when a marker row
134
+ // exists for a space that is not declared in the aggregate.
135
+ // Mirrors the M2 marker-check that `db init` / `db update` ran via
136
+ // `runContractSpaceVerifierMarkerCheck`. Runs before planning so a
137
+ // user with an orphaned marker (e.g. a retired extension whose
138
+ // migrations directory has been removed) is told to clean it up
139
+ // rather than silently advancing the app's marker.
140
+ const orphanMarkerError = detectOrphanMarkers(aggregate, markerRows);
141
+ if (orphanMarkerError !== null) {
142
+ throw orphanMarkerError;
143
+ }
144
+
145
+ onProgress?.({
146
+ action,
147
+ kind: 'spanStart',
148
+ spanId: SPAN_IDS.introspect,
149
+ label: 'Introspecting database schema',
150
+ });
151
+ const schemaIR = await familyInstance.introspect({ driver });
152
+ onProgress?.({ action, kind: 'spanEnd', spanId: SPAN_IDS.introspect, outcome: 'ok' });
153
+
154
+ // 3. Plan via aggregate planner. App is forced through synth (today's
155
+ // `db init` / `db update` daily-driver behaviour); extensions walk
156
+ // their on-disk migration graphs.
157
+ onProgress?.({
158
+ action,
159
+ kind: 'spanStart',
160
+ spanId: SPAN_IDS.plan,
161
+ label: 'Planning migration',
162
+ });
163
+ const planResult = await planAggregate<TFamilyId, TTargetId>({
164
+ aggregate,
165
+ currentDBState: { markersBySpaceId: markerRows, schemaIntrospection: schemaIR },
166
+ familyInstance,
167
+ migrations,
168
+ frameworkComponents,
169
+ callerPolicy: { ignoreGraphFor: new Set([aggregate.app.spaceId]) },
170
+ operationPolicy: policy,
171
+ });
172
+ if (!planResult.ok) {
173
+ onProgress?.({ action, kind: 'spanEnd', spanId: SPAN_IDS.plan, outcome: 'error' });
174
+ return mapPlannerError(planResult.failure);
175
+ }
176
+ onProgress?.({ action, kind: 'spanEnd', spanId: SPAN_IDS.plan, outcome: 'ok' });
177
+
178
+ const orderedResolutions = collectOrdered(planResult.value.applyOrder, planResult.value.perSpace);
179
+
180
+ // The destination's structural shape comes from the app's plan — its
181
+ // `destination` is the storage hash users see in CLI output.
182
+ const appResolution = orderedResolutions.find((r) => r.spaceId === aggregate.app.spaceId);
183
+ if (!appResolution) {
184
+ throw new Error(
185
+ 'Aggregate planner returned no plan for the app member — the planner is supposed to always emit one.',
186
+ );
187
+ }
188
+ const appPlan = appResolution.entry.plan;
189
+
190
+ // 4. Plan-mode: surface aggregate operations without applying.
191
+ if (mode === 'plan') {
192
+ const aggregateOps = orderedResolutions.flatMap((r) => r.entry.displayOps);
193
+ const preview = hasOperationPreview(familyInstance)
194
+ ? familyInstance.toOperationPreview(aggregateOps)
195
+ : undefined;
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
+ summary,
202
+ });
203
+ }
204
+
205
+ // 5. Apply mode: dispatch into the multi-space runner.
206
+ const runner = migrations.createRunner(familyInstance);
207
+ if (!hasMultiSpaceRunner(runner)) {
208
+ throw errorRunnerFailed(
209
+ `Runner for target "${aggregate.targetId}" does not implement \`executeAcrossSpaces\``,
210
+ {
211
+ why: `${action === 'dbInit' ? 'db init' : 'db update'} requires multi-space-capable runners (today: every SQL family runner).`,
212
+ },
213
+ );
214
+ }
215
+
216
+ onProgress?.({
217
+ action,
218
+ kind: 'spanStart',
219
+ spanId: SPAN_IDS.apply,
220
+ label: 'Applying migration plan across spaces',
221
+ });
222
+
223
+ const perSpaceOptions: MultiSpaceRunnerPerSpaceOptions<TFamilyId, TTargetId>[] =
224
+ orderedResolutions.map((r) => ({
225
+ space: r.spaceId,
226
+ plan: r.entry.plan,
227
+ driver,
228
+ destinationContract: r.entry.destinationContract,
229
+ policy,
230
+ executionChecks: { prechecks: false, postchecks: false, idempotencyChecks: false },
231
+ frameworkComponents,
232
+ // Per-space post-apply schema verification is non-strict: each
233
+ // space's `destinationContract` describes only its own slice; a
234
+ // strict verifier would treat every other space's tables as
235
+ // `extras`. Tolerant mode still catches missing tables / columns.
236
+ // SQL family runners read `strictVerification` via structural
237
+ // typing.
238
+ strictVerification: false,
239
+ })) as MultiSpaceRunnerPerSpaceOptions<TFamilyId, TTargetId>[];
240
+
241
+ const runnerResult = await (
242
+ runner as MultiSpaceCapableRunner<TFamilyId, TTargetId>
243
+ ).executeAcrossSpaces({ driver, perSpaceOptions });
244
+
245
+ if (!runnerResult.ok) {
246
+ onProgress?.({ action, kind: 'spanEnd', spanId: SPAN_IDS.apply, outcome: 'error' });
247
+ return buildRunnerFailure({
248
+ summary: runnerResult.failure.summary,
249
+ ...ifDefined('why', runnerResult.failure.why),
250
+ meta: {
251
+ ...(runnerResult.failure.meta ?? {}),
252
+ failingSpace: runnerResult.failure.failingSpace,
253
+ },
254
+ });
255
+ }
256
+ onProgress?.({ action, kind: 'spanEnd', spanId: SPAN_IDS.apply, outcome: 'ok' });
257
+
258
+ const totalOpsPlanned = runnerResult.value.perSpaceResults.reduce(
259
+ (sum, r) => sum + r.value.operationsPlanned,
260
+ 0,
261
+ );
262
+ const totalOpsExecuted = runnerResult.value.perSpaceResults.reduce(
263
+ (sum, r) => sum + r.value.operationsExecuted,
264
+ 0,
265
+ );
266
+
267
+ const aggregateOps = orderedResolutions.flatMap((r) => r.entry.displayOps);
268
+ const summary =
269
+ action === 'dbInit'
270
+ ? `Applied ${totalOpsExecuted} operation(s) across ${orderedResolutions.length} space(s), database signed`
271
+ : totalOpsExecuted === 0
272
+ ? `Database already matches contract across ${orderedResolutions.length} space(s), signature updated`
273
+ : `Applied ${totalOpsExecuted} operation(s) across ${orderedResolutions.length} space(s), signature updated`;
274
+
275
+ return wrapApplyResult({
276
+ operations: aggregateOps,
277
+ destination: appPlan.destination,
278
+ operationsPlanned: totalOpsPlanned,
279
+ operationsExecuted: totalOpsExecuted,
280
+ summary,
281
+ });
282
+ }
283
+
284
+ /**
285
+ * Compare the live `_prisma_marker` rows against the aggregate's
286
+ * declared members. Any marker row whose `space` is not a member of
287
+ * the aggregate is an "orphan" — typically a marker left behind by
288
+ * an extension that was removed from `extensionPacks` without first
289
+ * cleaning up its on-disk migrations / database tables.
290
+ *
291
+ * Returns a {@link CliStructuredError} envelope (code `5002`,
292
+ * `kind: 'orphanMarker'`) for the first orphan it finds, or `null`
293
+ * when every marker row maps to a declared member. Mirrors the M2
294
+ * `runContractSpaceVerifierMarkerCheck` envelope so downstream
295
+ * tooling (integration tests, JSON consumers) keeps asserting on the
296
+ * same shape.
297
+ */
298
+ function detectOrphanMarkers(
299
+ aggregate: ContractSpaceAggregate,
300
+ markerRows: ReadonlyMap<string, unknown>,
301
+ ): CliStructuredError | null {
302
+ const memberSpaceIds = new Set<string>([
303
+ aggregate.app.spaceId,
304
+ ...aggregate.extensions.map((m) => m.spaceId),
305
+ ]);
306
+ const orphans: string[] = [];
307
+ for (const [spaceId, row] of markerRows) {
308
+ if (row !== null && row !== undefined && !memberSpaceIds.has(spaceId)) {
309
+ orphans.push(spaceId);
310
+ }
311
+ }
312
+ if (orphans.length === 0) return null;
313
+ orphans.sort((a, b) => a.localeCompare(b));
314
+ const summary =
315
+ orphans.length === 1
316
+ ? `Orphan contract-space marker detected for "${orphans[0]}"`
317
+ : `Orphan contract-space markers detected for ${orphans.length} spaces`;
318
+ return new CliStructuredError('5002', summary, {
319
+ domain: 'MIG',
320
+ why: `The database has \`_prisma_marker\` rows for spaces (${orphans
321
+ .map((s) => `"${s}"`)
322
+ .join(
323
+ ', ',
324
+ )}) that are not declared in the project's \`extensionPacks\`. The aggregate pipeline refuses to advance markers it cannot account for.`,
325
+ 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.',
326
+ docsUrl: 'https://pris.ly/contract-spaces',
327
+ meta: {
328
+ violations: orphans.map((spaceId) => ({ kind: 'orphanMarker', spaceId })),
329
+ },
330
+ });
331
+ }
332
+
333
+ interface OrderedResolution {
334
+ readonly spaceId: string;
335
+ readonly entry: AggregatePerSpacePlan;
336
+ }
337
+
338
+ function collectOrdered(
339
+ applyOrder: readonly string[],
340
+ perSpace: ReadonlyMap<string, AggregatePerSpacePlan>,
341
+ ): readonly OrderedResolution[] {
342
+ return applyOrder.map((spaceId) => {
343
+ const entry = perSpace.get(spaceId);
344
+ if (!entry) {
345
+ throw new Error(`Aggregate planner output missing per-space plan for "${spaceId}"`);
346
+ }
347
+ return { spaceId, entry };
348
+ });
349
+ }
350
+
351
+ function mapPlannerError(error: AggregatePlannerError): DbInitResult | DbUpdateResult {
352
+ if (error.kind === 'appSynthFailure') {
353
+ const failure: DbInitFailure | DbUpdateFailure = {
354
+ code: 'PLANNING_FAILED',
355
+ summary: 'Migration planning failed due to conflicts',
356
+ conflicts: error.conflicts,
357
+ why: undefined,
358
+ meta: undefined,
359
+ };
360
+ return notOk(failure) as DbInitResult | DbUpdateResult;
361
+ }
362
+ if (error.kind === 'extensionPathUnreachable') {
363
+ return buildRunnerFailure({
364
+ summary: `Cannot resolve apply path for extension space "${error.spaceId}"`,
365
+ why: `No path in the on-disk migration graph for extension space "${error.spaceId}" reaches the on-disk head ref hash "${error.target}".`,
366
+ meta: { spaceId: error.spaceId, target: error.target },
367
+ });
368
+ }
369
+ if (error.kind === 'extensionPathUnsatisfiable') {
370
+ return buildRunnerFailure({
371
+ summary: `Cannot resolve apply path for extension space "${error.spaceId}"`,
372
+ 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(', ')}.`,
373
+ meta: { spaceId: error.spaceId, missingInvariants: error.missingInvariants },
374
+ });
375
+ }
376
+ // policyConflict — surfaces as a runner-style failure naming the
377
+ // space; conceptually a configuration bug, but mapping it onto the
378
+ // existing failure surface keeps callers untouched.
379
+ return buildRunnerFailure({
380
+ summary: `Aggregate planner policy conflict for space "${error.spaceId}"`,
381
+ why: error.detail,
382
+ meta: { spaceId: error.spaceId },
383
+ });
384
+ }
385
+
386
+ function wrapPlanResult(args: {
387
+ readonly operations: readonly MigrationPlanOperation[];
388
+ readonly destination: { readonly storageHash: string; readonly profileHash?: string };
389
+ readonly preview: OperationPreview | undefined;
390
+ readonly summary: string;
391
+ }): DbInitResult | DbUpdateResult {
392
+ const success: DbInitSuccess | DbUpdateSuccess = {
393
+ mode: 'plan',
394
+ plan: {
395
+ operations: stripOperations(args.operations),
396
+ ...ifDefined('preview', args.preview),
397
+ },
398
+ destination: {
399
+ storageHash: args.destination.storageHash,
400
+ ...ifDefined('profileHash', args.destination.profileHash),
401
+ },
402
+ summary: args.summary,
403
+ };
404
+ return ok(success);
405
+ }
406
+
407
+ function wrapApplyResult(args: {
408
+ readonly operations: readonly MigrationPlanOperation[];
409
+ readonly destination: { readonly storageHash: string; readonly profileHash?: string };
410
+ readonly operationsPlanned: number;
411
+ readonly operationsExecuted: number;
412
+ readonly summary: string;
413
+ }): DbInitResult | DbUpdateResult {
414
+ const success: DbInitSuccess | DbUpdateSuccess = {
415
+ mode: 'apply',
416
+ plan: { operations: stripOperations(args.operations) },
417
+ destination: {
418
+ storageHash: args.destination.storageHash,
419
+ ...ifDefined('profileHash', args.destination.profileHash),
420
+ },
421
+ execution: {
422
+ operationsPlanned: args.operationsPlanned,
423
+ operationsExecuted: args.operationsExecuted,
424
+ },
425
+ marker: args.destination.profileHash
426
+ ? { storageHash: args.destination.storageHash, profileHash: args.destination.profileHash }
427
+ : { storageHash: args.destination.storageHash },
428
+ summary: args.summary,
429
+ };
430
+ return ok(success);
431
+ }
432
+
433
+ function buildRunnerFailure(args: {
434
+ readonly summary: string;
435
+ readonly why?: string;
436
+ readonly meta: Record<string, unknown>;
437
+ }): DbInitResult | DbUpdateResult {
438
+ const failure: DbInitFailure | DbUpdateFailure = {
439
+ code: 'RUNNER_FAILED',
440
+ summary: args.summary,
441
+ why: args.why,
442
+ meta: args.meta,
443
+ conflicts: undefined,
444
+ };
445
+ return notOk(failure) as DbInitResult | DbUpdateResult;
446
+ }