@prisma-next/cli 0.14.0 → 0.15.0-dev.10

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 (157) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.mjs +33 -14
  3. package/dist/cli.mjs.map +1 -1
  4. package/dist/{client-CJzuo5wX.mjs → client-DwOyZz0a.mjs} +141 -123
  5. package/dist/client-DwOyZz0a.mjs.map +1 -0
  6. package/dist/{command-helpers-DGMvGBeX.mjs → command-helpers-CNwURRf9.mjs} +30 -11
  7. package/dist/command-helpers-CNwURRf9.mjs.map +1 -0
  8. package/dist/commands/contract-emit.mjs +1 -1
  9. package/dist/commands/contract-infer.mjs +1 -1
  10. package/dist/commands/db-init.mjs +3 -3
  11. package/dist/commands/db-schema.mjs +3 -3
  12. package/dist/commands/db-sign.mjs +5 -5
  13. package/dist/commands/db-sign.mjs.map +1 -1
  14. package/dist/commands/db-update.mjs +4 -4
  15. package/dist/commands/db-verify.d.mts.map +1 -1
  16. package/dist/commands/db-verify.mjs +1 -1
  17. package/dist/commands/migrate.d.mts +1 -1
  18. package/dist/commands/migrate.d.mts.map +1 -1
  19. package/dist/commands/migrate.mjs +37 -38
  20. package/dist/commands/migrate.mjs.map +1 -1
  21. package/dist/commands/migration-check.d.mts +1 -1
  22. package/dist/commands/migration-check.mjs +1 -1
  23. package/dist/commands/migration-graph.d.mts +1 -1
  24. package/dist/commands/migration-graph.mjs +9 -9
  25. package/dist/commands/migration-graph.mjs.map +1 -1
  26. package/dist/commands/migration-list.d.mts +6 -6
  27. package/dist/commands/migration-list.d.mts.map +1 -1
  28. package/dist/commands/migration-list.mjs +1 -1
  29. package/dist/commands/migration-log.d.mts +1 -1
  30. package/dist/commands/migration-log.mjs +1 -1
  31. package/dist/commands/migration-new.mjs +4 -4
  32. package/dist/commands/migration-new.mjs.map +1 -1
  33. package/dist/commands/migration-plan.d.mts.map +1 -1
  34. package/dist/commands/migration-plan.mjs +1 -1
  35. package/dist/commands/migration-show.mjs +4 -4
  36. package/dist/commands/migration-show.mjs.map +1 -1
  37. package/dist/commands/migration-status.d.mts +1 -1
  38. package/dist/commands/migration-status.mjs +1 -1
  39. package/dist/commands/ref.d.mts +1 -1
  40. package/dist/commands/ref.mjs +3 -3
  41. package/dist/commands/ref.mjs.map +1 -1
  42. package/dist/commands/telemetry/index.mjs +1 -1
  43. package/dist/{contract-at-errors-CFXsstzm.mjs → contract-at-errors-BHbY04pl.mjs} +2 -2
  44. package/dist/{contract-at-errors-CFXsstzm.mjs.map → contract-at-errors-BHbY04pl.mjs.map} +1 -1
  45. package/dist/{contract-emit-C8HmtboH.mjs → contract-emit-BeidkyUC.mjs} +9 -11
  46. package/dist/contract-emit-BeidkyUC.mjs.map +1 -0
  47. package/dist/{contract-emit-B_qriF8B.mjs → contract-emit-IQBYv351.mjs} +6 -6
  48. package/dist/contract-emit-IQBYv351.mjs.map +1 -0
  49. package/dist/{contract-infer-BYT_ra_U.mjs → contract-infer-B3Wq2cu1.mjs} +3 -3
  50. package/dist/{contract-infer-BYT_ra_U.mjs.map → contract-infer-B3Wq2cu1.mjs.map} +1 -1
  51. package/dist/{contract-space-aggregate-loader-ClI1KN6d.mjs → contract-space-aggregate-loader-BirzMmW1.mjs} +63 -5
  52. package/dist/contract-space-aggregate-loader-BirzMmW1.mjs.map +1 -0
  53. package/dist/{db-verify-C24FKhb7.mjs → db-verify-DpRrecs7.mjs} +57 -61
  54. package/dist/db-verify-DpRrecs7.mjs.map +1 -0
  55. package/dist/exports/control-api.d.mts +3 -3
  56. package/dist/exports/control-api.d.mts.map +1 -1
  57. package/dist/exports/control-api.mjs +2 -2
  58. package/dist/exports/index.d.mts +5 -1
  59. package/dist/exports/index.d.mts.map +1 -1
  60. package/dist/exports/index.mjs +4 -3
  61. package/dist/exports/index.mjs.map +1 -1
  62. package/dist/format-CaCF63ld.mjs +102 -0
  63. package/dist/format-CaCF63ld.mjs.map +1 -0
  64. package/dist/{framework-components-YVQHhPH7.mjs → framework-components-CnBcJDmF.mjs} +2 -2
  65. package/dist/{framework-components-YVQHhPH7.mjs.map → framework-components-CnBcJDmF.mjs.map} +1 -1
  66. package/dist/{init-0HwB-Vh8.mjs → init-Boa0LzeJ.mjs} +27 -19
  67. package/dist/init-Boa0LzeJ.mjs.map +1 -0
  68. package/dist/{inspect-live-schema-DF6IwcDl.mjs → inspect-live-schema-CldSQjVI.mjs} +4 -4
  69. package/dist/inspect-live-schema-CldSQjVI.mjs.map +1 -0
  70. package/dist/{migration-check-VwM8xCZV.mjs → migration-check-CGtYnR9k.mjs} +10 -11
  71. package/dist/migration-check-CGtYnR9k.mjs.map +1 -0
  72. package/dist/migration-cli.mjs +1 -1
  73. package/dist/migration-cli.mjs.map +1 -1
  74. package/dist/{migration-command-scaffold-DA-Lhx6o.mjs → migration-command-scaffold-B3m-MMAx.mjs} +4 -4
  75. package/dist/migration-command-scaffold-B3m-MMAx.mjs.map +1 -0
  76. package/dist/{migration-graph-command-render-CEez7YUK.mjs → migration-graph-command-render-B83xrnNy.mjs} +24 -24
  77. package/dist/{migration-graph-command-render-CEez7YUK.mjs.map → migration-graph-command-render-B83xrnNy.mjs.map} +1 -1
  78. package/dist/{migration-list-DlJJ_38Z.mjs → migration-list-DBU6Pneg.mjs} +16 -16
  79. package/dist/migration-list-DBU6Pneg.mjs.map +1 -0
  80. package/dist/{migration-log-CG0qQAFm.mjs → migration-log-BYngyvXa.mjs} +5 -5
  81. package/dist/migration-log-BYngyvXa.mjs.map +1 -0
  82. package/dist/{migration-path-target-Ce6OZImp.mjs → migration-path-target-DQa4Caoc.mjs} +2 -2
  83. package/dist/{migration-path-target-Ce6OZImp.mjs.map → migration-path-target-DQa4Caoc.mjs.map} +1 -1
  84. package/dist/{migration-plan-W_E8FQOk.mjs → migration-plan-D6bHpCzE.mjs} +30 -30
  85. package/dist/migration-plan-D6bHpCzE.mjs.map +1 -0
  86. package/dist/{migration-status-CD-LC2Ip.mjs → migration-status-CpQ5UhcU.mjs} +14 -14
  87. package/dist/migration-status-CpQ5UhcU.mjs.map +1 -0
  88. package/dist/{telemetry-BIM4beEO.mjs → telemetry-BQJ_XHa9.mjs} +2 -2
  89. package/dist/{telemetry-BIM4beEO.mjs.map → telemetry-BQJ_XHa9.mjs.map} +1 -1
  90. package/dist/{types-C_tYiJYx.d.mts → types-DFoqNIcQ.d.mts} +27 -21
  91. package/dist/types-DFoqNIcQ.d.mts.map +1 -0
  92. package/dist/{verify-DcOYZ1tH.mjs → verify-CInGyt4U.mjs} +58 -159
  93. package/dist/verify-CInGyt4U.mjs.map +1 -0
  94. package/package.json +28 -30
  95. package/src/cli.ts +7 -0
  96. package/src/commands/contract-emit.ts +1 -1
  97. package/src/commands/db-sign.ts +1 -1
  98. package/src/commands/db-verify.ts +34 -20
  99. package/src/commands/format.ts +75 -0
  100. package/src/commands/init/detect-package-manager.ts +8 -4
  101. package/src/commands/init/hygiene-package-scripts.ts +16 -3
  102. package/src/commands/inspect-live-schema.ts +1 -1
  103. package/src/commands/lsp.ts +32 -0
  104. package/src/commands/migrate.ts +47 -44
  105. package/src/commands/migration-check.ts +6 -6
  106. package/src/commands/migration-graph.ts +5 -5
  107. package/src/commands/migration-list.ts +13 -13
  108. package/src/commands/migration-log.ts +1 -1
  109. package/src/commands/migration-new.ts +1 -1
  110. package/src/commands/migration-plan.ts +15 -4
  111. package/src/commands/migration-show.ts +1 -1
  112. package/src/commands/migration-status.ts +10 -10
  113. package/src/commands/ref.ts +1 -1
  114. package/src/control-api/client.ts +8 -23
  115. package/src/control-api/operations/contract-emit.ts +9 -27
  116. package/src/control-api/operations/db-init.ts +2 -2
  117. package/src/control-api/operations/db-run.ts +18 -17
  118. package/src/control-api/operations/db-verify.ts +52 -44
  119. package/src/control-api/operations/format.ts +98 -0
  120. package/src/control-api/operations/migrate.ts +139 -95
  121. package/src/control-api/operations/run-migration.ts +6 -5
  122. package/src/control-api/types.ts +8 -8
  123. package/src/exports/index.ts +1 -0
  124. package/src/migration-cli.ts +1 -1
  125. package/src/utils/cli-errors.ts +14 -1
  126. package/src/utils/combine-verify-results.ts +94 -0
  127. package/src/utils/extension-pack-inputs.ts +1 -1
  128. package/src/utils/formatters/errors.ts +34 -5
  129. package/src/utils/formatters/verify.ts +89 -227
  130. package/src/utils/migration-command-scaffold.ts +1 -1
  131. package/src/utils/plan-resolution.ts +18 -18
  132. package/dist/client-CJzuo5wX.mjs.map +0 -1
  133. package/dist/command-helpers-DGMvGBeX.mjs.map +0 -1
  134. package/dist/config-loader-p9JMrekQ.mjs +0 -88
  135. package/dist/config-loader-p9JMrekQ.mjs.map +0 -1
  136. package/dist/config-loader.d.mts +0 -16
  137. package/dist/config-loader.d.mts.map +0 -1
  138. package/dist/config-loader.mjs +0 -2
  139. package/dist/contract-emit-B_qriF8B.mjs.map +0 -1
  140. package/dist/contract-emit-C8HmtboH.mjs.map +0 -1
  141. package/dist/contract-space-aggregate-loader-ClI1KN6d.mjs.map +0 -1
  142. package/dist/db-verify-C24FKhb7.mjs.map +0 -1
  143. package/dist/extension-pack-inputs-1ySHqxKG.mjs +0 -62
  144. package/dist/extension-pack-inputs-1ySHqxKG.mjs.map +0 -1
  145. package/dist/init-0HwB-Vh8.mjs.map +0 -1
  146. package/dist/inspect-live-schema-DF6IwcDl.mjs.map +0 -1
  147. package/dist/migration-check-VwM8xCZV.mjs.map +0 -1
  148. package/dist/migration-command-scaffold-DA-Lhx6o.mjs.map +0 -1
  149. package/dist/migration-list-DlJJ_38Z.mjs.map +0 -1
  150. package/dist/migration-log-CG0qQAFm.mjs.map +0 -1
  151. package/dist/migration-plan-W_E8FQOk.mjs.map +0 -1
  152. package/dist/migration-status-CD-LC2Ip.mjs.map +0 -1
  153. package/dist/types-C_tYiJYx.d.mts.map +0 -1
  154. package/dist/verify-DcOYZ1tH.mjs.map +0 -1
  155. package/src/config-loader.ts +0 -92
  156. package/src/config-path-validation.ts +0 -74
  157. package/src/utils/combine-schema-results.ts +0 -84
@@ -1,16 +1,14 @@
1
1
  import { mkdir } from 'node:fs/promises';
2
+ import { loadConfig } from '@prisma-next/config-loader';
2
3
  import type { Contract } from '@prisma-next/contract/types';
3
4
  import { emit, getEmittedArtifactPaths } from '@prisma-next/emitter';
4
5
  import { createControlStack } from '@prisma-next/framework-components/control';
5
6
  import { abortable } from '@prisma-next/utils/abortable';
6
- import { blindCast } from '@prisma-next/utils/casts';
7
7
  import { ifDefined } from '@prisma-next/utils/defined';
8
8
  import type { JsonObject } from '@prisma-next/utils/json';
9
9
  import { dirname, join } from 'pathe';
10
- import { loadConfig } from '../../config-loader';
11
10
  import { errorContractConfigMissing, errorRuntime } from '../../utils/cli-errors';
12
11
  import { queueEmitByOutput } from '../../utils/emit-queue';
13
- import { toExtensionInputs } from '../../utils/extension-pack-inputs';
14
12
  import { assertFrameworkComponentsCompatible } from '../../utils/framework-components';
15
13
  import { publishContractArtifactPair } from '../../utils/publish-contract-artifact-pair';
16
14
  import { validateContractDeps } from '../../utils/validate-contract-deps';
@@ -196,31 +194,15 @@ export async function executeContractEmit(
196
194
  return queueEmitByOutput(outputJsonPath, async () => {
197
195
  const stack = createControlStack(config);
198
196
 
199
- const extensionInputs = toExtensionInputs(
200
- blindCast<
201
- readonly unknown[],
202
- 'toExtensionInputs accepts readonly unknown[] per its documented structural cast boundary'
203
- >(stack.extensionPacks),
204
- );
205
- const composedExtensionContracts = new Map<string, Contract>(
206
- extensionInputs
207
- .filter((p) => p.contractSpace !== undefined)
208
- .map((p) => [
209
- p.id,
210
- blindCast<
211
- Contract,
212
- 'contractSpace.contractJson is the typed contract for this extension space'
213
- >(p.contractSpace!.contractJson),
214
- ]),
215
- );
216
197
  const sourceContext = {
217
198
  composedExtensionPacks: stack.extensionPacks.map((p) => p.id),
218
- composedExtensionContracts,
199
+ composedExtensionContracts: stack.extensionContracts,
219
200
  scalarTypeDescriptors: stack.scalarTypeDescriptors,
220
201
  authoringContributions: stack.authoringContributions,
221
202
  codecLookup: stack.codecLookup,
222
203
  controlMutationDefaults: stack.controlMutationDefaults,
223
204
  resolvedInputs: contractConfig.source.inputs ?? [],
205
+ capabilities: stack.capabilities,
224
206
  };
225
207
 
226
208
  startSpan(onProgress, 'resolveSource', 'Resolving contract source...');
@@ -258,16 +240,16 @@ export async function executeContractEmit(
258
240
  // Blind cast: `validateProviderResult` upstream has already
259
241
  // pinned `validatedContract.value` to the provider's loose
260
242
  // `Contract` envelope, but the local `Contract` type at this
261
- // call site is the precise structural interface. Re-narrowing
262
- // the envelope into the precise type is exactly what the
263
- // `familyInstance.deserializeContract(enrichedIR)` call on the
264
- // next line does — the cast just defers the structural check
265
- // by one statement so `enrichContract` can decorate first.
243
+ // call site is the precise structural interface. The cast just
244
+ // defers the structural check by one statement so `enrichContract`
245
+ // can decorate first; the subsequent serialize→deserialize round-trip
246
+ // re-narrows the envelope into the precise type.
266
247
  const enrichedIR = enrichContract(
267
248
  validatedContract.value as unknown as Contract,
268
249
  frameworkComponents,
269
250
  );
270
- const deserializedContract = familyInstance.deserializeContract(enrichedIR);
251
+ const rawContractJson = config.target.contractSerializer.serializeContract(enrichedIR);
252
+ const deserializedContract = familyInstance.deserializeContract(rawContractJson);
271
253
  // Each target's descriptor ships a `contractSerializer` SPI; the
272
254
  // framework canonicalizer threads its `serializeContract` so the
273
255
  // on-disk JSON envelope is constructed by target-owned code
@@ -20,7 +20,7 @@ import { executeRun } from './db-run';
20
20
  * {@link import('@prisma-next/migration-tools/aggregate').loadContractSpaceAggregate}
21
21
  * from the supplied descriptor set + on-disk on-disk artefacts.
22
22
  * 2. The aggregate planner runs with `callerPolicy.ignoreGraphFor`
23
- * locked to the app member — synth strategy for the app, graph-walk
23
+ * locked to the app space — synth strategy for the app, graph-walk
24
24
  * for every extension.
25
25
  * 3. The runner's `execute` applies the per-space plans
26
26
  * inside one outer transaction.
@@ -53,7 +53,7 @@ export interface ExecuteDbInitOptions<TFamilyId extends string, TTargetId extend
53
53
  /**
54
54
  * Declared extension descriptors. Defaults to an empty list, which
55
55
  * routes through the same loader → planner → runner pipeline with no
56
- * extension members in the aggregate.
56
+ * extension spaces in the aggregate.
57
57
  */
58
58
  readonly extensionPacks?: ReadonlyArray<ControlExtensionDescriptor<TFamilyId, TTargetId>>;
59
59
  /** Optional progress callback for observing operation progress */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Backs `db init` / `db update`. Strategy: introspect → planMigration; synth-for-app + graph-walk-extensions; plan-mode + orphan-marker preflight.
2
+ * Backs `db init` / `db update`. Strategy: introspect → planMigration; planFromDiff-for-app + resolveRecordedPath-extensions; plan-mode + orphan-marker preflight.
3
3
  */
4
4
 
5
5
  import type { Contract } from '@prisma-next/contract/types';
@@ -97,10 +97,11 @@ export interface ExecuteRunOptions<TFamilyId extends string, TTargetId extends s
97
97
  * integrity violation short-circuits with a structured error.
98
98
  * 2. **Read DB state**: marker rows (`familyInstance.readAllMarkers`)
99
99
  * + introspected schema (`familyInstance.introspect`).
100
- * 3. **Plan**: {@link planMigration} chooses graph-walk vs synth per
101
- * member according to `callerPolicy.ignoreGraphFor`. The app member
102
- * is forced through synth (today's daily-driver behaviour); every
103
- * extension member walks its on-disk graph.
100
+ * 3. **Plan**: {@link planMigration} chooses `resolveRecordedPath` vs
101
+ * `planFromDiff` per space according to `callerPolicy.ignoreGraphFor`.
102
+ * The app space is forced through `planFromDiff` (today's daily-driver
103
+ * behaviour); every extension space walks its on-disk graph via
104
+ * `resolveRecordedPath`.
104
105
  * 4. **Apply** (when `mode === 'apply'`): every per-space `MigrationPlan`
105
106
  * feeds into the runner's `execute` — one outer
106
107
  * transaction across every space; failure on any space rolls back
@@ -168,9 +169,9 @@ export async function executeRun<TFamilyId extends string, TTargetId extends str
168
169
  });
169
170
  onProgress?.({ action, kind: 'spanEnd', spanId: SPAN_IDS.introspect, outcome: 'ok' });
170
171
 
171
- // 3. Plan via aggregate planner. App is forced through synth (today's
172
- // `db init` / `db update` daily-driver behaviour); extensions walk
173
- // their on-disk migration graphs.
172
+ // 3. Plan via aggregate planner. App is forced through planFromDiff
173
+ // (today's `db init` / `db update` daily-driver behaviour); extensions
174
+ // walk their on-disk migration graphs via resolveRecordedPath.
174
175
  onProgress?.({
175
176
  action,
176
177
  kind: 'spanStart',
@@ -200,7 +201,7 @@ export async function executeRun<TFamilyId extends string, TTargetId extends str
200
201
  const appResolution = orderedResolutions.find((r) => r.spaceId === aggregate.app.spaceId);
201
202
  if (!appResolution) {
202
203
  throw new Error(
203
- 'Aggregate planner returned no plan for the app member — the planner is supposed to always emit one.',
204
+ 'Aggregate planner returned no plan for the app space — the planner is supposed to always emit one.',
204
205
  );
205
206
  }
206
207
  const appPlan = appResolution.entry.plan;
@@ -228,9 +229,9 @@ export async function executeRun<TFamilyId extends string, TTargetId extends str
228
229
  // 5. Run mode: hand off to the shared `runMigration` primitive.
229
230
  // The runner-driving tail is identical for `db init` / `db update` /
230
231
  // `migrate` — only how each caller produces `perSpacePlans`
231
- // differs (synth + graph-walk via planMigration here; graph-walk
232
- // only for migrate). Each caller produces perSpacePlans differently;
233
- // this helper handles the shared run tail.
232
+ // differs (planFromDiff + resolveRecordedPath via planMigration here;
233
+ // resolveRecordedPath only for migrate). Each caller produces
234
+ // perSpacePlans differently; this helper handles the shared run tail.
234
235
  const applied = await runMigration({
235
236
  aggregate,
236
237
  perSpacePlans: planResult.value.perSpace,
@@ -280,14 +281,14 @@ function aggregatePlannerWarnings(
280
281
 
281
282
  /**
282
283
  * Compare the live `_prisma_marker` rows against the aggregate's
283
- * declared members. Any marker row whose `space` is not a member of
284
+ * declared contract spaces. Any marker row whose `space` is not a space of
284
285
  * the aggregate is an "orphan" — typically a marker left behind by
285
286
  * an extension that was removed from `extensionPacks` without first
286
287
  * cleaning up its on-disk migrations / database tables.
287
288
  *
288
289
  * Returns a {@link CliStructuredError} envelope (code `5002`,
289
290
  * `kind: 'orphanMarker'`) for the first orphan it finds, or `null`
290
- * when every marker row maps to a declared member. Mirrors the M2
291
+ * when every marker row maps to a declared contract space. Mirrors the M2
291
292
  * `runContractSpaceVerifierMarkerCheck` envelope so downstream
292
293
  * tooling (integration tests, JSON consumers) keeps asserting on the
293
294
  * same shape.
@@ -296,13 +297,13 @@ function detectOrphanMarkers(
296
297
  aggregate: ContractSpaceAggregate,
297
298
  markerRows: ReadonlyMap<string, unknown>,
298
299
  ): CliStructuredError | null {
299
- const memberSpaceIds = new Set<string>([
300
+ const aggregateSpaceIds = new Set<string>([
300
301
  aggregate.app.spaceId,
301
302
  ...aggregate.extensions.map((m) => m.spaceId),
302
303
  ]);
303
304
  const orphans: string[] = [];
304
305
  for (const [spaceId, row] of markerRows) {
305
- if (row !== null && row !== undefined && !memberSpaceIds.has(spaceId)) {
306
+ if (row !== null && row !== undefined && !aggregateSpaceIds.has(spaceId)) {
306
307
  orphans.push(spaceId);
307
308
  }
308
309
  }
@@ -328,7 +329,7 @@ function detectOrphanMarkers(
328
329
  }
329
330
 
330
331
  function mapPlannerError(error: PlannerError): DbInitResult | DbUpdateResult {
331
- if (error.kind === 'appSynthFailure') {
332
+ if (error.kind === 'planFromDiffFailed') {
332
333
  const failure: DbInitFailure | DbUpdateFailure = {
333
334
  code: 'PLANNING_FAILED',
334
335
  summary: 'Migration planning failed due to conflicts',
@@ -4,16 +4,19 @@ import type {
4
4
  ControlDriverInstance,
5
5
  ControlExtensionDescriptor,
6
6
  ControlFamilyInstance,
7
+ SchemaDiffIssue,
7
8
  VerifyDatabaseSchemaResult,
8
9
  } from '@prisma-next/framework-components/control';
10
+ import { hasSchemaSubjectClassifier } from '@prisma-next/framework-components/control';
9
11
  import {
10
- type ContractSpaceMember,
12
+ type AggregateContractSpace,
11
13
  collectAggregateNamespaces,
12
14
  requireHeadRef,
13
15
  type VerifierOutput,
14
16
  verifyMigration,
15
17
  } from '@prisma-next/migration-tools/aggregate';
16
- import { blindCast, castAs } from '@prisma-next/utils/casts';
18
+ import { castAs } from '@prisma-next/utils/casts';
19
+ import { ifDefined } from '@prisma-next/utils/defined';
17
20
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
18
21
  import { CliStructuredError } from '../../utils/cli-errors';
19
22
  import {
@@ -37,9 +40,9 @@ const SPAN_IDS = {
37
40
  *
38
41
  * Loader → verifier pipeline. The loader (sole descriptor-import
39
42
  * boundary) builds a {@link import('@prisma-next/migration-tools/aggregate').ContractSpaceAggregate};
40
- * the aggregate verifier bundles `markerCheck` + per-space pre-projected
41
- * `schemaCheck`. `mode: 'strict' | 'lenient'` maps directly to the user
42
- * facing `--strict` flag.
43
+ * the aggregate verifier bundles `markerCheck` + per-space `schemaCheck`
44
+ * (each contract space verified against the full schema; extras stripped to its own view).
45
+ * `mode: 'strict' | 'lenient'` maps directly to the user facing `--strict` flag.
43
46
  */
44
47
  export interface ExecuteDbVerifyOptions<TFamilyId extends string, TTargetId extends string> {
45
48
  readonly driver: ControlDriverInstance<TFamilyId, TTargetId>;
@@ -63,13 +66,19 @@ export interface ExecuteDbVerifyOptions<TFamilyId extends string, TTargetId exte
63
66
  * emitted, so downstream tooling and integration tests assert on the
64
67
  * same shape).
65
68
  *
66
- * On success, the per-space schema results are returned for the CLI to
69
+ * On success, the per-space verify results are returned for the CLI to
67
70
  * render. When `skipSchema` is true (`--marker-only`), the schema map
68
71
  * is empty.
69
72
  */
70
73
  export interface ExecuteDbVerifySuccess {
71
74
  readonly schemaResults: ReadonlyMap<string, VerifyDatabaseSchemaResult>;
72
- readonly memberOrder: readonly string[];
75
+ /**
76
+ * Live element names no contract space declares, deduplicated and reported
77
+ * once for the whole database (never per space). Strict mode fails on a
78
+ * non-empty list; lenient mode surfaces it informationally.
79
+ */
80
+ readonly unclaimed: readonly string[];
81
+ readonly spaceOrder: readonly string[];
73
82
  readonly appSpaceId: string;
74
83
  }
75
84
 
@@ -85,11 +94,11 @@ export type ExecuteDbVerifyResult = Result<ExecuteDbVerifySuccess, CliStructured
85
94
  * structured CLI error.
86
95
  * 2. **Read DB state**: marker rows + (when `skipSchema` is `false`)
87
96
  * schema introspection.
88
- * 3. **Verify**: {@link verifyMigration} returns per-space
89
- * `markerCheck` + per-space pre-projected `schemaCheck` (closes F23).
90
- * Marker mismatches map to `CliStructuredError` (code `5002`) so
91
- * callers (CLI command) can render and exit. Schema results are
92
- * returned to the caller verbatim.
97
+ * 3. **Verify**: {@link verifyMigration} returns per-space `markerCheck` +
98
+ * per-space `schemaCheck` (each contract space verified against the full schema,
99
+ * then scoped to its own contract space). Marker mismatches map to
100
+ * `CliStructuredError` (code `5002`) so callers (CLI command) can render
101
+ * and exit. Verify results are returned to the caller verbatim.
93
102
  */
94
103
  export async function executeDbVerify<TFamilyId extends string, TTargetId extends string>(
95
104
  options: ExecuteDbVerifyOptions<TFamilyId, TTargetId>,
@@ -109,13 +118,26 @@ export async function executeDbVerify<TFamilyId extends string, TTargetId extend
109
118
  contract: collectAggregateNamespaces(aggregate),
110
119
  });
111
120
 
121
+ // The subject-granularity + entity-kind classifiers are an injected
122
+ // capability — not every family provides one — detected the same way the
123
+ // other optional per-family capabilities are (`hasSchemaView`, …), never
124
+ // assumed.
125
+ const classifySubjectGranularity = hasSchemaSubjectClassifier(familyInstance)
126
+ ? (issue: SchemaDiffIssue) => familyInstance.classifySubjectGranularity(issue)
127
+ : undefined;
128
+ const classifyEntityKind = hasSchemaSubjectClassifier(familyInstance)
129
+ ? (issue: SchemaDiffIssue) => familyInstance.classifyEntityKind(issue)
130
+ : undefined;
131
+
112
132
  emitVerifySpan(onProgress, 'spanStart');
113
133
  const verifyResult = verifyMigration({
114
134
  aggregate,
115
135
  markersBySpaceId,
116
136
  schemaIntrospection,
117
137
  mode: options.mode,
118
- verifySchemaForMember: createPerMemberVerifier(options),
138
+ verifySchemaForSpace: createPerSpaceVerifier(options),
139
+ ...ifDefined('classifySubjectGranularity', classifySubjectGranularity),
140
+ ...ifDefined('classifyEntityKind', classifyEntityKind),
119
141
  });
120
142
  return finaliseVerifyResult({ verifyResult, aggregate, skipMarker, onProgress });
121
143
  }
@@ -166,30 +188,27 @@ async function runIntrospection<TFamilyId extends string, TTargetId extends stri
166
188
  }
167
189
 
168
190
  /**
169
- * Build the per-member schema callback handed to the aggregate verifier.
191
+ * Build the per-space schema callback handed to the aggregate verifier.
170
192
  * When `skipSchema` is true the callback short-circuits with a synthetic
171
193
  * `ok` result so the verifier still runs the (cheap) schemaCheck loop
172
194
  * without invoking the family's verification path.
173
195
  */
174
- export function createPerMemberVerifier<TFamilyId extends string, TTargetId extends string>(
196
+ export function createPerSpaceVerifier<TFamilyId extends string, TTargetId extends string>(
175
197
  options: ExecuteDbVerifyOptions<TFamilyId, TTargetId>,
176
198
  ): (
177
- projectedSchema: unknown,
178
- member: ContractSpaceMember,
199
+ schema: unknown,
200
+ space: AggregateContractSpace,
179
201
  verifyMode: 'strict' | 'lenient',
180
202
  ) => VerifyDatabaseSchemaResult {
181
203
  const { skipSchema, familyInstance, frameworkComponents } = options;
182
- return (projectedSchema, member, verifyMode) => {
183
- if (skipSchema) return buildSkippedSchemaResult(member);
204
+ return (schema, space, verifyMode) => {
205
+ if (skipSchema) return buildSkippedSchemaResult(space);
184
206
  return familyInstance.verifySchema({
185
- contract: member.contract(),
186
- // The family's `TSchemaIR` is opaque to migration-tools; the
187
- // aggregate verifier passes through whatever we hand it. The
188
- // family expects its own IR shape on the way back.
189
- schema: blindCast<
190
- never,
191
- 'family TSchemaIR is opaque to migration-tools; projectedSchema is passed straight through'
192
- >(projectedSchema),
207
+ contract: space.contract(),
208
+ // `familyInstance` is `ControlFamilyInstance<_, unknown>`, so `verifySchema`
209
+ // takes its `TSchemaIR` as `unknown` the introspected schema passes
210
+ // straight through; the family narrows to its own IR node internally.
211
+ schema,
193
212
  strict: verifyMode === 'strict',
194
213
  frameworkComponents,
195
214
  });
@@ -223,7 +242,7 @@ function emitVerifySpan(
223
242
  * by the CLI's `--schema-only` mode.
224
243
  */
225
244
  function finaliseVerifyResult(args: {
226
- verifyResult: VerifierOutput<VerifyDatabaseSchemaResult>;
245
+ verifyResult: VerifierOutput;
227
246
  aggregate: {
228
247
  readonly app: { readonly spaceId: string };
229
248
  readonly extensions: ReadonlyArray<{ readonly spaceId: string }>;
@@ -253,14 +272,15 @@ function finaliseVerifyResult(args: {
253
272
  emitVerifySpan(onProgress, 'spanEndOk');
254
273
  return ok({
255
274
  schemaResults: verifyResult.value.schemaCheck.perSpace,
256
- memberOrder: [aggregate.app.spaceId, ...aggregate.extensions.map((e) => e.spaceId)],
275
+ unclaimed: verifyResult.value.schemaCheck.unclaimed,
276
+ spaceOrder: [aggregate.app.spaceId, ...aggregate.extensions.map((e) => e.spaceId)],
257
277
  appSpaceId: aggregate.app.spaceId,
258
278
  });
259
279
  }
260
280
 
261
- function buildSkippedSchemaResult(member: ContractSpaceMember): VerifyDatabaseSchemaResult {
262
- const contract = member.contract();
263
- const headRef = requireHeadRef(member);
281
+ function buildSkippedSchemaResult(space: AggregateContractSpace): VerifyDatabaseSchemaResult {
282
+ const contract = space.contract();
283
+ const headRef = requireHeadRef(space);
264
284
  const profileHash = castAs<{ profileHash?: string }>(contract).profileHash;
265
285
  return {
266
286
  ok: true,
@@ -272,18 +292,6 @@ function buildSkippedSchemaResult(member: ContractSpaceMember): VerifyDatabaseSc
272
292
  target: { expected: contract.target },
273
293
  schema: {
274
294
  issues: [],
275
- root: {
276
- status: 'pass',
277
- kind: 'skipped',
278
- name: member.spaceId,
279
- contractPath: '',
280
- code: 'SKIPPED',
281
- message: 'Schema verification skipped',
282
- expected: undefined,
283
- actual: undefined,
284
- children: [],
285
- },
286
- counts: { pass: 0, warn: 0, fail: 0, totalNodes: 0 },
287
295
  },
288
296
  timings: { total: 0 },
289
297
  };
@@ -0,0 +1,98 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { EOL } from 'node:os';
3
+ import { loadConfig } from '@prisma-next/config-loader';
4
+ import { type FormatOptions, format, PslFormatError } from '@prisma-next/psl-parser/format';
5
+ import { notOk, ok, type Result } from '@prisma-next/utils/result';
6
+ import { CliStructuredError, errorRuntime, errorUnexpected } from '../../utils/cli-errors';
7
+
8
+ export interface FormatOperationOptions {
9
+ readonly configPath?: string;
10
+ readonly eol?: string;
11
+ }
12
+
13
+ export interface FormatOperationResult {
14
+ readonly formatted: boolean;
15
+ readonly path?: string;
16
+ }
17
+
18
+ export function resolveNewline(
19
+ formatterNewline: 'LF' | 'CRLF' | undefined,
20
+ eol: string,
21
+ ): 'LF' | 'CRLF' {
22
+ if (formatterNewline !== undefined) {
23
+ return formatterNewline;
24
+ }
25
+ return eol === '\r\n' ? 'CRLF' : 'LF';
26
+ }
27
+
28
+ export async function executeFormat(
29
+ options: FormatOperationOptions,
30
+ ): Promise<Result<FormatOperationResult, CliStructuredError>> {
31
+ const eol = options.eol ?? EOL;
32
+
33
+ let config: Awaited<ReturnType<typeof loadConfig>>;
34
+ try {
35
+ config = await loadConfig(options.configPath);
36
+ } catch (error) {
37
+ if (CliStructuredError.is(error)) {
38
+ return notOk(error);
39
+ }
40
+ return notOk(errorUnexpected(error instanceof Error ? error.message : String(error)));
41
+ }
42
+
43
+ const source = config.contract?.source;
44
+ if (source?.sourceFormat !== 'psl') {
45
+ return ok({ formatted: false });
46
+ }
47
+
48
+ const inputPath = source.inputs?.[0];
49
+ if (inputPath === undefined) {
50
+ return ok({ formatted: false });
51
+ }
52
+
53
+ let contents: string;
54
+ try {
55
+ contents = await readFile(inputPath, 'utf-8');
56
+ } catch (error) {
57
+ return notOk(
58
+ errorRuntime('Failed to read contract source file', {
59
+ why: error instanceof Error ? error.message : String(error),
60
+ fix: `Check that ${inputPath} exists and is readable.`,
61
+ }),
62
+ );
63
+ }
64
+
65
+ const formatOptions: FormatOptions = {
66
+ indent: config.formatter?.indent ?? 2,
67
+ newline: resolveNewline(config.formatter?.newline, eol),
68
+ };
69
+
70
+ let formatted: string;
71
+ try {
72
+ formatted = format(contents, formatOptions);
73
+ } catch (error) {
74
+ if (error instanceof PslFormatError) {
75
+ return notOk(
76
+ errorRuntime('Cannot format PSL with parse errors', {
77
+ why: error.message,
78
+ fix: 'Fix the parse errors in your schema and try again.',
79
+ meta: { diagnostics: error.diagnostics },
80
+ }),
81
+ );
82
+ }
83
+ return notOk(errorUnexpected(error instanceof Error ? error.message : String(error)));
84
+ }
85
+
86
+ try {
87
+ await writeFile(inputPath, formatted, 'utf-8');
88
+ } catch (error) {
89
+ return notOk(
90
+ errorRuntime('Failed to write formatted contract source file', {
91
+ why: error instanceof Error ? error.message : String(error),
92
+ fix: `Check that ${inputPath} is writable.`,
93
+ }),
94
+ );
95
+ }
96
+
97
+ return ok({ formatted: true, path: inputPath });
98
+ }