@prisma-next/cli 0.5.0-dev.4 → 0.5.0-dev.41

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 (165) hide show
  1. package/README.md +56 -21
  2. package/dist/agent-skill-mongo.md +63 -31
  3. package/dist/agent-skill-postgres.md +1 -1
  4. package/dist/cli-errors-By1iVE3z.mjs +34 -0
  5. package/dist/cli-errors-By1iVE3z.mjs.map +1 -0
  6. package/dist/{cli-errors-C0JhVj0c.d.mts → cli-errors-DDeVsP2Y.d.mts} +1 -0
  7. package/dist/cli.mjs +123 -15
  8. package/dist/cli.mjs.map +1 -1
  9. package/dist/{client-TG7rbCWT.mjs → client-1JqqkiC7.mjs} +45 -20
  10. package/dist/client-1JqqkiC7.mjs.map +1 -0
  11. package/dist/commands/contract-emit.d.mts.map +1 -1
  12. package/dist/commands/contract-emit.mjs +2 -2
  13. package/dist/commands/contract-infer.d.mts.map +1 -1
  14. package/dist/commands/contract-infer.mjs +2 -2
  15. package/dist/commands/db-init.d.mts.map +1 -1
  16. package/dist/commands/db-init.mjs +10 -9
  17. package/dist/commands/db-init.mjs.map +1 -1
  18. package/dist/commands/db-schema.mjs +5 -5
  19. package/dist/commands/db-sign.mjs +7 -7
  20. package/dist/commands/db-update.mjs +9 -9
  21. package/dist/commands/db-update.mjs.map +1 -1
  22. package/dist/commands/db-verify.mjs +9 -9
  23. package/dist/commands/migration-apply.d.mts +5 -2
  24. package/dist/commands/migration-apply.d.mts.map +1 -1
  25. package/dist/commands/migration-apply.mjs +55 -56
  26. package/dist/commands/migration-apply.mjs.map +1 -1
  27. package/dist/commands/migration-new.d.mts.map +1 -1
  28. package/dist/commands/migration-new.mjs +26 -32
  29. package/dist/commands/migration-new.mjs.map +1 -1
  30. package/dist/commands/migration-plan.d.mts +14 -5
  31. package/dist/commands/migration-plan.d.mts.map +1 -1
  32. package/dist/commands/migration-plan.mjs +45 -48
  33. package/dist/commands/migration-plan.mjs.map +1 -1
  34. package/dist/commands/migration-ref.d.mts +1 -1
  35. package/dist/commands/migration-ref.d.mts.map +1 -1
  36. package/dist/commands/migration-ref.mjs +6 -10
  37. package/dist/commands/migration-ref.mjs.map +1 -1
  38. package/dist/commands/migration-show.d.mts +13 -7
  39. package/dist/commands/migration-show.d.mts.map +1 -1
  40. package/dist/commands/migration-show.mjs +27 -29
  41. package/dist/commands/migration-show.mjs.map +1 -1
  42. package/dist/commands/migration-status.d.mts +23 -5
  43. package/dist/commands/migration-status.d.mts.map +1 -1
  44. package/dist/commands/migration-status.mjs +3 -3
  45. package/dist/{config-loader-_W4T21X1.mjs → config-loader-ih8ViDb_.mjs} +2 -2
  46. package/dist/config-loader-ih8ViDb_.mjs.map +1 -0
  47. package/dist/config-loader.mjs +1 -1
  48. package/dist/contract-emit-LjzCoicC.mjs +4 -0
  49. package/dist/contract-emit-RZBWzkop.mjs +329 -0
  50. package/dist/contract-emit-RZBWzkop.mjs.map +1 -0
  51. package/dist/contract-emit-rt_Nmdwq.mjs +150 -0
  52. package/dist/contract-emit-rt_Nmdwq.mjs.map +1 -0
  53. package/dist/{contract-enrichment-CGW6mm-E.mjs → contract-enrichment-4Ptgw3Pe.mjs} +1 -1
  54. package/dist/{contract-enrichment-CGW6mm-E.mjs.map → contract-enrichment-4Ptgw3Pe.mjs.map} +1 -1
  55. package/dist/{contract-infer-BS4kIX9c.mjs → contract-infer-Cf5J2wVg.mjs} +11 -19
  56. package/dist/contract-infer-Cf5J2wVg.mjs.map +1 -0
  57. package/dist/exports/control-api.d.mts +86 -21
  58. package/dist/exports/control-api.d.mts.map +1 -1
  59. package/dist/exports/control-api.mjs +5 -5
  60. package/dist/exports/index.mjs +3 -3
  61. package/dist/exports/init-output.d.mts +39 -0
  62. package/dist/exports/init-output.d.mts.map +1 -0
  63. package/dist/exports/init-output.mjs +3 -0
  64. package/dist/{framework-components-DfZKQBQ2.mjs → framework-components-Bgcre3Z6.mjs} +2 -2
  65. package/dist/{framework-components-DfZKQBQ2.mjs.map → framework-components-Bgcre3Z6.mjs.map} +1 -1
  66. package/dist/init-C7dE9KOJ.mjs +2062 -0
  67. package/dist/init-C7dE9KOJ.mjs.map +1 -0
  68. package/dist/{inspect-live-schema-BsoFVoS1.mjs → inspect-live-schema-LWtXfxm_.mjs} +9 -9
  69. package/dist/inspect-live-schema-LWtXfxm_.mjs.map +1 -0
  70. package/dist/migration-cli.d.mts +41 -11
  71. package/dist/migration-cli.d.mts.map +1 -1
  72. package/dist/migration-cli.mjs +308 -84
  73. package/dist/migration-cli.mjs.map +1 -1
  74. package/dist/{migration-command-scaffold-DOXnheFa.mjs → migration-command-scaffold-CU452v9h.mjs} +7 -7
  75. package/dist/{migration-command-scaffold-DOXnheFa.mjs.map → migration-command-scaffold-CU452v9h.mjs.map} +1 -1
  76. package/dist/{migration-status-Ry3TnEya.mjs → migration-status-DoPrFIOQ.mjs} +114 -57
  77. package/dist/migration-status-DoPrFIOQ.mjs.map +1 -0
  78. package/dist/{migrations-fU0xoKjS.mjs → migrations-MEoKMiV5.mjs} +42 -21
  79. package/dist/migrations-MEoKMiV5.mjs.map +1 -0
  80. package/dist/output-BpcQrnnq.mjs +103 -0
  81. package/dist/output-BpcQrnnq.mjs.map +1 -0
  82. package/dist/{progress-adapter-B-YvmcDu.mjs → progress-adapter-DgRGldpT.mjs} +1 -1
  83. package/dist/{progress-adapter-B-YvmcDu.mjs.map → progress-adapter-DgRGldpT.mjs.map} +1 -1
  84. package/dist/quick-reference-mongo.md +34 -13
  85. package/dist/quick-reference-postgres.md +11 -9
  86. package/dist/{result-handler-BJwA7ufw.mjs → result-handler-Ch6hVnOo.mjs} +35 -93
  87. package/dist/result-handler-Ch6hVnOo.mjs.map +1 -0
  88. package/dist/{terminal-ui-C5k88MmW.mjs → terminal-ui-u2YgKghu.mjs} +76 -2
  89. package/dist/terminal-ui-u2YgKghu.mjs.map +1 -0
  90. package/dist/{verify-bl__PkXk.mjs → verify-BT9tgCOH.mjs} +2 -2
  91. package/dist/{verify-bl__PkXk.mjs.map → verify-BT9tgCOH.mjs.map} +1 -1
  92. package/package.json +22 -16
  93. package/src/cli.ts +32 -6
  94. package/src/commands/contract-emit.ts +67 -163
  95. package/src/commands/contract-infer.ts +7 -20
  96. package/src/commands/db-init.ts +1 -0
  97. package/src/commands/db-update.ts +1 -1
  98. package/src/commands/init/detect-pnpm-catalog.ts +141 -0
  99. package/src/commands/init/errors.ts +254 -0
  100. package/src/commands/init/exit-codes.ts +62 -0
  101. package/src/commands/init/hygiene-gitattributes.ts +97 -0
  102. package/src/commands/init/hygiene-gitignore.ts +48 -0
  103. package/src/commands/init/hygiene-package-scripts.ts +91 -0
  104. package/src/commands/init/index.ts +112 -7
  105. package/src/commands/init/init.ts +766 -144
  106. package/src/commands/init/inputs.ts +421 -0
  107. package/src/commands/init/output.ts +147 -0
  108. package/src/commands/init/probe-db.ts +308 -0
  109. package/src/commands/init/reinit-cleanup.ts +83 -0
  110. package/src/commands/init/templates/agent-skill-mongo.md +63 -31
  111. package/src/commands/init/templates/agent-skill-postgres.md +1 -1
  112. package/src/commands/init/templates/agent-skill.ts +25 -3
  113. package/src/commands/init/templates/code-templates.ts +125 -32
  114. package/src/commands/init/templates/env.ts +80 -0
  115. package/src/commands/init/templates/quick-reference-mongo.md +34 -13
  116. package/src/commands/init/templates/quick-reference-postgres.md +11 -9
  117. package/src/commands/init/templates/quick-reference.ts +42 -3
  118. package/src/commands/init/templates/tsconfig.ts +167 -5
  119. package/src/commands/inspect-live-schema.ts +10 -5
  120. package/src/commands/migration-apply.ts +84 -63
  121. package/src/commands/migration-new.ts +28 -34
  122. package/src/commands/migration-plan.ts +80 -56
  123. package/src/commands/migration-ref.ts +8 -7
  124. package/src/commands/migration-show.ts +53 -36
  125. package/src/commands/migration-status.ts +194 -58
  126. package/src/config-path-validation.ts +0 -1
  127. package/src/control-api/client.ts +21 -0
  128. package/src/control-api/operations/contract-emit.ts +198 -115
  129. package/src/control-api/operations/db-init.ts +10 -6
  130. package/src/control-api/operations/db-update.ts +10 -6
  131. package/src/control-api/operations/migration-apply.ts +30 -9
  132. package/src/control-api/types.ts +69 -7
  133. package/src/exports/control-api.ts +2 -1
  134. package/src/exports/init-output.ts +10 -0
  135. package/src/migration-cli.ts +445 -122
  136. package/src/utils/cli-errors.ts +49 -2
  137. package/src/utils/command-helpers.ts +45 -23
  138. package/src/utils/emit-queue.ts +26 -0
  139. package/src/utils/formatters/graph-migration-mapper.ts +7 -3
  140. package/src/utils/formatters/migrations.ts +62 -26
  141. package/src/utils/publish-contract-artifact-pair.ts +134 -0
  142. package/dist/cli-errors-DHq6GQGu.mjs +0 -5
  143. package/dist/client-TG7rbCWT.mjs.map +0 -1
  144. package/dist/config-loader-_W4T21X1.mjs.map +0 -1
  145. package/dist/contract-emit-CQfj7xJn.mjs +0 -122
  146. package/dist/contract-emit-CQfj7xJn.mjs.map +0 -1
  147. package/dist/contract-emit-DpPjuFy-.mjs +0 -195
  148. package/dist/contract-emit-DpPjuFy-.mjs.map +0 -1
  149. package/dist/contract-emit-fhNwwhkQ.mjs +0 -4
  150. package/dist/contract-infer-BS4kIX9c.mjs.map +0 -1
  151. package/dist/extract-operation-statements-DZUJNmL3.mjs +0 -13
  152. package/dist/extract-operation-statements-DZUJNmL3.mjs.map +0 -1
  153. package/dist/extract-sql-ddl-DDMX-9mz.mjs +0 -26
  154. package/dist/extract-sql-ddl-DDMX-9mz.mjs.map +0 -1
  155. package/dist/init-CQfo_4Ro.mjs +0 -430
  156. package/dist/init-CQfo_4Ro.mjs.map +0 -1
  157. package/dist/inspect-live-schema-BsoFVoS1.mjs.map +0 -1
  158. package/dist/migration-status-Ry3TnEya.mjs.map +0 -1
  159. package/dist/migrations-fU0xoKjS.mjs.map +0 -1
  160. package/dist/result-handler-BJwA7ufw.mjs.map +0 -1
  161. package/dist/terminal-ui-C5k88MmW.mjs.map +0 -1
  162. package/dist/validate-contract-deps-esa-VQ0h.mjs +0 -37
  163. package/dist/validate-contract-deps-esa-VQ0h.mjs.map +0 -1
  164. package/src/control-api/operations/extract-operation-statements.ts +0 -14
  165. package/src/control-api/operations/extract-sql-ddl.ts +0 -47
@@ -1,4 +1,4 @@
1
- import { mkdir, writeFile } from 'node:fs/promises';
1
+ import { mkdir } from 'node:fs/promises';
2
2
  import type { Contract } from '@prisma-next/contract/types';
3
3
  import { emit, getEmittedArtifactPaths } from '@prisma-next/emitter';
4
4
  import { createControlStack } from '@prisma-next/framework-components/control';
@@ -7,57 +7,156 @@ import { ifDefined } from '@prisma-next/utils/defined';
7
7
  import { dirname } from 'pathe';
8
8
  import { loadConfig } from '../../config-loader';
9
9
  import { errorContractConfigMissing, errorRuntime } from '../../utils/cli-errors';
10
+ import { queueEmitByOutput } from '../../utils/emit-queue';
10
11
  import { assertFrameworkComponentsCompatible } from '../../utils/framework-components';
12
+ import { publishContractArtifactPair } from '../../utils/publish-contract-artifact-pair';
13
+ import { validateContractDeps } from '../../utils/validate-contract-deps';
11
14
  import { enrichContract } from '../contract-enrichment';
12
- import type { ContractEmitOptions, ContractEmitResult } from '../types';
15
+ import type {
16
+ ContractEmitOptions,
17
+ ContractEmitResult,
18
+ ControlActionName,
19
+ OnControlProgress,
20
+ } from '../types';
13
21
 
14
- interface ProviderFailureLike {
15
- readonly summary: string;
16
- readonly diagnostics: readonly unknown[];
17
- readonly meta?: unknown;
18
- }
22
+ const EMIT_ACTION: ControlActionName = 'emit';
19
23
 
20
24
  function isRecord(value: unknown): value is Record<string, unknown> {
21
25
  return typeof value === 'object' && value !== null;
22
26
  }
23
27
 
24
- function isAbortError(error: unknown): boolean {
25
- return isRecord(error) && typeof error['name'] === 'string' && error['name'] === 'AbortError';
28
+ function startSpan(onProgress: OnControlProgress | undefined, spanId: string, label: string): void {
29
+ onProgress?.({ action: EMIT_ACTION, kind: 'spanStart', spanId, label });
30
+ }
31
+
32
+ function endSpan(
33
+ onProgress: OnControlProgress | undefined,
34
+ spanId: string,
35
+ outcome: 'ok' | 'error',
36
+ ): void {
37
+ onProgress?.({ action: EMIT_ACTION, kind: 'spanEnd', spanId, outcome });
38
+ }
39
+
40
+ function failedToResolveContractSource(why: string, fix: string, meta?: Record<string, unknown>) {
41
+ return errorRuntime('Failed to resolve contract source', {
42
+ why,
43
+ fix,
44
+ ...ifDefined('meta', meta),
45
+ });
46
+ }
47
+
48
+ type ValidatedProviderResult =
49
+ | { readonly ok: true; readonly value: unknown }
50
+ | { readonly ok: false; readonly error: ReturnType<typeof errorRuntime> };
51
+
52
+ function diagnosticLocationSuffix(diagnostic: Record<string, unknown>): string {
53
+ const sourceId = typeof diagnostic['sourceId'] === 'string' ? diagnostic['sourceId'] : undefined;
54
+ const span = isRecord(diagnostic['span']) ? diagnostic['span'] : undefined;
55
+ const start = span && isRecord(span['start']) ? span['start'] : undefined;
56
+ const line = start && typeof start['line'] === 'number' ? start['line'] : undefined;
57
+ const column = start && typeof start['column'] === 'number' ? start['column'] : undefined;
58
+ if (sourceId && line !== undefined && column !== undefined) {
59
+ return ` (${sourceId}:${line}:${column})`;
60
+ }
61
+ if (sourceId) {
62
+ return ` (${sourceId})`;
63
+ }
64
+ return '';
26
65
  }
27
66
 
28
- function isProviderFailureLike(value: unknown): value is ProviderFailureLike {
29
- return (
30
- isRecord(value) && typeof value['summary'] === 'string' && Array.isArray(value['diagnostics'])
31
- );
67
+ function mapDiagnosticsToIssues(
68
+ diagnostics: readonly unknown[],
69
+ ): ReadonlyArray<{ readonly kind: string; readonly message: string }> {
70
+ const issues: { readonly kind: string; readonly message: string }[] = [];
71
+ for (const raw of diagnostics) {
72
+ if (!isRecord(raw)) continue;
73
+ const code = typeof raw['code'] === 'string' ? raw['code'] : 'diagnostic';
74
+ const message = typeof raw['message'] === 'string' ? raw['message'] : '';
75
+ issues.push({ kind: code, message: `${message}${diagnosticLocationSuffix(raw)}` });
76
+ }
77
+ return issues;
78
+ }
79
+
80
+ function validateProviderResult(providerResult: unknown): ValidatedProviderResult {
81
+ if (!isRecord(providerResult) || typeof providerResult['ok'] !== 'boolean') {
82
+ return {
83
+ ok: false,
84
+ error: failedToResolveContractSource(
85
+ 'Contract source provider returned malformed result shape.',
86
+ 'Ensure contract.source.load resolves to ok(Contract) or notOk({ summary, diagnostics }).',
87
+ ),
88
+ };
89
+ }
90
+
91
+ if (providerResult['ok']) {
92
+ if (!('value' in providerResult)) {
93
+ return {
94
+ ok: false,
95
+ error: failedToResolveContractSource(
96
+ 'Contract source provider returned malformed success result: missing value.',
97
+ 'Ensure contract.source.load success payload is ok(Contract).',
98
+ ),
99
+ };
100
+ }
101
+ return { ok: true, value: providerResult['value'] };
102
+ }
103
+
104
+ const failure = providerResult['failure'];
105
+ if (
106
+ !isRecord(failure) ||
107
+ typeof failure['summary'] !== 'string' ||
108
+ !Array.isArray(failure['diagnostics'])
109
+ ) {
110
+ return {
111
+ ok: false,
112
+ error: failedToResolveContractSource(
113
+ 'Contract source provider returned malformed failure result: expected summary and diagnostics.',
114
+ 'Ensure contract.source.load failure payload is notOk({ summary, diagnostics, meta? }).',
115
+ ),
116
+ };
117
+ }
118
+ return {
119
+ ok: false,
120
+ error: failedToResolveContractSource(
121
+ String(failure['summary']),
122
+ 'Fix contract source diagnostics and return ok(Contract).',
123
+ {
124
+ diagnostics: failure['diagnostics'],
125
+ issues: mapDiagnosticsToIssues(failure['diagnostics']),
126
+ ...ifDefined('providerMeta', failure['meta']),
127
+ },
128
+ ),
129
+ };
32
130
  }
33
131
 
34
132
  /**
35
- * Executes the contract emit operation.
133
+ * Canonical contract emit operation.
36
134
  *
37
- * This is an offline operation that:
38
- * 1. Loads the Prisma Next config from the specified path
39
- * 2. Resolves the contract source from config
40
- * 3. Creates a control plane stack and family instance
41
- * 4. Emits contract artifacts (JSON and DTS)
42
- * 5. Writes files to the paths specified in config
135
+ * This is the SINGLE publication path used by both the CLI command
136
+ * (`prisma-next contract emit`) and the Vite plugin
137
+ * (`@prisma-next/vite-plugin-contract-emit`). New callers must go through this
138
+ * function rather than re-implementing load emit publish.
43
139
  *
44
- * Supports AbortSignal for cancellation, enabling "last change wins" behavior.
140
+ * The whole flow (load config resolve source → emit bytes → atomic publish)
141
+ * is serialized per output JSON path via `queueEmitByOutput`. Concurrent calls
142
+ * for the same output line up FIFO; the user-visible outcome is "last
143
+ * submission wins on disk" without any supersession bookkeeping. Within a
144
+ * single emit, `publishContractArtifactPair` stages temp files, renames
145
+ * `contract.d.ts` before `contract.json`, and attempts to restore the previous
146
+ * pair if either rename fails — so type-only consumers never observe a
147
+ * mismatched pair.
45
148
  *
46
- * @param options - Options including configPath and optional signal
47
- * @returns File paths and hashes of emitted artifacts
48
- * @throws If config loading fails, contract is invalid, or file I/O fails
49
- * @throws signal.reason if cancelled via AbortSignal (typically DOMException with name 'AbortError')
149
+ * @throws {CliStructuredError} on config/source/validation problems
150
+ * @throws {DOMException} `AbortError` if cancelled via `signal`
50
151
  */
51
152
  export async function executeContractEmit(
52
153
  options: ContractEmitOptions,
53
154
  ): Promise<ContractEmitResult> {
54
- const { configPath, signal = new AbortController().signal } = options;
155
+ const { configPath, signal = new AbortController().signal, onProgress } = options;
55
156
  const unlessAborted = abortable(signal);
56
157
 
57
- // Load config using the existing config loader
58
158
  const config = await unlessAborted(loadConfig(configPath));
59
159
 
60
- // Validate contract config is present
61
160
  if (!config.contract) {
62
161
  throw errorContractConfigMissing({
63
162
  why: 'Config.contract is required for emit. Define it in your config: contract: { source: ..., output: ... }',
@@ -66,14 +165,12 @@ export async function executeContractEmit(
66
165
 
67
166
  const contractConfig = config.contract;
68
167
 
69
- // Validate output path is present and ends with .json
70
168
  if (!contractConfig.output) {
71
169
  throw errorContractConfigMissing({
72
170
  why: 'Contract config must have output path. This should not happen if defineConfig() was used.',
73
171
  });
74
172
  }
75
173
 
76
- // Validate source exists and is callable
77
174
  if (typeof contractConfig.source?.load !== 'function') {
78
175
  throw errorContractConfigMissing({
79
176
  why: 'Contract config must include a valid source provider object',
@@ -90,98 +187,84 @@ export async function executeContractEmit(
90
187
  }
91
188
  const { jsonPath: outputJsonPath, dtsPath: outputDtsPath } = outputPaths;
92
189
 
93
- const stack = createControlStack(config);
190
+ return queueEmitByOutput(outputJsonPath, async () => {
191
+ const stack = createControlStack(config);
94
192
 
95
- const sourceContext = {
96
- composedExtensionPacks: stack.extensionPacks.map((p) => p.id),
97
- scalarTypeDescriptors: stack.scalarTypeDescriptors,
98
- authoringContributions: stack.authoringContributions,
99
- codecLookup: stack.codecLookup,
100
- controlMutationDefaults: stack.controlMutationDefaults,
101
- resolvedInputs: contractConfig.source.inputs ?? [],
102
- };
193
+ const sourceContext = {
194
+ composedExtensionPacks: stack.extensionPacks.map((p) => p.id),
195
+ scalarTypeDescriptors: stack.scalarTypeDescriptors,
196
+ authoringContributions: stack.authoringContributions,
197
+ codecLookup: stack.codecLookup,
198
+ controlMutationDefaults: stack.controlMutationDefaults,
199
+ resolvedInputs: contractConfig.source.inputs ?? [],
200
+ };
103
201
 
104
- let providerResult: Awaited<ReturnType<typeof contractConfig.source.load>>;
105
- try {
106
- providerResult = await unlessAborted(contractConfig.source.load(sourceContext));
107
- } catch (error) {
108
- if (signal.aborted || isAbortError(error)) {
109
- throw error;
202
+ startSpan(onProgress, 'resolveSource', 'Resolving contract source...');
203
+ let providerResult: Awaited<ReturnType<typeof contractConfig.source.load>>;
204
+ try {
205
+ providerResult = await unlessAborted(contractConfig.source.load(sourceContext));
206
+ } catch (error) {
207
+ endSpan(onProgress, 'resolveSource', 'error');
208
+ if (signal.aborted || (isRecord(error) && error['name'] === 'AbortError')) {
209
+ throw error;
210
+ }
211
+ throw failedToResolveContractSource(
212
+ error instanceof Error ? error.message : String(error),
213
+ 'Ensure contract.source.load resolves to ok(Contract) or returns structured diagnostics.',
214
+ );
110
215
  }
111
- throw errorRuntime('Failed to resolve contract source', {
112
- why: error instanceof Error ? error.message : String(error),
113
- fix: 'Ensure contract.source.load resolves to ok(Contract) or returns structured diagnostics.',
114
- });
115
- }
116
216
 
117
- if (!isRecord(providerResult) || typeof providerResult.ok !== 'boolean') {
118
- throw errorRuntime('Failed to resolve contract source', {
119
- why: 'Contract source provider returned malformed result shape.',
120
- fix: 'Ensure contract.source.load resolves to ok(Contract) or notOk({ summary, diagnostics }).',
121
- });
122
- }
123
-
124
- if (providerResult.ok && !('value' in providerResult)) {
125
- throw errorRuntime('Failed to resolve contract source', {
126
- why: 'Contract source provider returned malformed success result: missing value.',
127
- fix: 'Ensure contract.source.load success payload is ok(Contract).',
128
- });
129
- }
217
+ const validatedContract = validateProviderResult(providerResult);
218
+ if (!validatedContract.ok) {
219
+ endSpan(onProgress, 'resolveSource', 'error');
220
+ throw validatedContract.error;
221
+ }
222
+ endSpan(onProgress, 'resolveSource', 'ok');
130
223
 
131
- if (!providerResult.ok && !isProviderFailureLike(providerResult.failure)) {
132
- throw errorRuntime('Failed to resolve contract source', {
133
- why: 'Contract source provider returned malformed failure result: expected summary and diagnostics.',
134
- fix: 'Ensure contract.source.load failure payload is notOk({ summary, diagnostics, meta? }).',
135
- });
136
- }
224
+ startSpan(onProgress, 'emit', 'Emitting contract...');
225
+ let emitResult: Awaited<ReturnType<typeof emit>>;
226
+ try {
227
+ const familyInstance = config.family.create(stack);
228
+ const rawComponents = [config.target, config.adapter, ...(config.extensionPacks ?? [])];
229
+ const frameworkComponents = assertFrameworkComponentsCompatible(
230
+ config.family.familyId,
231
+ config.target.targetId,
232
+ rawComponents,
233
+ );
234
+ const enrichedIR = enrichContract(validatedContract.value as Contract, frameworkComponents);
235
+ familyInstance.validateContract(enrichedIR);
236
+ emitResult = await unlessAborted(
237
+ emit(enrichedIR, stack, config.family.emission, { outputJsonPath }),
238
+ );
239
+ } catch (error) {
240
+ endSpan(onProgress, 'emit', 'error');
241
+ throw error;
242
+ }
243
+ endSpan(onProgress, 'emit', 'ok');
137
244
 
138
- if (!providerResult.ok) {
139
- throw errorRuntime('Failed to resolve contract source', {
140
- why: providerResult.failure.summary,
141
- fix: 'Fix contract source diagnostics and return ok(Contract).',
142
- meta: {
143
- diagnostics: providerResult.failure.diagnostics,
144
- ...ifDefined('providerMeta', providerResult.failure.meta),
145
- },
245
+ await unlessAborted(mkdir(dirname(outputJsonPath), { recursive: true }));
246
+ await publishContractArtifactPair({
247
+ outputJsonPath,
248
+ outputDtsPath,
249
+ contractJson: emitResult.contractJson,
250
+ contractDts: emitResult.contractDts,
251
+ publicationToken: String(process.hrtime.bigint()),
146
252
  });
147
- }
148
253
 
149
- const familyInstance = config.family.create(stack);
150
-
151
- const rawComponents = [config.target, config.adapter, ...(config.extensionPacks ?? [])];
152
- const frameworkComponents = assertFrameworkComponentsCompatible(
153
- config.family.familyId,
154
- config.target.targetId,
155
- rawComponents,
156
- );
157
- const enrichedIR = enrichContract(providerResult.value as Contract, frameworkComponents);
158
-
159
- familyInstance.validateContract(enrichedIR);
160
- const emitResult = await unlessAborted(
161
- emit(enrichedIR, stack, config.family.emission, {
162
- outputJsonPath: outputJsonPath,
163
- }),
164
- );
165
-
166
- // Create directory if needed and write files (both colocated)
167
- await unlessAborted(mkdir(dirname(outputJsonPath), { recursive: true }));
168
- await unlessAborted(writeFile(outputJsonPath, emitResult.contractJson, 'utf-8'));
169
- await unlessAborted(writeFile(outputDtsPath, emitResult.contractDts, 'utf-8'));
170
-
171
- // Validate that contract.d.ts type imports are resolvable
172
- const { validateContractDeps } = await import('../../utils/validate-contract-deps');
173
- const depsValidation = validateContractDeps(emitResult.contractDts, dirname(outputDtsPath));
174
- if (depsValidation.warning) {
175
- process.stderr.write(`\n⚠ ${depsValidation.warning}\n`);
176
- }
254
+ const validationWarning = validateContractDeps(
255
+ emitResult.contractDts,
256
+ dirname(outputDtsPath),
257
+ ).warning;
177
258
 
178
- return {
179
- storageHash: emitResult.storageHash,
180
- ...ifDefined('executionHash', emitResult.executionHash),
181
- profileHash: emitResult.profileHash,
182
- files: {
183
- json: outputJsonPath,
184
- dts: outputDtsPath,
185
- },
186
- };
259
+ return {
260
+ storageHash: emitResult.storageHash,
261
+ ...ifDefined('executionHash', emitResult.executionHash),
262
+ profileHash: emitResult.profileHash,
263
+ files: {
264
+ json: outputJsonPath,
265
+ dts: outputDtsPath,
266
+ },
267
+ ...ifDefined('validationWarning', validationWarning),
268
+ };
269
+ });
187
270
  }
@@ -8,10 +8,10 @@ import type {
8
8
  MigrationRunnerResult,
9
9
  TargetMigrationsCapability,
10
10
  } from '@prisma-next/framework-components/control';
11
+ import { hasOperationPreview } from '@prisma-next/framework-components/control';
11
12
  import { ifDefined } from '@prisma-next/utils/defined';
12
13
  import { notOk, ok } from '@prisma-next/utils/result';
13
14
  import type { DbInitResult, DbInitSuccess, OnControlProgress } from '../types';
14
- import { extractOperationStatements } from './extract-operation-statements';
15
15
  import { createOperationCallbacks, stripOperations } from './migration-helpers';
16
16
 
17
17
  /**
@@ -83,9 +83,11 @@ export async function executeDbInit<TFamilyId extends string, TTargetId extends
83
83
  contract,
84
84
  schema: schemaIR,
85
85
  policy,
86
- // `db init` does not produce a `migration.ts`, so the from-hash on the
87
- // resulting plan is never surfaced to authoring pass empty string.
88
- fromHash: '',
86
+ // `db init` reconciles against the live introspected schema; there is no
87
+ // prior contract to derive a "from" identity from. The required
88
+ // `fromContract: null` makes that structural fact visible at the call
89
+ // site (vs. silently letting the planner default to a baseline plan).
90
+ fromContract: null,
89
91
  frameworkComponents,
90
92
  });
91
93
 
@@ -194,12 +196,14 @@ export async function executeDbInit<TFamilyId extends string, TTargetId extends
194
196
 
195
197
  // Plan mode - don't execute
196
198
  if (mode === 'plan') {
197
- const planSql = extractOperationStatements(familyInstance.familyId, migrationPlan.operations);
199
+ const preview = hasOperationPreview(familyInstance)
200
+ ? familyInstance.toOperationPreview(migrationPlan.operations)
201
+ : undefined;
198
202
  const result: DbInitSuccess = {
199
203
  mode: 'plan',
200
204
  plan: {
201
205
  operations: stripOperations(migrationPlan.operations),
202
- ...ifDefined('sql', planSql),
206
+ ...ifDefined('preview', preview),
203
207
  },
204
208
  destination: {
205
209
  storageHash: migrationPlan.destination.storageHash,
@@ -7,10 +7,10 @@ import type {
7
7
  MigrationRunnerResult,
8
8
  TargetMigrationsCapability,
9
9
  } from '@prisma-next/framework-components/control';
10
+ import { hasOperationPreview } from '@prisma-next/framework-components/control';
10
11
  import { ifDefined } from '@prisma-next/utils/defined';
11
12
  import { notOk, ok } from '@prisma-next/utils/result';
12
13
  import type { DbUpdateResult, DbUpdateSuccess, OnControlProgress } from '../types';
13
- import { extractOperationStatements } from './extract-operation-statements';
14
14
  import { createOperationCallbacks, stripOperations } from './migration-helpers';
15
15
 
16
16
  // F12: db update allows additive, widening, and destructive operations.
@@ -83,9 +83,11 @@ export async function executeDbUpdate<TFamilyId extends string, TTargetId extend
83
83
  contract,
84
84
  schema: schemaIR,
85
85
  policy,
86
- // `db update` does not produce a `migration.ts`, so the from-hash on the
87
- // resulting plan is never surfaced to authoring pass empty string.
88
- fromHash: '',
86
+ // `db update` reconciles against the live introspected schema; there is
87
+ // no prior contract to derive a "from" identity from. The required
88
+ // `fromContract: null` makes that structural fact visible at the call
89
+ // site (vs. silently letting the planner default to a baseline plan).
90
+ fromContract: null,
89
91
  frameworkComponents,
90
92
  });
91
93
  if (plannerResult.kind === 'failure') {
@@ -113,12 +115,14 @@ export async function executeDbUpdate<TFamilyId extends string, TTargetId extend
113
115
  const migrationPlan = plannerResult.plan;
114
116
 
115
117
  if (mode === 'plan') {
116
- const planSql = extractOperationStatements(familyInstance.familyId, migrationPlan.operations);
118
+ const preview = hasOperationPreview(familyInstance)
119
+ ? familyInstance.toOperationPreview(migrationPlan.operations)
120
+ : undefined;
117
121
  const result: DbUpdateSuccess = {
118
122
  mode: 'plan',
119
123
  plan: {
120
124
  operations: stripOperations(migrationPlan.operations),
121
- ...(planSql !== undefined ? { sql: planSql } : {}),
125
+ ...ifDefined('preview', preview),
122
126
  },
123
127
  destination: {
124
128
  storageHash: migrationPlan.destination.storageHash,
@@ -30,6 +30,21 @@ export interface ExecuteMigrationApplyOptions<TFamilyId extends string, TTargetI
30
30
  readonly onProgress?: OnControlProgress;
31
31
  }
32
32
 
33
+ /**
34
+ * Apply a sequence of migration packages against the configured driver.
35
+ *
36
+ * Validates the path's continuity (origin → ... → destination, no gaps),
37
+ * then drives the family/target's migration runner over each package's
38
+ * operations in order, surfacing per-migration progress through `onProgress`.
39
+ *
40
+ * The `pendingMigrations` parameter is trusted input. Callers are responsible
41
+ * for upstream verification of the originating migration packages — typically
42
+ * by loading them via `readMigrationPackage` from
43
+ * `@prisma-next/migration-tools/io`, which performs hash-integrity checks at
44
+ * the load boundary. This operation does not re-verify the packages and
45
+ * assumes the `(metadata, ops)` pairs on disk have not been tampered with
46
+ * since emit.
47
+ */
33
48
  export async function executeMigrationApply<TFamilyId extends string, TTargetId extends string>(
34
49
  options: ExecuteMigrationApplyOptions<TFamilyId, TTargetId>,
35
50
  ): Promise<MigrationApplyResult> {
@@ -64,15 +79,19 @@ export async function executeMigrationApply<TFamilyId extends string, TTargetId
64
79
 
65
80
  const firstMigration = pendingMigrations[0]!;
66
81
  const lastMigration = pendingMigrations[pendingMigrations.length - 1]!;
67
- if (firstMigration.from !== originHash || lastMigration.to !== destinationHash) {
82
+ // Manifest `from` is `string | null` (null = baseline). The live-marker
83
+ // layer encodes "no prior state" as EMPTY_CONTRACT_HASH; bridge here so the
84
+ // string comparisons below work uniformly.
85
+ const firstFromMarker = firstMigration.from ?? EMPTY_CONTRACT_HASH;
86
+ if (firstFromMarker !== originHash || lastMigration.to !== destinationHash) {
68
87
  return notOk({
69
88
  code: 'MIGRATION_PATH_NOT_FOUND' as const,
70
89
  summary: 'Migration apply path does not match requested origin and destination',
71
- why: `Path resolved as ${firstMigration.from} -> ${lastMigration.to}, but requested ${originHash} -> ${destinationHash}`,
90
+ why: `Path resolved as ${firstFromMarker} -> ${lastMigration.to}, but requested ${originHash} -> ${destinationHash}`,
72
91
  meta: {
73
92
  originHash,
74
93
  destinationHash,
75
- pathOrigin: firstMigration.from,
94
+ pathOrigin: firstFromMarker,
76
95
  pathDestination: lastMigration.to,
77
96
  },
78
97
  });
@@ -81,18 +100,19 @@ export async function executeMigrationApply<TFamilyId extends string, TTargetId
81
100
  for (let i = 1; i < pendingMigrations.length; i++) {
82
101
  const previous = pendingMigrations[i - 1]!;
83
102
  const current = pendingMigrations[i]!;
84
- if (previous.to !== current.from) {
103
+ const currentFromMarker = current.from ?? EMPTY_CONTRACT_HASH;
104
+ if (previous.to !== currentFromMarker) {
85
105
  return notOk({
86
106
  code: 'MIGRATION_PATH_NOT_FOUND' as const,
87
107
  summary: 'Migration apply path contains a discontinuity between adjacent migrations',
88
- why: `Migration "${previous.dirName}" ends at ${previous.to}, but next migration "${current.dirName}" starts at ${current.from}`,
108
+ why: `Migration "${previous.dirName}" ends at ${previous.to}, but next migration "${current.dirName}" starts at ${currentFromMarker}`,
89
109
  meta: {
90
110
  originHash,
91
111
  destinationHash,
92
112
  previousDirName: previous.dirName,
93
113
  previousTo: previous.to,
94
114
  currentDirName: current.dirName,
95
- currentFrom: current.from,
115
+ currentFrom: currentFromMarker,
96
116
  discontinuityIndex: i,
97
117
  },
98
118
  });
@@ -120,13 +140,14 @@ export async function executeMigrationApply<TFamilyId extends string, TTargetId
120
140
  allowedOperationClasses: ['additive', 'widening', 'destructive', 'data'] as const,
121
141
  };
122
142
 
123
- // EMPTY_CONTRACT_HASH means "no prior state" — the runner expects origin: null
124
- // for a fresh database (no marker present).
143
+ // Manifest `from === null` means "no prior state" — the runner expects
144
+ // `origin: null` for a fresh database (no marker present).
125
145
  const plan = {
126
146
  targetId,
127
- origin: migration.from === EMPTY_CONTRACT_HASH ? null : { storageHash: migration.from },
147
+ origin: migration.from === null ? null : { storageHash: migration.from },
128
148
  destination: { storageHash: migration.to },
129
149
  operations,
150
+ providedInvariants: migration.providedInvariants,
130
151
  };
131
152
 
132
153
  const destinationContract = familyInstance.validateContract(migration.toContract);