@prisma-next/framework-components 0.5.0-dev.9 → 0.5.0

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 (95) hide show
  1. package/README.md +51 -4
  2. package/dist/authoring.d.mts +2 -2
  3. package/dist/authoring.mjs +2 -122
  4. package/dist/codec-m_-FAyQn.d.mts +168 -0
  5. package/dist/codec-m_-FAyQn.d.mts.map +1 -0
  6. package/dist/codec.d.mts +48 -2
  7. package/dist/codec.d.mts.map +1 -0
  8. package/dist/codec.mjs +67 -4
  9. package/dist/codec.mjs.map +1 -1
  10. package/dist/components.d.mts +1 -1
  11. package/dist/components.mjs +2 -3
  12. package/dist/control.d.mts +420 -76
  13. package/dist/control.d.mts.map +1 -1
  14. package/dist/control.mjs +83 -58
  15. package/dist/control.mjs.map +1 -1
  16. package/dist/emission-types-CMv_053d.d.mts +38 -0
  17. package/dist/emission-types-CMv_053d.d.mts.map +1 -0
  18. package/dist/emission.d.mts +2 -2
  19. package/dist/emission.mjs +1 -1
  20. package/dist/execution.d.mts +5 -5
  21. package/dist/execution.d.mts.map +1 -1
  22. package/dist/execution.mjs +4 -6
  23. package/dist/execution.mjs.map +1 -1
  24. package/dist/{framework-authoring-D1-JZ37B.d.mts → framework-authoring-DGIQbNPt.d.mts} +43 -12
  25. package/dist/framework-authoring-DGIQbNPt.d.mts.map +1 -0
  26. package/dist/framework-authoring-DxXcjyJX.mjs +209 -0
  27. package/dist/framework-authoring-DxXcjyJX.mjs.map +1 -0
  28. package/dist/{framework-components-DFZMi2h7.d.mts → framework-components-CHuBHXQN.d.mts} +45 -58
  29. package/dist/framework-components-CHuBHXQN.d.mts.map +1 -0
  30. package/dist/{framework-components-C8ZhSwXe.mjs → framework-components-FdqmlGUj.mjs} +3 -3
  31. package/dist/framework-components-FdqmlGUj.mjs.map +1 -0
  32. package/dist/psl-ast-Ckn_G-jv.d.mts +159 -0
  33. package/dist/psl-ast-Ckn_G-jv.d.mts.map +1 -0
  34. package/dist/psl-ast.d.mts +2 -0
  35. package/dist/psl-ast.mjs +1 -0
  36. package/dist/runtime.d.mts +273 -37
  37. package/dist/runtime.d.mts.map +1 -1
  38. package/dist/runtime.mjs +172 -30
  39. package/dist/runtime.mjs.map +1 -1
  40. package/dist/{types-import-spec-C4sc7wbb.d.mts → types-import-spec-BxI5cSQy.d.mts} +2 -2
  41. package/dist/types-import-spec-BxI5cSQy.d.mts.map +1 -0
  42. package/package.json +10 -6
  43. package/src/control/control-capabilities.ts +96 -0
  44. package/src/{control-descriptors.ts → control/control-descriptors.ts} +7 -7
  45. package/src/{control-instances.ts → control/control-instances.ts} +52 -6
  46. package/src/{control-migration-types.ts → control/control-migration-types.ts} +251 -63
  47. package/src/control/control-operation-preview.ts +23 -0
  48. package/src/control/control-spaces.ts +82 -0
  49. package/src/{control-stack.ts → control/control-stack.ts} +77 -111
  50. package/src/control/emission-types.ts +48 -0
  51. package/src/control/psl-ast.ts +193 -0
  52. package/src/{execution-descriptors.ts → execution/execution-descriptors.ts} +7 -7
  53. package/src/{execution-instances.ts → execution/execution-instances.ts} +1 -1
  54. package/src/{execution-requirements.ts → execution/execution-requirements.ts} +1 -1
  55. package/src/execution/race-against-abort.ts +89 -0
  56. package/src/execution/run-with-middleware.ts +153 -0
  57. package/src/{runtime-core.ts → execution/runtime-core.ts} +27 -3
  58. package/src/execution/runtime-error.ts +94 -0
  59. package/src/execution/runtime-middleware.ts +235 -0
  60. package/src/exports/authoring.ts +5 -2
  61. package/src/exports/codec.ts +27 -2
  62. package/src/exports/components.ts +2 -2
  63. package/src/exports/control.ts +41 -14
  64. package/src/exports/emission.ts +2 -2
  65. package/src/exports/execution.ts +5 -5
  66. package/src/exports/psl-ast.ts +1 -0
  67. package/src/exports/runtime.ts +18 -9
  68. package/src/shared/codec-descriptor.ts +87 -0
  69. package/src/shared/codec-types.ts +79 -0
  70. package/src/shared/codec.ts +80 -0
  71. package/src/shared/column-spec.ts +83 -0
  72. package/src/{framework-authoring.ts → shared/framework-authoring.ts} +210 -23
  73. package/src/{framework-components.ts → shared/framework-components.ts} +22 -49
  74. package/src/{mutation-default-types.ts → shared/mutation-default-types.ts} +22 -2
  75. package/dist/authoring.mjs.map +0 -1
  76. package/dist/codec-types-DQ1Agjom.d.mts +0 -58
  77. package/dist/codec-types-DQ1Agjom.d.mts.map +0 -1
  78. package/dist/emission-types-BPAALJbF.d.mts +0 -24
  79. package/dist/emission-types-BPAALJbF.d.mts.map +0 -1
  80. package/dist/framework-authoring-D1-JZ37B.d.mts.map +0 -1
  81. package/dist/framework-components-C8ZhSwXe.mjs.map +0 -1
  82. package/dist/framework-components-DFZMi2h7.d.mts.map +0 -1
  83. package/dist/types-import-spec-C4sc7wbb.d.mts.map +0 -1
  84. package/src/codec-types.ts +0 -64
  85. package/src/control-capabilities.ts +0 -34
  86. package/src/emission-types.ts +0 -28
  87. package/src/run-with-middleware.ts +0 -77
  88. package/src/runtime-error.ts +0 -55
  89. package/src/runtime-middleware.ts +0 -87
  90. /package/src/{control-result-types.ts → control/control-result-types.ts} +0 -0
  91. /package/src/{control-schema-view.ts → control/control-schema-view.ts} +0 -0
  92. /package/src/{async-iterable-result.ts → execution/async-iterable-result.ts} +0 -0
  93. /package/src/{execution-stack.ts → execution/execution-stack.ts} +0 -0
  94. /package/src/{query-plan.ts → execution/query-plan.ts} +0 -0
  95. /package/src/{types-import-spec.ts → shared/types-import-spec.ts} +0 -0
@@ -1,9 +1,4 @@
1
1
  import type { Contract, ContractMarkerRecord } from '@prisma-next/contract/types';
2
- import type {
3
- SignDatabaseResult,
4
- VerifyDatabaseResult,
5
- VerifyDatabaseSchemaResult,
6
- } from './control-result-types';
7
2
  import type {
8
3
  AdapterInstance,
9
4
  DriverInstance,
@@ -11,7 +6,12 @@ import type {
11
6
  FamilyInstance,
12
7
  TargetBoundComponentDescriptor,
13
8
  TargetInstance,
14
- } from './framework-components';
9
+ } from '../shared/framework-components';
10
+ import type {
11
+ SignDatabaseResult,
12
+ VerifyDatabaseResult,
13
+ VerifyDatabaseSchemaResult,
14
+ } from './control-result-types';
15
15
 
16
16
  export interface ControlFamilyInstance<TFamilyId extends string, TSchemaIR>
17
17
  extends FamilyInstance<TFamilyId> {
@@ -34,6 +34,24 @@ export interface ControlFamilyInstance<TFamilyId extends string, TSchemaIR>
34
34
  readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, string>>;
35
35
  }): Promise<VerifyDatabaseSchemaResult>;
36
36
 
37
+ /**
38
+ * Verify a contract against an already-introspected schema slice.
39
+ *
40
+ * Difference from {@link schemaVerify}: no `driver`, no introspection
41
+ * — the caller hands over the schema directly. Used by the aggregate
42
+ * verifier to invoke the family's verification logic per member,
43
+ * with the schema **pre-projected** to that member's claimed slice
44
+ * via {@link import('@prisma-next/migration-tools/aggregate').projectSchemaToSpace}.
45
+ *
46
+ * Synchronous — no I/O. Idempotent.
47
+ */
48
+ schemaVerifyAgainstSchema(options: {
49
+ readonly contract: unknown;
50
+ readonly schema: TSchemaIR;
51
+ readonly strict: boolean;
52
+ readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, string>>;
53
+ }): VerifyDatabaseSchemaResult;
54
+
37
55
  sign(options: {
38
56
  readonly driver: ControlDriverInstance<TFamilyId, string>;
39
57
  readonly contract: unknown;
@@ -41,10 +59,38 @@ export interface ControlFamilyInstance<TFamilyId extends string, TSchemaIR>
41
59
  readonly configPath?: string;
42
60
  }): Promise<SignDatabaseResult>;
43
61
 
62
+ /**
63
+ * Reads the contract marker for `space` from the database, returning
64
+ * `null` if no marker row exists for that space (or if the marker
65
+ * table itself is missing).
66
+ *
67
+ * `space` is required at every call site so the type system surfaces
68
+ * every place that needs to thread the value: callers in single-app
69
+ * paths pass {@link import('./control-spaces').APP_SPACE_ID}
70
+ * (`'app'`); per-extension callers pass the extension's space id.
71
+ * Defaulting at the family-interface level was a silent bug door —
72
+ * it let multi-space-aware callers forget to pass `space` and
73
+ * collapse onto the app's marker row.
74
+ *
75
+ * Families whose underlying storage doesn't yet support per-space
76
+ * markers (Mongo, today) accept `space` for interface conformance and
77
+ * reject any non-`APP_SPACE_ID` value rather than silently ignoring
78
+ * it; see the family-specific implementation for details.
79
+ */
44
80
  readMarker(options: {
45
81
  readonly driver: ControlDriverInstance<TFamilyId, string>;
82
+ readonly space: string;
46
83
  }): Promise<ContractMarkerRecord | null>;
47
84
 
85
+ /**
86
+ * Reads every marker row keyed by `space`. Used by the per-space
87
+ * verifier to detect orphan marker rows and marker-vs-on-disk drift.
88
+ * Returns an empty map when the marker table does not yet exist.
89
+ */
90
+ readAllMarkers(options: {
91
+ readonly driver: ControlDriverInstance<TFamilyId, string>;
92
+ }): Promise<ReadonlyMap<string, ContractMarkerRecord>>;
93
+
48
94
  introspect(options: {
49
95
  readonly driver: ControlDriverInstance<TFamilyId, string>;
50
96
  readonly contract?: unknown;
@@ -10,9 +10,65 @@
10
10
  */
11
11
 
12
12
  import type { Contract } from '@prisma-next/contract/types';
13
+ import type { ImportRequirement } from '@prisma-next/ts-render';
13
14
  import type { Result } from '@prisma-next/utils/result';
15
+ import type { TargetBoundComponentDescriptor } from '../shared/framework-components';
14
16
  import type { ControlDriverInstance, ControlFamilyInstance } from './control-instances';
15
- import type { TargetBoundComponentDescriptor } from './framework-components';
17
+
18
+ // ============================================================================
19
+ // Migration Package Metadata
20
+ // ============================================================================
21
+
22
+ /**
23
+ * Planner provenance recorded inside {@link MigrationMetadata}.
24
+ *
25
+ * `used` / `applied` track which migration hints the planner consulted
26
+ * vs. which it actually applied during emission; `plannerVersion`
27
+ * pins the planner build that produced the migration so future
28
+ * verification passes can recognise plans authored against an older
29
+ * planner.
30
+ */
31
+ export interface MigrationHints {
32
+ readonly used: readonly string[];
33
+ readonly applied: readonly string[];
34
+ readonly plannerVersion: string;
35
+ }
36
+
37
+ /**
38
+ * In-memory migration metadata envelope. Every migration is
39
+ * content-addressed: the `migrationHash` is a hash over the metadata
40
+ * envelope plus the operations list, computed at write time. There is no
41
+ * draft state — a migration directory either exists with fully attested
42
+ * metadata or it does not.
43
+ *
44
+ * When the planner cannot lower an operation because of an unfilled
45
+ * `placeholder(...)` slot, the migration is still written with
46
+ * `migrationHash` hashed over `ops: []`. Re-running self-emit after the
47
+ * user fills the placeholder produces a *different* `migrationHash`
48
+ * (committed to the real ops); this is intentional.
49
+ *
50
+ * The on-disk JSON shape in `migration.json` matches this type
51
+ * field-for-field — `JSON.stringify(metadata, null, 2)` is the canonical
52
+ * writer output (defined in `@prisma-next/migration-tools/io`).
53
+ */
54
+ export interface MigrationMetadata {
55
+ readonly migrationHash: string;
56
+ readonly from: string | null;
57
+ readonly to: string;
58
+ readonly fromContract: Contract | null;
59
+ readonly toContract: Contract;
60
+ readonly hints: MigrationHints;
61
+ readonly labels: readonly string[];
62
+ /**
63
+ * Sorted, deduplicated list of `invariantId`s declared by the
64
+ * migration's data-transform ops. Always present; an empty array
65
+ * means the migration has no routing-visible data transforms.
66
+ */
67
+ readonly providedInvariants: readonly string[];
68
+ readonly authorship?: { readonly author?: string; readonly email?: string };
69
+ readonly signature?: { readonly keyId: string; readonly value: string } | null;
70
+ readonly createdAt: string;
71
+ }
16
72
 
17
73
  // ============================================================================
18
74
  // Operation Classes and Policy
@@ -28,61 +84,23 @@ import type { TargetBoundComponentDescriptor } from './framework-components';
28
84
  export type MigrationOperationClass = 'additive' | 'widening' | 'destructive' | 'data';
29
85
 
30
86
  // ============================================================================
31
- // Data Transform Operation
87
+ // Serialized Query Plan
32
88
  // ============================================================================
33
89
 
34
90
  /**
35
91
  * A lowered query statement as stored in ops.json.
36
92
  * Contains the SQL string and parameter values — ready for execution.
37
93
  * Lowering from query builder AST to SQL happens at verify time.
94
+ *
95
+ * The Postgres `dataTransform` factory uses this shape internally to
96
+ * carry the user's lowered `check`/`run` plans before wrapping them
97
+ * into precheck/execute/postcheck steps on the unified migration op.
38
98
  */
39
99
  export interface SerializedQueryPlan {
40
100
  readonly sql: string;
41
101
  readonly params: readonly unknown[];
42
102
  }
43
103
 
44
- /**
45
- * A data transform operation within a migration edge.
46
- *
47
- * Data transforms are authored in TypeScript using the query builder,
48
- * serialized to JSON ASTs at verification time, and rendered to SQL
49
- * by the target adapter at apply time.
50
- *
51
- * The `name` serves as the invariant identity — it's recorded in the
52
- * ledger and used for invariant-aware routing via environment refs.
53
- *
54
- * In draft state (before verification), `check` and `run` are null.
55
- * After verification, they contain the serialized query ASTs.
56
- */
57
- export interface DataTransformOperation extends MigrationPlanOperation {
58
- readonly operationClass: 'data';
59
- /**
60
- * The invariant name for this data transform.
61
- * Recorded in the ledger on successful edge completion.
62
- * Used by environment refs to declare required invariants.
63
- */
64
- readonly name: string;
65
- /**
66
- * Path to the TypeScript source file that produced this operation.
67
- * Not part of edgeId computation — for traceability only.
68
- */
69
- readonly source: string;
70
- /**
71
- * Serialized check query plan, or a boolean literal.
72
- * - SerializedQueryPlan: describes violations; empty result = already applied.
73
- * - false: always run (no check).
74
- * - true: always skip.
75
- * - null: not yet serialized (draft state).
76
- */
77
- readonly check: SerializedQueryPlan | boolean | null;
78
- /**
79
- * Serialized run query plans.
80
- * - Array of serialized query plans to execute sequentially.
81
- * - null: not yet serialized (draft state).
82
- */
83
- readonly run: readonly SerializedQueryPlan[] | null;
84
- }
85
-
86
104
  /**
87
105
  * Policy defining which operation classes are allowed during a migration.
88
106
  */
@@ -105,6 +123,17 @@ export interface MigrationPlanOperation {
105
123
  readonly label: string;
106
124
  /** The class of operation (additive, widening, destructive). */
107
125
  readonly operationClass: MigrationOperationClass;
126
+ /**
127
+ * Optional opt-in routing identity for data-transform operations.
128
+ * Presence opts the transform into invariant-aware routing; absence
129
+ * means it is path-dependent and not referenceable from refs.
130
+ *
131
+ * Lives on the base op so the manifest emitter and
132
+ * `deriveProvidedInvariants` can read it without depending on a
133
+ * target-specific shape. Schema-DDL ops (additive / widening /
134
+ * destructive) leave it undefined.
135
+ */
136
+ readonly invariantId?: string;
108
137
  }
109
138
 
110
139
  // ============================================================================
@@ -112,9 +141,33 @@ export interface MigrationPlanOperation {
112
141
  // ============================================================================
113
142
 
114
143
  /**
115
- * Framework-level contract for a single factory call in a target's planner IR.
144
+ * Framework-level contract for a single factory call in a target's planner
145
+ * IR — the canonical shape for any node participating in the two-renderer
146
+ * pattern (source-text rendering for `migration.ts` + runtime-op derivation
147
+ * for `ops.json`).
148
+ *
149
+ * Implementations declare:
150
+ *
151
+ * - **Identity / display metadata** (`factoryName`, `operationClass`,
152
+ * `label`) used by CLI summaries and the issue planner.
153
+ * - **`renderTypeScript()`** — emit the call as a TypeScript expression
154
+ * suitable for inclusion in a generated `migration.ts`. Polymorphic
155
+ * across postgres / mongo / sqlite / extension-owned calls.
156
+ * - **`importRequirements()`** — the symbols this rendered expression
157
+ * pulls in. Aggregated and deduplicated by the top-level renderer
158
+ * into a single import block per file.
159
+ * - **`toOp()`** — lower the call to a runtime
160
+ * `MigrationPlanOperation`. Returns the framework base; concrete
161
+ * implementations narrow via covariant return (e.g. SQL targets
162
+ * return `SqlMigrationPlanOperation<TTargetDetails>`).
116
163
  *
117
- * @see ADR 195
164
+ * Each domain (target, extension) defines its own set of concrete `*Call`
165
+ * classes that implement this interface — typically by extending
166
+ * {@link import('@prisma-next/ts-render').TsExpression} and adding the
167
+ * concrete `toOp()` body. Extensions can implement the interface
168
+ * directly without depending on a target's package-private base.
169
+ *
170
+ * @see ADR 195 — Planner IR with two renderers.
118
171
  */
119
172
  export interface OpFactoryCall {
120
173
  /** The name of the factory that would produce this call's runtime op. */
@@ -123,6 +176,26 @@ export interface OpFactoryCall {
123
176
  readonly operationClass: MigrationOperationClass;
124
177
  /** Human-readable label for CLI output and diagnostics. */
125
178
  readonly label: string;
179
+ /**
180
+ * Render this call as a TypeScript expression suitable for inclusion in
181
+ * a generated `migration.ts`. The output is composed alongside other
182
+ * calls' rendered expressions inside the migration's `operations`
183
+ * array.
184
+ */
185
+ renderTypeScript(): string;
186
+ /**
187
+ * Import requirements pulled in by the rendered TypeScript expression.
188
+ * Aggregated and deduplicated across all calls into a single import
189
+ * block per file.
190
+ */
191
+ importRequirements(): readonly ImportRequirement[];
192
+ /**
193
+ * Lower this call to a runtime migration plan operation suitable for
194
+ * execution / inclusion in `ops.json`. Concrete implementations narrow
195
+ * the return type via covariant return (e.g. SQL targets return
196
+ * `SqlMigrationPlanOperation<TTargetDetails>`).
197
+ */
198
+ toOp(): MigrationPlanOperation;
126
199
  }
127
200
 
128
201
  // ============================================================================
@@ -136,6 +209,16 @@ export interface OpFactoryCall {
136
209
  export interface MigrationPlan {
137
210
  /** The target ID this plan is for (e.g., 'postgres'). */
138
211
  readonly targetId: string;
212
+ /**
213
+ * Contract space this plan applies to. Runners cross-check
214
+ * `options.space` against `plan.spaceId` so the marker row gets keyed
215
+ * by the right space when applying via `executeAcrossSpaces`.
216
+ *
217
+ * Optional for backward compatibility with single-space callers that
218
+ * pre-date the contract-space aggregate; when present, runners
219
+ * enforce that it matches `options.space`.
220
+ */
221
+ readonly spaceId?: string;
139
222
  /**
140
223
  * Origin contract identity that the plan expects the database to currently be at.
141
224
  * If omitted or null, the runner skips origin validation entirely.
@@ -151,6 +234,17 @@ export interface MigrationPlan {
151
234
  };
152
235
  /** Ordered list of operations to execute. */
153
236
  readonly operations: readonly MigrationPlanOperation[];
237
+ /**
238
+ * Sorted, deduplicated invariant ids declared by this plan's data-transform
239
+ * ops. Authored migrations carry the canonical value from
240
+ * `migration.json.providedInvariants`; planner-built plans (`db init`,
241
+ * `db update`) omit it (the runner treats it as `[]`). Runners read this
242
+ * field for marker writes and self-edge no-op detection rather than
243
+ * re-deriving from `operations`, since the manifest is the canonical
244
+ * source for the invariant set across all runners (postgres, sqlite,
245
+ * mongo).
246
+ */
247
+ readonly providedInvariants?: readonly string[];
154
248
  }
155
249
 
156
250
  /**
@@ -289,21 +383,23 @@ export interface MigrationPlanner<
289
383
  readonly contract: unknown;
290
384
  readonly schema: unknown;
291
385
  readonly policy: MigrationOperationPolicy;
292
- /**
293
- * Storage hash of the "from" contract (the state the planner assumes the
294
- * database starts at). Planners use this to populate `describe()` on the
295
- * produced plan so the rendered `migration.ts` has correct `from`/`to`
296
- * metadata.
297
- */
298
- readonly fromHash: string;
299
386
  /**
300
387
  * The "from" contract (the state the planner assumes the database starts
301
- * at). Planners pass this to data-safety strategies so they can compare
302
- * `from` and `to` column shapes (e.g. to detect unsafe type changes).
303
- * `db update` / `db init` reconcile against the live schema and have no
304
- * "from" contract; only `migration plan` provides one.
388
+ * at), or `null` for a baseline plan with no prior state.
389
+ *
390
+ * Planners derive any "from" identity they need to stamp onto the
391
+ * produced plan's `describe()` from `fromContract?.storage.storageHash
392
+ * ?? null`. They also pass this to data-safety strategies so they can
393
+ * compare `from` and `to` column shapes (e.g. to detect unsafe type
394
+ * changes).
395
+ *
396
+ * Required at every call site to make the structural fact "I have a
397
+ * prior contract / I don't" visible in the type. Reconciliation
398
+ * commands (`db init`, `db update`) introspect a live schema and pass
399
+ * `null`; authoring commands (`migration plan`) pass the previous
400
+ * bundle's `metadata.toContract`.
305
401
  */
306
- readonly fromContract?: unknown;
402
+ readonly fromContract: Contract | null;
307
403
  /**
308
404
  * Active framework components participating in this composition.
309
405
  * Families/targets can interpret this list to derive family-specific metadata.
@@ -312,6 +408,13 @@ export interface MigrationPlanner<
312
408
  readonly frameworkComponents: ReadonlyArray<
313
409
  TargetBoundComponentDescriptor<TFamilyId, TTargetId>
314
410
  >;
411
+ /**
412
+ * Contract space this plan applies to. Stamped onto the produced
413
+ * plan so the runner keys the marker row by the right space when
414
+ * executing. App-plan callers pass `APP_SPACE_ID` (`'app'`);
415
+ * per-extension callers pass the extension's space id.
416
+ */
417
+ readonly spaceId: string;
315
418
  }): MigrationPlannerResult;
316
419
 
317
420
  /**
@@ -320,8 +423,15 @@ export interface MigrationPlanner<
320
423
  * Used by `migration new` to scaffold a fresh `migration.ts`. The
321
424
  * returned plan has no operations; its `renderTypeScript()` yields a
322
425
  * stub the user can edit.
426
+ *
427
+ * `spaceId` is stamped onto the produced plan; reconciliation flows
428
+ * (`db init`, `db update`) and authoring flows (`migration new`) all
429
+ * pass it explicitly.
323
430
  */
324
- emptyMigration(context: MigrationScaffoldContext): MigrationPlanWithAuthoringSurface;
431
+ emptyMigration(
432
+ context: MigrationScaffoldContext,
433
+ spaceId: string,
434
+ ): MigrationPlanWithAuthoringSurface;
325
435
  }
326
436
 
327
437
  /**
@@ -335,6 +445,16 @@ export interface MigrationRunner<
335
445
  TFamilyId extends string = string,
336
446
  TTargetId extends string = string,
337
447
  > {
448
+ /**
449
+ * Execute a migration plan against the configured driver.
450
+ *
451
+ * The `plan` parameter is trusted input. Callers are responsible for
452
+ * upstream verification of the originating migration package — typically
453
+ * by obtaining the package via `readMigrationPackage` from
454
+ * `@prisma-next/migration-tools/io`, which performs hash-integrity checks
455
+ * at the load boundary. Runners do not re-verify the plan and assume the
456
+ * `(metadata, ops)` pair on disk has not been tampered with since emit.
457
+ */
338
458
  execute(options: {
339
459
  readonly plan: MigrationPlan;
340
460
  readonly driver: ControlDriverInstance<TFamilyId, TTargetId>;
@@ -360,6 +480,73 @@ export interface MigrationRunner<
360
480
  }): Promise<MigrationRunnerResult>;
361
481
  }
362
482
 
483
+ // ============================================================================
484
+ // Multi-space runner protocol (extension contract spaces, TML-2397)
485
+ // ============================================================================
486
+
487
+ /**
488
+ * Per-space input for {@link MultiSpaceCapableRunner.executeAcrossSpaces}.
489
+ *
490
+ * Mirrors the single-space `MigrationRunner.execute` options, extended with a
491
+ * required `space` identifier. Each entry's `driver` must reference the same
492
+ * connection the outer transaction is opened on (typically the same value as
493
+ * the top-level `driver` on `executeAcrossSpaces`).
494
+ *
495
+ * Family-specific runners (e.g. the SQL family's `SqlMigrationRunner`) define
496
+ * a richer per-space option shape that is structurally compatible with this
497
+ * one — additional optional fields (e.g. SQL's `strictVerification`,
498
+ * `schemaName`, `callbacks`) are tolerated by the underlying runner without
499
+ * affecting cross-target wiring.
500
+ */
501
+ export interface MultiSpaceRunnerPerSpaceOptions<
502
+ TFamilyId extends string = string,
503
+ TTargetId extends string = string,
504
+ > {
505
+ readonly space: string;
506
+ readonly plan: MigrationPlan;
507
+ readonly driver: ControlDriverInstance<TFamilyId, TTargetId>;
508
+ readonly destinationContract: unknown;
509
+ readonly policy: MigrationOperationPolicy;
510
+ readonly executionChecks?: MigrationRunnerExecutionChecks;
511
+ readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<TFamilyId, TTargetId>>;
512
+ }
513
+
514
+ export interface MultiSpaceRunnerSuccessValue {
515
+ readonly perSpaceResults: ReadonlyArray<{
516
+ readonly space: string;
517
+ readonly value: MigrationRunnerSuccessValue;
518
+ }>;
519
+ }
520
+
521
+ export interface MultiSpaceRunnerFailure extends MigrationRunnerFailure {
522
+ /** Identifier of the space whose plan caused the rollback. */
523
+ readonly failingSpace: string;
524
+ }
525
+
526
+ export type MultiSpaceRunnerResult = Result<MultiSpaceRunnerSuccessValue, MultiSpaceRunnerFailure>;
527
+
528
+ /**
529
+ * Optional capability for runners that can apply a list of per-space plans
530
+ * inside a single outer transaction. A failure on any space rolls back every
531
+ * space's writes.
532
+ *
533
+ * The SQL family (`SqlMigrationRunner`) implements this with a true outer
534
+ * transaction across every space. The Mongo family implements a degenerate
535
+ * single-space shim (per-space is a non-goal per the extension-contract-spaces
536
+ * project spec — Mongo aggregates are always single-member). The capability
537
+ * is declared at the framework layer so CLI utilities can route through it
538
+ * without importing any specific family directly.
539
+ */
540
+ export interface MultiSpaceCapableRunner<
541
+ TFamilyId extends string = string,
542
+ TTargetId extends string = string,
543
+ > {
544
+ executeAcrossSpaces(options: {
545
+ readonly driver: ControlDriverInstance<TFamilyId, TTargetId>;
546
+ readonly perSpaceOptions: ReadonlyArray<MultiSpaceRunnerPerSpaceOptions<TFamilyId, TTargetId>>;
547
+ }): Promise<MultiSpaceRunnerResult>;
548
+ }
549
+
363
550
  // ============================================================================
364
551
  // Target Migrations Capability
365
552
  // ============================================================================
@@ -414,11 +601,12 @@ export interface MigrationScaffoldContext {
414
601
  /** Absolute path to the contract.json file, if one exists. Used by targets that emit typed-contract imports. */
415
602
  readonly contractJsonPath?: string;
416
603
  /**
417
- * Storage hash of the "from" contract. Targets use this to populate
418
- * `describe()` on the rendered empty migration so that identity metadata
419
- * is correctly populated.
604
+ * Storage hash of the "from" contract, or `null` for a baseline scaffold
605
+ * with no prior state. Targets use this to populate `describe()` on the
606
+ * rendered empty migration so that identity metadata is correctly
607
+ * populated.
420
608
  */
421
- readonly fromHash: string;
609
+ readonly fromHash: string | null;
422
610
  /**
423
611
  * Storage hash of the "to" contract. Same purpose as `fromHash` — threaded
424
612
  * through so the rendered class's `describe()` declares the correct
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Family-agnostic textual preview of a migration plan, used by the CLI to
3
+ * render a "DDL preview" section for `db init` / `db update` / `migration plan`
4
+ * / `migration show`. Each statement carries a free-form `language` tag so
5
+ * formatters can suffix `;` for SQL but render Mongo shell lines verbatim.
6
+ *
7
+ * Producers are family-specific: SQL emits `language: 'sql'` (existing DDL
8
+ * extraction); Mongo emits `language: 'mongodb-shell'` via the
9
+ * `MongoDdlCommandFormatter` visitor.
10
+ *
11
+ * The capability `OperationPreviewCapable` (declared in
12
+ * `./control-capabilities`) is how a family announces it can produce these.
13
+ */
14
+
15
+ export interface OperationPreviewStatement {
16
+ readonly text: string;
17
+ /** Dialect identifier, e.g. `'sql'`, `'mongodb-shell'`. Free-form by design (OQ-3). */
18
+ readonly language: string;
19
+ }
20
+
21
+ export interface OperationPreview {
22
+ readonly statements: readonly OperationPreviewStatement[];
23
+ }
@@ -0,0 +1,82 @@
1
+ import type { Contract } from '@prisma-next/contract/types';
2
+ import type { MigrationMetadata, MigrationPlanOperation } from './control-migration-types';
3
+
4
+ /**
5
+ * Canonical control-plane identifiers for contract spaces.
6
+ *
7
+ * A contract space is the disjoint `(contract.json, migration-graph)` unit
8
+ * the per-space planner / runner / verifier (project: extension contract
9
+ * spaces, TML-2397) operates on. The application owns one well-known
10
+ * space — the value below — and each loaded extension that contributes
11
+ * schema owns a uniquely-named space.
12
+ *
13
+ * Lives in `framework-components/control` so every layer that has to
14
+ * reason about space identity (the migration tooling, the SQL runtime's
15
+ * marker reader, target-side statement builders, target-side adapters)
16
+ * can import a single value rather than duplicating the literal. Raw
17
+ * `'app'` string literals in framework / target / runtime / adapter
18
+ * source code are forbidden and policed by
19
+ * `scripts/lint-app-space-id.mjs` (wired into `pnpm lint:deps`).
20
+ *
21
+ * @see specs/framework-mechanism.spec.md § 3 — Layout convention (γ).
22
+ */
23
+ export const APP_SPACE_ID = 'app' as const;
24
+
25
+ /**
26
+ * Head ref for a contract space — the `(hash, invariants)` tuple
27
+ * a runner targets when applying that space's migration graph. Identical
28
+ * in shape to the on-disk `migrations/<space-id>/refs/head.json` the
29
+ * framework writes per loaded extension, and to the app-space
30
+ * `<projectRoot>/refs/head.json`. Family-agnostic: SQL, Mongo, and any
31
+ * future family share the same head-ref shape.
32
+ *
33
+ * @see specs/framework-mechanism.spec.md § 1.
34
+ */
35
+ export interface ContractSpaceHeadRef {
36
+ readonly hash: string;
37
+ readonly invariants: readonly string[];
38
+ }
39
+
40
+ /**
41
+ * Canonical structural shape of a migration package — the unit a planner
42
+ * produces and a runner consumes: a directory name, the ADR 197 metadata
43
+ * envelope (which carries the `toContract` snapshot), and the operation
44
+ * list.
45
+ *
46
+ * In-memory by default. Readers in `@prisma-next/migration-tools`
47
+ * (`readMigrationPackage` / `readMigrationsDir`) return the augmented
48
+ * {@link import('@prisma-next/migration-tools/package').OnDiskMigrationPackage}
49
+ * variant which adds `dirPath`; everything else operates against the
50
+ * canonical shape so the same value flows through pre-emission
51
+ * authoring, on-disk loading, and runner execution without conversion.
52
+ *
53
+ * @see specs/framework-mechanism.spec.md § 1.
54
+ */
55
+ export interface MigrationPackage {
56
+ readonly dirName: string;
57
+ readonly metadata: MigrationMetadata;
58
+ readonly ops: readonly MigrationPlanOperation[];
59
+ }
60
+
61
+ /**
62
+ * Canonical structural shape of a contract space — one disjoint
63
+ * `(contractJson, migration-graph)` unit the per-space planner / runner
64
+ * / verifier operates on. The application owns one well-known space
65
+ * ({@link APP_SPACE_ID}); each loaded extension that contributes schema
66
+ * owns a uniquely-named space. Whether a value is the app's space or an
67
+ * extension's space is a control-plane concern; the type carries no
68
+ * such distinction.
69
+ *
70
+ * Generic over the contract so each family pins a typed contract value
71
+ * at consumption time. The SQL family specialises to
72
+ * `ContractSpace<Contract<SqlStorage>>` at the descriptor surface;
73
+ * Mongo's symmetrical `ContractSpace<Contract<MongoStorage>>` will land
74
+ * with that family.
75
+ *
76
+ * @see specs/framework-mechanism.spec.md § 1.
77
+ */
78
+ export interface ContractSpace<TContract extends Contract = Contract> {
79
+ readonly contractJson: TContract;
80
+ readonly migrations: readonly MigrationPackage[];
81
+ readonly headRef: ContractSpaceHeadRef;
82
+ }