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

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 +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 +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
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Re-export all domain error factories from @prisma-next/errors for convenience.
3
- * CLI-specific errors (e.g., Commander.js argument validation) can be added here if needed.
3
+ * CLI-specific errors (e.g., Commander argument validation in the main CLI, or
4
+ * clipanion parse errors in the migration-file CLI) can be added here if needed.
4
5
  */
5
6
  export type { CliErrorConflict, CliErrorEnvelope } from '@prisma-next/errors/control';
6
- export {
7
+
8
+ import {
7
9
  CliStructuredError,
8
10
  errorConfigFileNotFound,
9
11
  errorConfigValidation,
@@ -15,11 +17,33 @@ export {
15
17
  errorFamilyReadMarkerSqlRequired,
16
18
  errorFileNotFound,
17
19
  errorMigrationCliInvalidConfigArg,
20
+ errorMigrationCliUnknownFlag,
18
21
  errorMigrationPlanningFailed,
19
22
  errorQueryRunnerFactoryRequired,
20
23
  errorTargetMigrationNotSupported,
21
24
  errorUnexpected,
22
25
  } from '@prisma-next/errors/control';
26
+ import { errorRuntime } from '@prisma-next/errors/execution';
27
+ import type { MigrationToolsError } from '@prisma-next/migration-tools/errors';
28
+
29
+ export {
30
+ CliStructuredError,
31
+ errorConfigFileNotFound,
32
+ errorConfigValidation,
33
+ errorContractConfigMissing,
34
+ errorContractMissingExtensionPacks,
35
+ errorContractValidationFailed,
36
+ errorDatabaseConnectionRequired,
37
+ errorDriverRequired,
38
+ errorFamilyReadMarkerSqlRequired,
39
+ errorFileNotFound,
40
+ errorMigrationCliInvalidConfigArg,
41
+ errorMigrationCliUnknownFlag,
42
+ errorMigrationPlanningFailed,
43
+ errorQueryRunnerFactoryRequired,
44
+ errorTargetMigrationNotSupported,
45
+ errorUnexpected,
46
+ };
23
47
  export {
24
48
  ERROR_CODE_DESTRUCTIVE_CHANGES,
25
49
  errorDestructiveChanges,
@@ -38,3 +62,26 @@ export {
38
62
  errorUnfilledPlaceholder,
39
63
  placeholder,
40
64
  } from '@prisma-next/errors/migration';
65
+
66
+ /**
67
+ * Maps a `MigrationToolsError` raised by the migration-tools loader/graph
68
+ * surface (`readMigrationPackage`, `readMigrationsDir`, `readRefs`,
69
+ * `resolveRef`, `reconstructGraph`, ...) into a CLI `errorRuntime` envelope.
70
+ *
71
+ * The full `error.details` payload is forwarded into `meta` so machine
72
+ * consumers (`--json`) see structural fields like `dir`, `storedHash`,
73
+ * `computedHash` (for `MIGRATION.HASH_MISMATCH`) alongside the stable
74
+ * `code`. The user-visible `summary`/`why`/`fix` text is unchanged.
75
+ *
76
+ * Callers are expected to gate on `MigrationToolsError.is(error)` first
77
+ * (mirroring the original inline pattern); non-`MigrationToolsError`
78
+ * values are caller-classified (rethrow, wrap with command-specific
79
+ * `errorUnexpected`, etc.).
80
+ */
81
+ export function mapMigrationToolsError(error: MigrationToolsError): CliStructuredError {
82
+ return errorRuntime(error.message, {
83
+ why: error.why,
84
+ fix: error.fix,
85
+ meta: { code: error.code, ...(error.details ?? {}) },
86
+ });
87
+ }
@@ -0,0 +1,84 @@
1
+ import type { VerifyDatabaseSchemaResult } from '@prisma-next/framework-components/control';
2
+
3
+ /**
4
+ * Collapse the aggregate verifier's per-space schema results into a
5
+ * single {@link VerifyDatabaseSchemaResult} for the existing CLI
6
+ * display surface. Concatenates issues across members; sums counts;
7
+ * uses the app member's result as the structural envelope (storage
8
+ * hash, target).
9
+ *
10
+ * **Summary policy.** Preserve the per-family phrasing whenever the
11
+ * combined `ok` flag agrees with the app member's `ok` flag — this is
12
+ * the common case (single-family deployments, single-app deployments)
13
+ * and the family's "satisfies / does not satisfy contract" phrasing
14
+ * stays user-visible. When the app passes but an extension fails (or
15
+ * vice versa) the app's summary contradicts the envelope, so fall back
16
+ * to the first failing member's summary. This keeps family phrasing
17
+ * intact and the envelope internally consistent (`ok: false` ↔ failure
18
+ * summary).
19
+ */
20
+ export function combineSchemaResults(
21
+ perSpace: ReadonlyMap<string, VerifyDatabaseSchemaResult>,
22
+ appSpaceId: string,
23
+ strict: boolean,
24
+ ): VerifyDatabaseSchemaResult {
25
+ const appResult = perSpace.get(appSpaceId) ?? perSpace.values().next().value;
26
+ if (appResult === undefined) {
27
+ throw new Error('Aggregate verifier returned no schema results — this is a wiring bug.');
28
+ }
29
+
30
+ let okAll = true;
31
+ let firstFailure: VerifyDatabaseSchemaResult | undefined;
32
+ let issues: VerifyDatabaseSchemaResult['schema']['issues'] = [];
33
+ const counts = { pass: 0, warn: 0, fail: 0, totalNodes: 0 };
34
+ const childRoots: Array<VerifyDatabaseSchemaResult['schema']['root']> = [];
35
+ for (const [, result] of perSpace) {
36
+ if (!result.ok) {
37
+ okAll = false;
38
+ if (firstFailure === undefined) firstFailure = result;
39
+ }
40
+ issues = [...issues, ...result.schema.issues];
41
+ counts.pass += result.schema.counts.pass;
42
+ counts.warn += result.schema.counts.warn;
43
+ counts.fail += result.schema.counts.fail;
44
+ counts.totalNodes += result.schema.counts.totalNodes;
45
+ childRoots.push(result.schema.root);
46
+ }
47
+
48
+ // When `okAll !== appResult.ok`, exactly one shape is reachable: app passes
49
+ // (`appResult.ok === true`) and at least one other member failed
50
+ // (`okAll === false`). In that shape the failure was assigned to
51
+ // `firstFailure` during iteration, so non-null assertion is safe. The mirror
52
+ // shape (app fails while every member passes) is impossible because
53
+ // `appResult` either *is* a member of `perSpace` or is the first iterator
54
+ // value; either way its `ok` flag participates in `okAll`.
55
+ const summary =
56
+ okAll === appResult.ok
57
+ ? appResult.summary
58
+ : (firstFailure as VerifyDatabaseSchemaResult).summary;
59
+
60
+ return {
61
+ ok: okAll,
62
+ ...(okAll ? {} : { code: appResult.code ?? 'PN-RUN-3010' }),
63
+ summary,
64
+ contract: appResult.contract,
65
+ target: appResult.target,
66
+ schema: {
67
+ issues,
68
+ root: {
69
+ status: okAll ? 'pass' : 'fail',
70
+ kind: 'aggregate',
71
+ name: 'aggregate',
72
+ contractPath: '',
73
+ code: 'AGGREGATE',
74
+ message: okAll ? 'Aggregate schema matches' : 'Aggregate schema mismatch',
75
+ expected: undefined,
76
+ actual: undefined,
77
+ children: childRoots,
78
+ },
79
+ counts,
80
+ },
81
+ meta: { strict },
82
+ timings: { total: 0 },
83
+ };
84
+ }
@@ -1,10 +1,13 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import type { ControlTargetDescriptor } from '@prisma-next/framework-components/control';
3
3
  import { hasMigrations } from '@prisma-next/framework-components/control';
4
- import type { PathDecision } from '@prisma-next/migration-tools/dag';
5
- import { reconstructGraph } from '@prisma-next/migration-tools/dag';
4
+ import type { NoInvariantPathStructuralEdge } from '@prisma-next/migration-tools/errors';
5
+ import type { MigrationEdge, MigrationGraph } from '@prisma-next/migration-tools/graph';
6
6
  import { readMigrationsDir } from '@prisma-next/migration-tools/io';
7
- import type { MigrationBundle, MigrationGraph } from '@prisma-next/migration-tools/types';
7
+ import type { PathDecision } from '@prisma-next/migration-tools/migration-graph';
8
+ import { reconstructGraph } from '@prisma-next/migration-tools/migration-graph';
9
+ import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
10
+ import { APP_SPACE_ID, spaceMigrationDirectory } from '@prisma-next/migration-tools/spaces';
8
11
  import { ifDefined } from '@prisma-next/utils/defined';
9
12
  import type { Command } from 'commander';
10
13
  import { relative, resolve } from 'pathe';
@@ -77,6 +80,16 @@ export function resolveContractPath(config: { contract?: { output?: string } }):
77
80
  /**
78
81
  * Resolves the migrations directory and config path from CLI options.
79
82
  * Shared by migration-apply, migration-plan, and migration-status.
83
+ *
84
+ * - `migrationsDir` is the project's top-level `migrations/` directory
85
+ * (the root that the aggregate loader walks for every contract space).
86
+ * - `appMigrationsDir` is the app subspace directory under it
87
+ * (`<migrationsDir>/<APP_SPACE_ID>/`). Every per-app reader / writer
88
+ * (`migration new`, `migration plan`, `migration apply`,
89
+ * `migration status`, `migration show`, `migration ref`) operates on
90
+ * this directory. Extensions own their own `migrations/<spaceId>/`.
91
+ * - `refsDir` is the app's refs directory (`<appMigrationsDir>/refs/`).
92
+ * The framework does not maintain refs at the migrations root.
80
93
  */
81
94
  export function resolveMigrationPaths(
82
95
  configOption: string | undefined,
@@ -85,6 +98,8 @@ export function resolveMigrationPaths(
85
98
  configPath: string;
86
99
  migrationsDir: string;
87
100
  migrationsRelative: string;
101
+ appMigrationsDir: string;
102
+ appMigrationsRelative: string;
88
103
  refsDir: string;
89
104
  } {
90
105
  const configPath = configOption
@@ -95,8 +110,17 @@ export function resolveMigrationPaths(
95
110
  config.migrations?.dir ?? 'migrations',
96
111
  );
97
112
  const migrationsRelative = relative(process.cwd(), migrationsDir);
98
- const refsDir = resolve(migrationsDir, 'refs');
99
- return { configPath, migrationsDir, migrationsRelative, refsDir };
113
+ const appMigrationsDir = spaceMigrationDirectory(migrationsDir, APP_SPACE_ID);
114
+ const appMigrationsRelative = relative(process.cwd(), appMigrationsDir);
115
+ const refsDir = resolve(appMigrationsDir, 'refs');
116
+ return {
117
+ configPath,
118
+ migrationsDir,
119
+ migrationsRelative,
120
+ appMigrationsDir,
121
+ appMigrationsRelative,
122
+ refsDir,
123
+ };
100
124
  }
101
125
 
102
126
  /**
@@ -109,14 +133,45 @@ export interface PathDecisionResult {
109
133
  readonly alternativeCount: number;
110
134
  readonly tieBreakReasons: readonly string[];
111
135
  readonly refName?: string;
136
+ readonly requiredInvariants: readonly string[];
137
+ readonly satisfiedInvariants: readonly string[];
112
138
  readonly selectedPath: readonly {
113
139
  readonly dirName: string;
114
- readonly migrationId: string;
140
+ readonly migrationHash: string;
115
141
  readonly from: string;
116
142
  readonly to: string;
143
+ readonly invariants: readonly string[];
117
144
  }[];
118
145
  }
119
146
 
147
+ export function collectDeclaredInvariants(graph: MigrationGraph): ReadonlySet<string> {
148
+ const declared = new Set<string>();
149
+ for (const edges of graph.forwardChain.values()) {
150
+ for (const edge of edges) {
151
+ for (const inv of edge.invariants) {
152
+ declared.add(inv);
153
+ }
154
+ }
155
+ }
156
+ return declared;
157
+ }
158
+
159
+ /**
160
+ * Maps a `MigrationEdge` to the structural-edge shape used in the
161
+ * `MIGRATION.NO_INVARIANT_PATH` error envelope. Shared between
162
+ * `migration apply` and `migration status` so both commands surface
163
+ * the same JSON wire shape when an invariant-aware route is unsatisfiable.
164
+ */
165
+ export function toStructuralEdge(edge: MigrationEdge): NoInvariantPathStructuralEdge {
166
+ return {
167
+ dirName: edge.dirName,
168
+ migrationHash: edge.migrationHash,
169
+ from: edge.from,
170
+ to: edge.to,
171
+ invariants: edge.invariants,
172
+ };
173
+ }
174
+
120
175
  /**
121
176
  * Maps a PathDecision to the slim CLI output representation.
122
177
  */
@@ -126,12 +181,15 @@ export function toPathDecisionResult(decision: PathDecision): PathDecisionResult
126
181
  toHash: decision.toHash,
127
182
  alternativeCount: decision.alternativeCount,
128
183
  tieBreakReasons: decision.tieBreakReasons,
184
+ requiredInvariants: decision.requiredInvariants ?? [],
185
+ satisfiedInvariants: decision.satisfiedInvariants ?? [],
129
186
  ...ifDefined('refName', decision.refName),
130
187
  selectedPath: decision.selectedPath.map((entry) => ({
131
188
  dirName: entry.dirName,
132
- migrationId: entry.migrationId,
189
+ migrationHash: entry.migrationHash,
133
190
  from: entry.from,
134
191
  to: entry.to,
192
+ invariants: entry.invariants,
135
193
  })),
136
194
  };
137
195
  }
@@ -146,13 +204,13 @@ export function getTargetMigrations(target: ControlTargetDescriptor<string, stri
146
204
 
147
205
  /**
148
206
  * Reads the migrations directory and builds the migration graph from all
149
- * bundles. Throws on I/O or graph errors — callers handle error mapping.
207
+ * packages. Throws on I/O or graph errors — callers handle error mapping.
150
208
  *
151
- * Every on-disk bundle is content-addressed (`migrationId` is always a
209
+ * Every on-disk package is content-addressed (`migrationHash` is always a
152
210
  * string); there is no draft state to filter out.
153
211
  */
154
- export async function loadMigrationBundles(migrationsDir: string): Promise<{
155
- bundles: readonly MigrationBundle[];
212
+ export async function loadMigrationPackages(migrationsDir: string): Promise<{
213
+ bundles: readonly OnDiskMigrationPackage[];
156
214
  graph: MigrationGraph;
157
215
  }> {
158
216
  const bundles = await readMigrationsDir(migrationsDir);
@@ -160,20 +218,6 @@ export async function loadMigrationBundles(migrationsDir: string): Promise<{
160
218
  return { bundles, graph };
161
219
  }
162
220
 
163
- export interface MigrationBundleSet {
164
- readonly bundles: readonly MigrationBundle[];
165
- readonly graph: MigrationGraph;
166
- }
167
-
168
- /**
169
- * Alias of `loadMigrationBundles` retained for naming-clarity in commands
170
- * that previously needed both attested and draft splits. With the
171
- * collapse of the draft state, both helpers do the same thing.
172
- */
173
- export async function loadAllBundles(migrationsDir: string): Promise<MigrationBundleSet> {
174
- return loadMigrationBundles(migrationsDir);
175
- }
176
-
177
221
  /**
178
222
  * The subset of the emitted contract.json that the framework layer can
179
223
  * safely type. The emitter adds these fields on top of the family-specific
@@ -0,0 +1,204 @@
1
+ import type { Contract } from '@prisma-next/contract/types';
2
+ import type { ControlExtensionDescriptor } from '@prisma-next/framework-components/control';
3
+ import type {
4
+ ContractSpaceAggregate,
5
+ LoadAggregateError,
6
+ LoadAggregateInput,
7
+ LoadAggregateOutput,
8
+ } from '@prisma-next/migration-tools/aggregate';
9
+ import { loadContractSpaceAggregate } from '@prisma-next/migration-tools/aggregate';
10
+ import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
11
+ import { notOk, ok, type Result } from '@prisma-next/utils/result';
12
+ import { CliStructuredError } from './cli-errors';
13
+ import { toDeclaredExtensions, toExtensionInputs } from './extension-pack-inputs';
14
+
15
+ /**
16
+ * Render a {@link LoadAggregateError} into a CLI structured-error
17
+ * envelope. Preserves error codes `5001` (layout) and `5002` (marker /
18
+ * drift / disjointness / etc.) so existing integration tests and
19
+ * downstream tooling continue to assert on the same `meta.violations[]`
20
+ * shape they did under the old precheck/marker-check helpers.
21
+ */
22
+ export function mapLoadAggregateError(error: LoadAggregateError): CliStructuredError {
23
+ if (error.kind === 'layoutViolation') {
24
+ const lines = error.violations.map((v) => `- [${v.kind}] ${v.spaceId}`);
25
+ const summary =
26
+ error.violations.length === 1
27
+ ? 'Contract-space layout violation detected'
28
+ : `Contract-space layout violations detected (${error.violations.length})`;
29
+ return new CliStructuredError('5001', summary, {
30
+ domain: 'MIG',
31
+ why: `The on-disk \`migrations/\` directory and your \`extensionPacks\` declaration are not in agreement.\n${lines.join('\n')}`,
32
+ fix: 'Run `prisma-next migrate` to materialise on-disk artefacts for declared extensions, or remove the orphan directory.',
33
+ docsUrl: 'https://pris.ly/contract-spaces',
34
+ meta: {
35
+ violations: error.violations.map((v) => ({
36
+ kind: v.kind,
37
+ spaceId: v.spaceId,
38
+ })),
39
+ },
40
+ });
41
+ }
42
+ if (error.kind === 'driftViolation') {
43
+ return new CliStructuredError('5002', `Contract-space drift detected for "${error.spaceId}"`, {
44
+ domain: 'MIG',
45
+ why: `The on-disk contract for space "${error.spaceId}" (hash ${error.priorHeadHash}) does not match the live extension descriptor (hash ${error.liveHash}).`,
46
+ fix: 'Run `prisma-next migrate` to refresh the on-disk artefacts to match the live descriptor.',
47
+ docsUrl: 'https://pris.ly/contract-spaces',
48
+ meta: {
49
+ violations: [
50
+ {
51
+ kind: 'drift',
52
+ spaceId: error.spaceId,
53
+ priorHeadHash: error.priorHeadHash,
54
+ liveHash: error.liveHash,
55
+ },
56
+ ],
57
+ },
58
+ });
59
+ }
60
+ if (error.kind === 'disjointnessViolation') {
61
+ return new CliStructuredError(
62
+ '5002',
63
+ `Contract-space disjointness violation: storage element "${error.element}" claimed by multiple spaces`,
64
+ {
65
+ domain: 'MIG',
66
+ why: `Spaces ${error.claimedBy.map((s) => `"${s}"`).join(', ')} all claim the storage element "${error.element}". Each storage element must be owned by exactly one contract space.`,
67
+ fix: 'Update the conflicting contracts so each storage element is claimed by exactly one space.',
68
+ docsUrl: 'https://pris.ly/contract-spaces',
69
+ meta: {
70
+ violations: [
71
+ {
72
+ kind: 'disjointness',
73
+ spaceId: error.claimedBy.join(','),
74
+ element: error.element,
75
+ claimedBy: error.claimedBy,
76
+ },
77
+ ],
78
+ },
79
+ },
80
+ );
81
+ }
82
+ if (error.kind === 'integrityFailure') {
83
+ return new CliStructuredError(
84
+ '5002',
85
+ `Contract-space integrity failure for "${error.spaceId}"`,
86
+ {
87
+ domain: 'MIG',
88
+ why: error.detail,
89
+ fix: 'Run `prisma-next migrate` to refresh on-disk artefacts, or restore the on-disk `migrations/` directory from version control.',
90
+ docsUrl: 'https://pris.ly/contract-spaces',
91
+ meta: {
92
+ violations: [{ kind: 'integrity', spaceId: error.spaceId, detail: error.detail }],
93
+ },
94
+ },
95
+ );
96
+ }
97
+ if (error.kind === 'validationFailure') {
98
+ return new CliStructuredError(
99
+ '5002',
100
+ `Contract-space contract validation failed for "${error.spaceId}"`,
101
+ {
102
+ domain: 'MIG',
103
+ why: error.detail,
104
+ fix: 'Run `prisma-next migrate` to refresh on-disk artefacts, or fix the extension descriptor producing the invalid contract.',
105
+ meta: {
106
+ violations: [{ kind: 'validation', spaceId: error.spaceId, detail: error.detail }],
107
+ },
108
+ },
109
+ );
110
+ }
111
+ // targetMismatch
112
+ return new CliStructuredError('5002', `Contract-space target mismatch for "${error.spaceId}"`, {
113
+ domain: 'MIG',
114
+ why: `Space "${error.spaceId}" targets "${error.actual}" but the project's adapter targets "${error.expected}".`,
115
+ fix: 'Update the extension descriptor to target the configured database, or change the project adapter.',
116
+ meta: {
117
+ violations: [
118
+ {
119
+ kind: 'targetMismatch',
120
+ spaceId: error.spaceId,
121
+ expected: error.expected,
122
+ actual: error.actual,
123
+ },
124
+ ],
125
+ },
126
+ });
127
+ }
128
+
129
+ /**
130
+ * Inputs needed to compose the aggregate loader at the CLI surface.
131
+ *
132
+ * Keeps the loader framework-neutral (no `Config` import) by accepting
133
+ * already-resolved structural inputs: validated app contract, target
134
+ * id, migrations root directory, and the set of extension descriptors.
135
+ */
136
+ export interface BuildAggregateInputs<TFamilyId extends string, TTargetId extends string> {
137
+ readonly targetId: TTargetId;
138
+ readonly migrationsDir: string;
139
+ readonly appContract: Contract;
140
+ readonly extensionPacks: ReadonlyArray<ControlExtensionDescriptor<TFamilyId, TTargetId>>;
141
+ readonly validateContract: (contractJson: unknown) => Contract;
142
+ /**
143
+ * App-space migration packages to hydrate the app member's
144
+ * migration graph with. Defaults to `[]` (matches the `db init` /
145
+ * `db update` daily-driver behaviour, where the app's authored
146
+ * `migrations/` graph is not walked — the planner uses the synth
147
+ * strategy for the app member instead).
148
+ *
149
+ * `migration apply` callers thread the user's authored app-space
150
+ * packages (loaded via `loadMigrationPackages(appMigrationsDir)`)
151
+ * through here so the graph-walk strategy can plot a path through
152
+ * them — the prod-time replay path explicitly forbids synth.
153
+ */
154
+ readonly appMigrationPackages?: ReadonlyArray<OnDiskMigrationPackage>;
155
+ }
156
+
157
+ /**
158
+ * Run the aggregate loader at the CLI surface, mapping any
159
+ * {@link LoadAggregateError} into a {@link CliStructuredError} envelope.
160
+ *
161
+ * App-side migration packages flow through `inputs.appMigrationPackages`
162
+ * (defaulting to `[]`). `db init` / `db update` leave it empty: the
163
+ * planner's `synth` strategy is used for the app member (driven by
164
+ * `callerPolicy.ignoreGraphFor`), so the app's authored `migrations/`
165
+ * graph does not need to be walked. `migration apply` threads the
166
+ * already-loaded app-space packages through so the graph-walk strategy
167
+ * can plot a path through them — replay forbids synth.
168
+ *
169
+ * @see specs/contract-space-aggregate-spec.md § Loader.
170
+ */
171
+ export async function buildContractSpaceAggregate<
172
+ TFamilyId extends string,
173
+ TTargetId extends string,
174
+ >(
175
+ inputs: BuildAggregateInputs<TFamilyId, TTargetId>,
176
+ ): Promise<Result<ContractSpaceAggregate, CliStructuredError>> {
177
+ const { entries, hashByContractJson } = toDeclaredExtensions(
178
+ toExtensionInputs(inputs.extensionPacks),
179
+ );
180
+
181
+ const loadInput: LoadAggregateInput = {
182
+ targetId: inputs.targetId,
183
+ migrationsDir: inputs.migrationsDir,
184
+ appContract: inputs.appContract,
185
+ declaredExtensions: entries,
186
+ validateContract: inputs.validateContract,
187
+ hashContract: (contractJson: unknown) => {
188
+ const precomputed = hashByContractJson.get(contractJson);
189
+ if (precomputed === undefined) {
190
+ throw new Error(
191
+ 'CLI aggregate loader: encountered an extension contract without a pre-computed descriptor hash. This is a wiring bug.',
192
+ );
193
+ }
194
+ return precomputed;
195
+ },
196
+ appMigrationPackages: inputs.appMigrationPackages ?? [],
197
+ };
198
+
199
+ const result: LoadAggregateOutput = await loadContractSpaceAggregate(loadInput);
200
+ if (!result.ok) {
201
+ return notOk(mapLoadAggregateError(result.failure));
202
+ }
203
+ return ok(result.value.aggregate);
204
+ }
@@ -0,0 +1,120 @@
1
+ import { materialiseExtensionMigrationPackageIfMissing } from '@prisma-next/migration-tools/io';
2
+ import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
3
+ import type { MigrationOps } from '@prisma-next/migration-tools/package';
4
+ import {
5
+ planAllSpaces,
6
+ type SpacePlanOutput,
7
+ spaceMigrationDirectory,
8
+ } from '@prisma-next/migration-tools/spaces';
9
+
10
+ /**
11
+ * In-memory authored migration package shipped by an extension descriptor.
12
+ * Mirrors `MigrationPackage` from `@prisma-next/migration-tools/io`
13
+ * (the on-disk shape minus `dirPath`); redeclared structurally here so
14
+ * the CLI helper does not couple to the SQL family's `ExtensionMigrationPackage`
15
+ * type — any family that ships pre-built migration packages can pass them
16
+ * through unchanged.
17
+ */
18
+ export interface DescriptorMigrationPackage {
19
+ readonly dirName: string;
20
+ readonly metadata: MigrationMetadata;
21
+ readonly ops: MigrationOps;
22
+ }
23
+
24
+ /**
25
+ * Minimal descriptor view consumed by the migration-materialisation pass.
26
+ * Mirrors {@link import('./contract-space-migrate-pass').MigrateExtensionInput}
27
+ * but adds the `migrations` field — the canonical set of pre-built
28
+ * migration packages the extension ships.
29
+ */
30
+ export interface ExtensionMigrationsExtensionInput {
31
+ readonly id: string;
32
+ readonly contractSpace?: {
33
+ readonly contractJson: unknown;
34
+ readonly migrations: readonly DescriptorMigrationPackage[];
35
+ readonly headRef: { readonly hash: string; readonly invariants: readonly string[] };
36
+ };
37
+ }
38
+
39
+ export interface ContractSpaceExtensionMigrationsPassInputs {
40
+ readonly migrationsDir: string;
41
+ readonly extensionPacks: ReadonlyArray<ExtensionMigrationsExtensionInput>;
42
+ }
43
+
44
+ export interface ContractSpaceExtensionMigrationsPassResult {
45
+ readonly emitted: readonly { readonly spaceId: string; readonly dirName: string }[];
46
+ readonly skipped: readonly { readonly spaceId: string; readonly dirName: string }[];
47
+ }
48
+
49
+ /**
50
+ * Materialise an extension's pre-built migration packages onto disk
51
+ * under `migrations/<spaceId>/<dirName>/` for every package that does
52
+ * not yet exist there.
53
+ *
54
+ * Helper-location pattern — the per-space "planner" for extension
55
+ * spaces is a no-op that just returns the descriptor's `migrations`
56
+ * verbatim; the value `planAllSpaces` brings to this consumer site is
57
+ * **deterministic ordering** (alphabetical by spaceId) and
58
+ * **duplicate-spaceId detection**. The actual write is performed via
59
+ * `materialiseMigrationPackage` per package.
60
+ *
61
+ * Idempotent: an existing `migrations/<spaceId>/<dirName>/` is left
62
+ * untouched and reported in `result.skipped` — the helper never
63
+ * overwrites authored migration content, ensuring re-running
64
+ * `migrate` does not corrupt or churn extension migration packages.
65
+ *
66
+ * Per-space artefacts (`contract.json`, `contract.d.ts`,
67
+ * `refs/head.json`) are emitted by
68
+ * {@link import('./contract-space-migrate-pass').runContractSpaceMigratePass}
69
+ * separately — they cover the head-pointer side of the ledger. This
70
+ * helper covers the migration-graph side.
71
+ */
72
+ export async function runContractSpaceExtensionMigrationsPass(
73
+ inputs: ContractSpaceExtensionMigrationsPassInputs,
74
+ ): Promise<ContractSpaceExtensionMigrationsPassResult> {
75
+ const planInputs = inputs.extensionPacks
76
+ .filter(
77
+ (
78
+ pack,
79
+ ): pack is ExtensionMigrationsExtensionInput & {
80
+ contractSpace: NonNullable<ExtensionMigrationsExtensionInput['contractSpace']>;
81
+ } => pack.contractSpace !== undefined,
82
+ )
83
+ .map((pack) => ({
84
+ spaceId: pack.id,
85
+ priorContract: null,
86
+ newContract: pack.contractSpace.contractJson,
87
+ __migrations: pack.contractSpace.migrations,
88
+ }));
89
+
90
+ // Threading the descriptor's pre-built migrations into the
91
+ // `planAllSpaces` callback by piggybacking on the input shape.
92
+ // The framework helper is generic over the per-space planner output;
93
+ // here the "planner" is a no-op that returns the descriptor's
94
+ // `migrations` array. The benefit of routing through `planAllSpaces`
95
+ // is duplicate-spaceId detection + alphabetical ordering — failures
96
+ // there throw `MIGRATION.DUPLICATE_SPACE_ID` before any write.
97
+ const planned: readonly SpacePlanOutput<DescriptorMigrationPackage>[] = planAllSpaces(
98
+ planInputs,
99
+ (input) =>
100
+ (input as typeof input & { readonly __migrations: readonly DescriptorMigrationPackage[] })
101
+ .__migrations,
102
+ );
103
+
104
+ const emitted: { spaceId: string; dirName: string }[] = [];
105
+ const skipped: { spaceId: string; dirName: string }[] = [];
106
+
107
+ for (const space of planned) {
108
+ const spaceDir = spaceMigrationDirectory(inputs.migrationsDir, space.spaceId);
109
+ for (const pkg of space.migrationPackages) {
110
+ const { written } = await materialiseExtensionMigrationPackageIfMissing(spaceDir, pkg);
111
+ if (written) {
112
+ emitted.push({ spaceId: space.spaceId, dirName: pkg.dirName });
113
+ } else {
114
+ skipped.push({ spaceId: space.spaceId, dirName: pkg.dirName });
115
+ }
116
+ }
117
+ }
118
+
119
+ return { emitted, skipped };
120
+ }