@prisma-next/cli 0.8.0-dev.9 → 0.9.0-dev.1

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 (71) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.mjs +5 -5
  3. package/dist/{client-XkUw4xD0.mjs → client-Brv4qlfB.mjs} +13 -19
  4. package/dist/client-Brv4qlfB.mjs.map +1 -0
  5. package/dist/commands/contract-emit.mjs +1 -1
  6. package/dist/commands/contract-infer.mjs +1 -1
  7. package/dist/commands/db-init.mjs +2 -2
  8. package/dist/commands/db-schema.mjs +1 -1
  9. package/dist/commands/db-sign.mjs +1 -1
  10. package/dist/commands/db-update.mjs +2 -2
  11. package/dist/commands/db-verify.d.mts.map +1 -1
  12. package/dist/commands/db-verify.mjs +1 -1
  13. package/dist/commands/migrate.d.mts +1 -1
  14. package/dist/commands/migrate.d.mts.map +1 -1
  15. package/dist/commands/migrate.mjs +12 -4
  16. package/dist/commands/migrate.mjs.map +1 -1
  17. package/dist/commands/migration-log.mjs +1 -1
  18. package/dist/commands/migration-new.mjs +9 -9
  19. package/dist/commands/migration-new.mjs.map +1 -1
  20. package/dist/commands/migration-plan.d.mts.map +1 -1
  21. package/dist/commands/migration-plan.mjs +1 -1
  22. package/dist/commands/migration-show.d.mts.map +1 -1
  23. package/dist/commands/migration-show.mjs +7 -7
  24. package/dist/commands/migration-show.mjs.map +1 -1
  25. package/dist/commands/migration-status.d.mts +2 -2
  26. package/dist/commands/migration-status.d.mts.map +1 -1
  27. package/dist/commands/migration-status.mjs +5 -5
  28. package/dist/commands/migration-status.mjs.map +1 -1
  29. package/dist/{contract-emit-GpxW5RLe.mjs → contract-emit-C3STUIBg.mjs} +2 -2
  30. package/dist/{contract-emit-GpxW5RLe.mjs.map → contract-emit-C3STUIBg.mjs.map} +1 -1
  31. package/dist/{contract-emit-CgoFk9AU.mjs → contract-emit-iynA3BCA.mjs} +2 -2
  32. package/dist/{contract-emit-CgoFk9AU.mjs.map → contract-emit-iynA3BCA.mjs.map} +1 -1
  33. package/dist/{contract-infer-D8edZOCi.mjs → contract-infer-Cnj8G1E2.mjs} +2 -2
  34. package/dist/{contract-infer-D8edZOCi.mjs.map → contract-infer-Cnj8G1E2.mjs.map} +1 -1
  35. package/dist/{contract-space-aggregate-loader-D68YpuPR.mjs → contract-space-aggregate-loader-pAc8CDfY.mjs} +2 -2
  36. package/dist/{contract-space-aggregate-loader-D68YpuPR.mjs.map → contract-space-aggregate-loader-pAc8CDfY.mjs.map} +1 -1
  37. package/dist/{db-verify-DtRB9iHJ.mjs → db-verify-D7cyH_zz.mjs} +7 -4
  38. package/dist/db-verify-D7cyH_zz.mjs.map +1 -0
  39. package/dist/exports/control-api.d.mts +1 -1
  40. package/dist/exports/control-api.mjs +2 -2
  41. package/dist/exports/index.mjs +1 -1
  42. package/dist/exports/index.mjs.map +1 -1
  43. package/dist/{init-BU2G31T8.mjs → init-Bqg5JWg7.mjs} +3 -3
  44. package/dist/{init-BU2G31T8.mjs.map → init-Bqg5JWg7.mjs.map} +1 -1
  45. package/dist/{inspect-live-schema-CPPqCips.mjs → inspect-live-schema-CWLK_lgs.mjs} +2 -2
  46. package/dist/{inspect-live-schema-CPPqCips.mjs.map → inspect-live-schema-CWLK_lgs.mjs.map} +1 -1
  47. package/dist/{migration-command-scaffold-B_ezTTwX.mjs → migration-command-scaffold-CmXXC1UZ.mjs} +2 -2
  48. package/dist/{migration-command-scaffold-B_ezTTwX.mjs.map → migration-command-scaffold-CmXXC1UZ.mjs.map} +1 -1
  49. package/dist/{migration-plan-DWB-NTxH.mjs → migration-plan-CHyUlBV0.mjs} +34 -27
  50. package/dist/migration-plan-CHyUlBV0.mjs.map +1 -0
  51. package/dist/{types-BS_wpjAY.d.mts → types-0aS865QN.d.mts} +13 -7
  52. package/dist/types-0aS865QN.d.mts.map +1 -0
  53. package/package.json +17 -17
  54. package/src/commands/db-verify.ts +19 -3
  55. package/src/commands/migrate.ts +23 -3
  56. package/src/commands/migration-new.ts +13 -13
  57. package/src/commands/migration-plan.ts +50 -31
  58. package/src/commands/migration-show.ts +9 -5
  59. package/src/commands/migration-status.ts +5 -5
  60. package/src/control-api/client.ts +19 -17
  61. package/src/control-api/operations/contract-emit.ts +13 -2
  62. package/src/control-api/operations/db-apply-aggregate.ts +1 -1
  63. package/src/control-api/operations/db-verify.ts +1 -1
  64. package/src/control-api/operations/migration-apply.ts +1 -1
  65. package/src/control-api/types.ts +13 -7
  66. package/src/load-ts-contract.ts +9 -1
  67. package/src/utils/contract-space-aggregate-loader.ts +2 -2
  68. package/dist/client-XkUw4xD0.mjs.map +0 -1
  69. package/dist/db-verify-DtRB9iHJ.mjs.map +0 -1
  70. package/dist/migration-plan-DWB-NTxH.mjs.map +0 -1
  71. package/dist/types-BS_wpjAY.d.mts.map +0 -1
@@ -8,7 +8,7 @@
8
8
  * verbatim.
9
9
  */
10
10
 
11
- import { readFileSync } from 'node:fs';
11
+ import { readFile } from 'node:fs/promises';
12
12
  import type { Contract } from '@prisma-next/contract/types';
13
13
  import { getEmittedArtifactPaths } from '@prisma-next/emitter';
14
14
  import { APP_SPACE_ID, createControlStack } from '@prisma-next/framework-components/control';
@@ -73,11 +73,17 @@ async function executeMigrationNewCommand(
73
73
  const config = await loadConfig(options.config);
74
74
  const { appMigrationsDir, appMigrationsRelative } = resolveMigrationPaths(options.config, config);
75
75
 
76
+ // Construct the family instance up-front so the on-disk contract read
77
+ // below crosses the serializer seam (`familyInstance.deserializeContract`)
78
+ // at the read site, not somewhere downstream. See TML-2536.
79
+ const stack = createControlStack(config);
80
+ const familyInstance = config.family.create(stack);
81
+
76
82
  const contractPathAbsolute = resolveContractPath(config);
77
83
 
78
84
  let contractJsonContent: string;
79
85
  try {
80
- contractJsonContent = readFileSync(contractPathAbsolute, 'utf-8');
86
+ contractJsonContent = await readFile(contractPathAbsolute, 'utf-8');
81
87
  } catch (error) {
82
88
  if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
83
89
  return notOk(
@@ -90,24 +96,20 @@ async function executeMigrationNewCommand(
90
96
  throw error;
91
97
  }
92
98
 
93
- let toContractJson: Contract;
99
+ let toContract: Contract;
94
100
  try {
95
- toContractJson = JSON.parse(contractJsonContent) as Contract;
101
+ toContract = familyInstance.deserializeContract(JSON.parse(contractJsonContent) as unknown);
96
102
  } catch (error) {
97
103
  return notOk(
98
104
  errorRuntime('Contract JSON is invalid', {
99
- why: `Failed to parse ${contractPathAbsolute}: ${error instanceof Error ? error.message : String(error)}`,
105
+ why: `Failed to deserialize ${contractPathAbsolute}: ${error instanceof Error ? error.message : String(error)}`,
100
106
  fix: 'Run `prisma-next contract emit` to regenerate the contract',
101
107
  }),
102
108
  );
103
109
  }
104
110
 
105
- const toStorageHash = (
106
- (toContractJson as unknown as Record<string, unknown>)['storage'] as
107
- | Record<string, unknown>
108
- | undefined
109
- )?.['storageHash'] as string | undefined;
110
- if (!toStorageHash) {
111
+ const toStorageHash = toContract.storage?.storageHash;
112
+ if (typeof toStorageHash !== 'string') {
111
113
  return notOk(
112
114
  errorRuntime('Contract is missing storageHash', {
113
115
  why: `Contract at ${contractPathAbsolute} has no storageHash`,
@@ -236,8 +238,6 @@ async function executeMigrationNewCommand(
236
238
  }
237
239
  }
238
240
 
239
- const stack = createControlStack(config);
240
- const familyInstance = config.family.create(stack);
241
241
  const planner = migrations.createPlanner(familyInstance);
242
242
  const emptyPlan = planner.emptyMigration(
243
243
  {
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
2
2
  import type { Contract } from '@prisma-next/contract/types';
3
3
  import { getEmittedArtifactPaths } from '@prisma-next/emitter';
4
4
  import {
5
+ type ControlFamilyInstance,
5
6
  createControlStack,
6
7
  hasOperationPreview,
7
8
  type MigrationPlanOperation,
@@ -62,16 +63,27 @@ interface MigrationPlanOptions extends CommonCommandOptions {
62
63
 
63
64
  /**
64
65
  * Load a predecessor migration's destination contract from its sibling
65
- * `end-contract.json` on disk. The manifest no longer inlines the
66
- * contract; the planner reads it from the canonical on-disk artefact
67
- * authored by a previous `migration plan` run.
66
+ * `end-contract.json` on disk and route it through the family's
67
+ * `ContractSerializer` (via `deserializeContract`) so the in-memory shape
68
+ * is the hydrated `Contract` every other caller sees. Bypassing this
69
+ * seam was the root cause of TML-2536: a raw `JSON.parse(...) as Contract`
70
+ * here let polymorphic `storage.types` entries reach the planner without
71
+ * the `kind` discriminator the planner dispatches on.
68
72
  *
69
- * Throws `CliStructuredError` with `errorFileNotFound` when the
70
- * sibling file is missing — the user has likely deleted or never
71
- * authored the snapshot, and the message names the file and points
72
- * them at re-emitting from the source.
73
+ * Throws `CliStructuredError` with:
74
+ * - `errorFileNotFound` when the sibling file is missing — the user
75
+ * has likely deleted or never authored the snapshot, and the
76
+ * message names the file and points them at re-emitting from the
77
+ * source.
78
+ * - `errorContractValidationFailed` when the JSON parses but the
79
+ * family deserializer rejects it (legacy untagged shape, structural
80
+ * mismatch, etc.) — the message names the predecessor's path so
81
+ * the operator can locate the bad snapshot.
73
82
  */
74
- async function readPredecessorEndContract(migrationDir: string): Promise<Contract> {
83
+ async function readPredecessorEndContract(
84
+ migrationDir: string,
85
+ familyInstance: ControlFamilyInstance<string, unknown>,
86
+ ): Promise<Contract> {
75
87
  const path = join(migrationDir, 'end-contract.json');
76
88
  let raw: string;
77
89
  try {
@@ -85,7 +97,17 @@ async function readPredecessorEndContract(migrationDir: string): Promise<Contrac
85
97
  }
86
98
  throw error;
87
99
  }
88
- return JSON.parse(raw) as Contract;
100
+ try {
101
+ return familyInstance.deserializeContract(JSON.parse(raw) as unknown);
102
+ } catch (error) {
103
+ if (CliStructuredError.is(error)) {
104
+ throw error;
105
+ }
106
+ throw errorContractValidationFailed(
107
+ `Predecessor contract at ${path} failed to deserialize: ${error instanceof Error ? error.message : String(error)}`,
108
+ { where: { path } },
109
+ );
110
+ }
89
111
  }
90
112
 
91
113
  export interface MigrationPlanResult {
@@ -185,19 +207,26 @@ async function executeMigrationPlanCommand(
185
207
  );
186
208
  }
187
209
 
188
- let toContractJson: Contract;
210
+ // Construct the family instance up-front so on-disk reads (the app
211
+ // contract here + every `readPredecessorEndContract` below) cross the
212
+ // serializer seam at the read site, not after the planner has already
213
+ // started dispatching on raw shapes. See TML-2536.
214
+ const stack = createControlStack(config);
215
+ const familyInstance = config.family.create(stack);
216
+
217
+ let toContract: Contract;
189
218
  try {
190
- toContractJson = JSON.parse(contractJsonContent) as Contract;
219
+ toContract = familyInstance.deserializeContract(JSON.parse(contractJsonContent) as unknown);
191
220
  } catch (error) {
192
221
  return notOk(
193
222
  errorContractValidationFailed(
194
- `Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`,
223
+ `Contract at ${contractPathAbsolute} failed to deserialize: ${error instanceof Error ? error.message : String(error)}`,
195
224
  { where: { path: contractPathAbsolute } },
196
225
  ),
197
226
  );
198
227
  }
199
228
 
200
- const rawStorageHash = toContractJson.storage?.storageHash;
229
+ const rawStorageHash = toContract.storage?.storageHash;
201
230
  if (typeof rawStorageHash !== 'string') {
202
231
  return notOk(
203
232
  errorContractValidationFailed('Contract is missing storageHash', {
@@ -235,7 +264,7 @@ async function executeMigrationPlanCommand(
235
264
  );
236
265
  }
237
266
  fromContractSourceDir = matchingBundle.dirPath;
238
- fromContract = await readPredecessorEndContract(fromContractSourceDir);
267
+ fromContract = await readPredecessorEndContract(fromContractSourceDir, familyInstance);
239
268
  } else {
240
269
  const latestMigration = findLatestMigration(graph);
241
270
  if (latestMigration) {
@@ -245,7 +274,7 @@ async function executeMigrationPlanCommand(
245
274
  );
246
275
  if (leafPkg) {
247
276
  fromContractSourceDir = leafPkg.dirPath;
248
- fromContract = await readPredecessorEndContract(fromContractSourceDir);
277
+ fromContract = await readPredecessorEndContract(fromContractSourceDir, familyInstance);
249
278
  }
250
279
  }
251
280
  }
@@ -324,26 +353,16 @@ async function executeMigrationPlanCommand(
324
353
  // Phase 2 — load: build the aggregate against the now-consistent disk
325
354
  // state that phase 1 just seeded. The seed phase guarantees every
326
355
  // declared extension has its head ref pinned, so the loader's
327
- // declaredButUnmigrated precheck always passes here.
328
- const stack = createControlStack(config);
329
- const familyInstance = config.family.create(stack);
330
- let validatedAppContract: Contract;
331
- try {
332
- validatedAppContract = familyInstance.validateContract(toContractJson);
333
- } catch (error) {
334
- return notOk(
335
- errorContractValidationFailed(
336
- `Contract validation failed: ${error instanceof Error ? error.message : String(error)}`,
337
- { where: { path: contractPathAbsolute } },
338
- ),
339
- );
340
- }
356
+ // declaredButUnmigrated precheck always passes here. The app contract
357
+ // was already routed through `familyInstance.deserializeContract` at the
358
+ // read site above (see TML-2536), so it's the hydrated `Contract`
359
+ // here — no second validation pass needed.
341
360
  const aggregateResult = await buildContractSpaceAggregate({
342
361
  targetId: config.target.targetId,
343
362
  migrationsDir,
344
- appContract: validatedAppContract,
363
+ appContract: toContract,
345
364
  extensionPacks: config.extensionPacks ?? [],
346
- validateContract: (json: unknown) => familyInstance.validateContract(json),
365
+ deserializeContract: (json: unknown) => familyInstance.deserializeContract(json),
347
366
  });
348
367
  if (!aggregateResult.ok) {
349
368
  return notOk(aggregateResult.failure);
@@ -370,26 +370,30 @@ async function executeMigrationShowCommand(
370
370
  );
371
371
  }
372
372
 
373
+ // Construct the family instance up-front so the on-disk app contract
374
+ // read crosses the serializer seam (`familyInstance.deserializeContract`)
375
+ // at the read site. See TML-2536.
376
+ const stack = createControlStack(config);
377
+ const familyInstance = config.family.create(stack);
378
+
373
379
  let appContract: Contract;
374
380
  try {
375
- appContract = JSON.parse(contractJsonContent) as Contract;
381
+ appContract = familyInstance.deserializeContract(JSON.parse(contractJsonContent) as unknown);
376
382
  } catch (error) {
377
383
  return notOk(
378
384
  errorContractValidationFailed(
379
- `Contract JSON is invalid: ${error instanceof Error ? error.message : String(error)}`,
385
+ `Contract at ${contractPathAbsolute} failed to deserialize: ${error instanceof Error ? error.message : String(error)}`,
380
386
  { where: { path: contractPathAbsolute } },
381
387
  ),
382
388
  );
383
389
  }
384
390
 
385
- const stack = createControlStack(config);
386
- const familyInstance = config.family.create(stack);
387
391
  const aggregateResult = await buildContractSpaceAggregate({
388
392
  targetId: config.target.targetId,
389
393
  migrationsDir,
390
394
  appContract,
391
395
  extensionPacks: config.extensionPacks ?? [],
392
- validateContract: (json: unknown) => familyInstance.validateContract(json),
396
+ deserializeContract: (json: unknown) => familyInstance.deserializeContract(json),
393
397
  });
394
398
  if (!aggregateResult.ok) {
395
399
  return notOk(aggregateResult.failure);
@@ -437,15 +437,15 @@ export async function loadAggregateStatusSpaces(args: {
437
437
  readonly migrationsDir: string;
438
438
  readonly appContractRaw: unknown;
439
439
  readonly extensionPacks: BuildAggregateInputs<string, string>['extensionPacks'];
440
- readonly validateContract: BuildAggregateInputs<string, string>['validateContract'];
440
+ readonly deserializeContract: BuildAggregateInputs<string, string>['deserializeContract'];
441
441
  readonly markersBySpace: ReadonlyMap<string, ContractMarkerRecordLike> | null;
442
442
  }): Promise<readonly MigrationStatusSpaceEntry[]> {
443
443
  const loadInputs: BuildAggregateInputs<string, string> = {
444
444
  targetId: args.targetId,
445
445
  migrationsDir: args.migrationsDir,
446
- appContract: args.validateContract(args.appContractRaw),
446
+ appContract: args.deserializeContract(args.appContractRaw),
447
447
  extensionPacks: args.extensionPacks,
448
- validateContract: args.validateContract,
448
+ deserializeContract: args.deserializeContract,
449
449
  };
450
450
 
451
451
  const loaded = await buildContractSpaceAggregate(loadInputs);
@@ -774,7 +774,7 @@ async function executeMigrationStatusCommand(
774
774
  let aggregateSpaces: readonly MigrationStatusSpaceEntry[] = [];
775
775
  if (contractRawForAggregate !== null) {
776
776
  // The aggregate loader needs a typed-Contract producer. Build a
777
- // real control stack so `validateContract` runs against a fully
777
+ // real control stack so `deserializeContract` runs against a fully
778
778
  // composed family instance — descriptors that read stack members
779
779
  // during construction (e.g. codec lookups) get a consistent view.
780
780
  const stack = createControlStack(config);
@@ -785,7 +785,7 @@ async function executeMigrationStatusCommand(
785
785
  migrationsDir,
786
786
  appContractRaw: contractRawForAggregate,
787
787
  extensionPacks: config.extensionPacks ?? [],
788
- validateContract: (json: unknown) => familyInstance.validateContract(json),
788
+ deserializeContract: (json: unknown) => familyInstance.deserializeContract(json),
789
789
  markersBySpace: allMarkers,
790
790
  });
791
791
  } catch {
@@ -201,7 +201,7 @@ class ControlClientImpl implements ControlClient {
201
201
  // Validate contract using family instance
202
202
  let contract: Contract;
203
203
  try {
204
- contract = familyInstance.validateContract(options.contract);
204
+ contract = familyInstance.deserializeContract(options.contract);
205
205
  } catch (error) {
206
206
  const message = error instanceof Error ? error.message : String(error);
207
207
  throw new ContractValidationError(message, error);
@@ -254,7 +254,7 @@ class ControlClientImpl implements ControlClient {
254
254
  // Validate contract using family instance
255
255
  let contract: Contract;
256
256
  try {
257
- contract = familyInstance.validateContract(options.contract);
257
+ contract = familyInstance.deserializeContract(options.contract);
258
258
  } catch (error) {
259
259
  const message = error instanceof Error ? error.message : String(error);
260
260
  throw new ContractValidationError(message, error);
@@ -308,7 +308,7 @@ class ControlClientImpl implements ControlClient {
308
308
  // Validate contract using family instance
309
309
  let contract: Contract;
310
310
  try {
311
- contract = familyInstance.validateContract(options.contract);
311
+ contract = familyInstance.deserializeContract(options.contract);
312
312
  } catch (error) {
313
313
  const message = error instanceof Error ? error.message : String(error);
314
314
  throw new ContractValidationError(message, error);
@@ -361,7 +361,7 @@ class ControlClientImpl implements ControlClient {
361
361
 
362
362
  let contract: Contract;
363
363
  try {
364
- contract = familyInstance.validateContract(options.contract);
364
+ contract = familyInstance.deserializeContract(options.contract);
365
365
  } catch (error) {
366
366
  const message = error instanceof Error ? error.message : String(error);
367
367
  throw new ContractValidationError(message, error);
@@ -392,7 +392,7 @@ class ControlClientImpl implements ControlClient {
392
392
 
393
393
  let contract: Contract;
394
394
  try {
395
- contract = familyInstance.validateContract(options.contract);
395
+ contract = familyInstance.deserializeContract(options.contract);
396
396
  } catch (error) {
397
397
  const message = error instanceof Error ? error.message : String(error);
398
398
  throw new ContractValidationError(message, error);
@@ -418,18 +418,10 @@ class ControlClientImpl implements ControlClient {
418
418
  await this.connectWithProgress(options.connection, 'dbVerify', onProgress);
419
419
  const { driver, familyInstance, frameworkComponents } = await this.ensureConnected();
420
420
 
421
- let contract: Contract;
422
- try {
423
- contract = familyInstance.validateContract(options.contract);
424
- } catch (error) {
425
- const message = error instanceof Error ? error.message : String(error);
426
- throw new ContractValidationError(message, error);
427
- }
428
-
429
421
  return executeDbVerify({
430
422
  driver,
431
423
  familyInstance,
432
- contract,
424
+ contract: options.contract,
433
425
  migrationsDir: options.migrationsDir,
434
426
  targetId: this.options.target.targetId,
435
427
  extensionPacks: this.options.extensionPacks ?? [],
@@ -466,7 +458,7 @@ class ControlClientImpl implements ControlClient {
466
458
 
467
459
  let contract: Contract;
468
460
  try {
469
- contract = familyInstance.validateContract(options.contract);
461
+ contract = familyInstance.deserializeContract(options.contract);
470
462
  } catch (error) {
471
463
  const message = error instanceof Error ? error.message : String(error);
472
464
  throw new ContractValidationError(message, error);
@@ -641,10 +633,20 @@ class ControlClientImpl implements ControlClient {
641
633
  });
642
634
 
643
635
  try {
644
- const enrichedIR = enrichContract(contractRaw as Contract, this.frameworkComponents ?? []);
636
+ // Blind cast: `contractRaw` is the unverified provider
637
+ // payload — `enrichContract` only adds capability + extension
638
+ // metadata onto whatever shape it receives. The structural
639
+ // check happens immediately afterwards via
640
+ // `familyInstance.deserializeContract(enrichedIR)`, which is
641
+ // the seam-of-record and the only thing that may surface
642
+ // structural errors to the caller.
643
+ const enrichedIR = enrichContract(
644
+ contractRaw as unknown as Contract,
645
+ this.frameworkComponents ?? [],
646
+ );
645
647
 
646
648
  try {
647
- this.familyInstance.validateContract(enrichedIR);
649
+ this.familyInstance.deserializeContract(enrichedIR);
648
650
  } catch (error) {
649
651
  onProgress?.({
650
652
  action: 'emit',
@@ -232,8 +232,19 @@ export async function executeContractEmit(
232
232
  config.target.targetId,
233
233
  rawComponents,
234
234
  );
235
- const enrichedIR = enrichContract(validatedContract.value as Contract, frameworkComponents);
236
- familyInstance.validateContract(enrichedIR);
235
+ // Blind cast: `validateProviderResult` upstream has already
236
+ // pinned `validatedContract.value` to the provider's loose
237
+ // `Contract` envelope, but the local `Contract` type at this
238
+ // call site is the precise structural interface. Re-narrowing
239
+ // the envelope into the precise type is exactly what the
240
+ // `familyInstance.deserializeContract(enrichedIR)` call on the
241
+ // next line does — the cast just defers the structural check
242
+ // by one statement so `enrichContract` can decorate first.
243
+ const enrichedIR = enrichContract(
244
+ validatedContract.value as unknown as Contract,
245
+ frameworkComponents,
246
+ );
247
+ familyInstance.deserializeContract(enrichedIR);
237
248
  // Each target's descriptor ships a `contractSerializer` SPI; the
238
249
  // framework canonicalizer threads its `serializeContract` so the
239
250
  // on-disk JSON envelope is constructed by target-owned code
@@ -116,7 +116,7 @@ export async function executeAggregateApply<TFamilyId extends string, TTargetId
116
116
  migrationsDir,
117
117
  appContract: contract,
118
118
  extensionPacks,
119
- validateContract: (json) => familyInstance.validateContract(json),
119
+ deserializeContract: (json) => familyInstance.deserializeContract(json),
120
120
  };
121
121
  const loaded = await buildContractSpaceAggregate(loadInputs);
122
122
  if (!loaded.ok) {
@@ -120,7 +120,7 @@ function buildLoadInputs<TFamilyId extends string, TTargetId extends string>(
120
120
  migrationsDir: options.migrationsDir,
121
121
  appContract: options.contract,
122
122
  extensionPacks: options.extensionPacks,
123
- validateContract: (json) => options.familyInstance.validateContract(json),
123
+ deserializeContract: (json) => options.familyInstance.deserializeContract(json),
124
124
  };
125
125
  }
126
126
 
@@ -137,7 +137,7 @@ export async function executeMigrationApply<TFamilyId extends string, TTargetId
137
137
  migrationsDir,
138
138
  appContract: contract,
139
139
  extensionPacks,
140
- validateContract: (json) => familyInstance.validateContract(json),
140
+ deserializeContract: (json) => familyInstance.deserializeContract(json),
141
141
  appMigrationPackages,
142
142
  };
143
143
  const loaded = await buildContractSpaceAggregate(loadInputs);
@@ -2,7 +2,7 @@ import type {
2
2
  ContractSourceDiagnostics,
3
3
  ContractSourceProvider,
4
4
  } from '@prisma-next/config/config-types';
5
- import type { ContractMarkerRecord } from '@prisma-next/contract/types';
5
+ import type { Contract, ContractMarkerRecord } from '@prisma-next/contract/types';
6
6
  import type {
7
7
  ControlAdapterDescriptor,
8
8
  ControlDriverDescriptor,
@@ -118,7 +118,7 @@ export type OnControlProgress = (event: ControlProgressEvent) => void;
118
118
  * Options for the verify operation.
119
119
  */
120
120
  export interface VerifyOptions {
121
- /** Contract or unvalidated JSON - validated at runtime via familyInstance.validateContract() */
121
+ /** Contract or unvalidated JSON - validated at runtime via familyInstance.deserializeContract() */
122
122
  readonly contract: unknown;
123
123
  /**
124
124
  * Database connection. If provided, verify will connect before executing.
@@ -134,7 +134,7 @@ export interface VerifyOptions {
134
134
  * Options for the schemaVerify operation.
135
135
  */
136
136
  export interface SchemaVerifyOptions {
137
- /** Contract or unvalidated JSON - validated at runtime via familyInstance.validateContract() */
137
+ /** Contract or unvalidated JSON - validated at runtime via familyInstance.deserializeContract() */
138
138
  readonly contract: unknown;
139
139
  /**
140
140
  * Whether to use strict mode for schema verification.
@@ -156,7 +156,7 @@ export interface SchemaVerifyOptions {
156
156
  * Options for the sign operation.
157
157
  */
158
158
  export interface SignOptions {
159
- /** Contract or unvalidated JSON - validated at runtime via familyInstance.validateContract() */
159
+ /** Contract or unvalidated JSON - validated at runtime via familyInstance.deserializeContract() */
160
160
  readonly contract: unknown;
161
161
  /**
162
162
  * Path to the contract file (for metadata in the result).
@@ -180,7 +180,7 @@ export interface SignOptions {
180
180
  * Options for the dbInit operation.
181
181
  */
182
182
  export interface DbInitOptions {
183
- /** Contract or unvalidated JSON - validated at runtime via familyInstance.validateContract() */
183
+ /** Contract or unvalidated JSON - validated at runtime via familyInstance.deserializeContract() */
184
184
  readonly contract: unknown;
185
185
  /**
186
186
  * Mode for the dbInit operation.
@@ -209,7 +209,7 @@ export interface DbInitOptions {
209
209
  * Options for the dbUpdate operation.
210
210
  */
211
211
  export interface DbUpdateOptions {
212
- /** Contract or unvalidated JSON - validated at runtime via familyInstance.validateContract() */
212
+ /** Contract or unvalidated JSON - validated at runtime via familyInstance.deserializeContract() */
213
213
  readonly contract: unknown;
214
214
  /**
215
215
  * Mode for the dbUpdate operation.
@@ -251,7 +251,13 @@ export interface DbUpdateOptions {
251
251
  * portion of the verifier.
252
252
  */
253
253
  export interface DbVerifyOptions {
254
- readonly contract: unknown;
254
+ /**
255
+ * Already-deserialized contract. Callers cross the family
256
+ * `deserializeContract` seam at the read site (TML-2536) and pass the
257
+ * hydrated value through unchanged; this op no longer re-runs the
258
+ * SerializerBase pipeline.
259
+ */
260
+ readonly contract: Contract;
255
261
  readonly migrationsDir: string;
256
262
  readonly strict: boolean;
257
263
  readonly skipSchema: boolean;
@@ -212,7 +212,15 @@ export async function loadContractFromTs(
212
212
 
213
213
  validatePurity(contract);
214
214
 
215
- return contract as Contract;
215
+ // Blind cast: the loaded module was authored by user code
216
+ // (typically via `defineContract` / a contract builder) and
217
+ // its runtime shape is structurally a `Contract`, but the
218
+ // dynamic import collapses the source typing. The contract
219
+ // structural validation that asserts the shape happens
220
+ // downstream at the `familyInstance.deserializeContract` seam
221
+ // (e.g. in `executeContractEmit`); this helper only checks
222
+ // purity here.
223
+ return contract as unknown as Contract;
216
224
  } catch (error) {
217
225
  try {
218
226
  if (tempFile) {
@@ -120,7 +120,7 @@ export interface BuildAggregateInputs<TFamilyId extends string, TTargetId extend
120
120
  readonly migrationsDir: string;
121
121
  readonly appContract: Contract;
122
122
  readonly extensionPacks: ReadonlyArray<ControlExtensionDescriptor<TFamilyId, TTargetId>>;
123
- readonly validateContract: (contractJson: unknown) => Contract;
123
+ readonly deserializeContract: (contractJson: unknown) => Contract;
124
124
  /**
125
125
  * App-space migration packages to hydrate the app member's
126
126
  * migration graph with. Defaults to `[]` (matches the `db init` /
@@ -165,7 +165,7 @@ export async function buildContractSpaceAggregate<
165
165
  migrationsDir: inputs.migrationsDir,
166
166
  appContract: inputs.appContract,
167
167
  declaredExtensions,
168
- validateContract: inputs.validateContract,
168
+ deserializeContract: inputs.deserializeContract,
169
169
  appMigrationPackages: inputs.appMigrationPackages ?? [],
170
170
  };
171
171