@prisma-next/cli 0.14.0-dev.39 → 0.14.0-dev.40

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 (80) hide show
  1. package/dist/cli.mjs +8 -8
  2. package/dist/{client-VA66WgBx.mjs → client-CWTNqJdj.mjs} +68 -67
  3. package/dist/client-CWTNqJdj.mjs.map +1 -0
  4. package/dist/commands/contract-infer.mjs +1 -1
  5. package/dist/commands/db-init.mjs +2 -2
  6. package/dist/commands/db-schema.mjs +2 -2
  7. package/dist/commands/db-sign.mjs +2 -2
  8. package/dist/commands/db-update.mjs +2 -2
  9. package/dist/commands/db-verify.mjs +1 -1
  10. package/dist/commands/migrate.d.mts +1 -1
  11. package/dist/commands/migrate.d.mts.map +1 -1
  12. package/dist/commands/migrate.mjs +31 -31
  13. package/dist/commands/migrate.mjs.map +1 -1
  14. package/dist/commands/migration-check.d.mts +1 -1
  15. package/dist/commands/migration-check.mjs +1 -1
  16. package/dist/commands/migration-graph.d.mts +1 -1
  17. package/dist/commands/migration-graph.mjs +5 -5
  18. package/dist/commands/migration-graph.mjs.map +1 -1
  19. package/dist/commands/migration-list.d.mts +6 -6
  20. package/dist/commands/migration-list.d.mts.map +1 -1
  21. package/dist/commands/migration-list.mjs +1 -1
  22. package/dist/commands/migration-log.d.mts +1 -1
  23. package/dist/commands/migration-log.mjs +1 -1
  24. package/dist/commands/migration-plan.mjs +1 -1
  25. package/dist/commands/migration-show.mjs +1 -1
  26. package/dist/commands/migration-status.d.mts +1 -1
  27. package/dist/commands/migration-status.mjs +1 -1
  28. package/dist/commands/ref.d.mts +1 -1
  29. package/dist/{contract-infer-Bmq_szvX.mjs → contract-infer-B__56PGb.mjs} +2 -2
  30. package/dist/{contract-infer-Bmq_szvX.mjs.map → contract-infer-B__56PGb.mjs.map} +1 -1
  31. package/dist/{db-verify-r157atLp.mjs → db-verify-DlqHvkkA.mjs} +65 -48
  32. package/dist/db-verify-DlqHvkkA.mjs.map +1 -0
  33. package/dist/exports/control-api.d.mts +3 -3
  34. package/dist/exports/control-api.mjs +1 -1
  35. package/dist/extension-pack-inputs-1ySHqxKG.mjs.map +1 -1
  36. package/dist/{inspect-live-schema-DBF3Rx17.mjs → inspect-live-schema-Dd1Ft0dw.mjs} +2 -2
  37. package/dist/{inspect-live-schema-DBF3Rx17.mjs.map → inspect-live-schema-Dd1Ft0dw.mjs.map} +1 -1
  38. package/dist/{migration-check-CTcJfCC4.mjs → migration-check-UoHMvsP3.mjs} +6 -6
  39. package/dist/migration-check-UoHMvsP3.mjs.map +1 -0
  40. package/dist/{migration-command-scaffold-bxooJ8Jl.mjs → migration-command-scaffold-DW2BpjZW.mjs} +2 -2
  41. package/dist/{migration-command-scaffold-bxooJ8Jl.mjs.map → migration-command-scaffold-DW2BpjZW.mjs.map} +1 -1
  42. package/dist/{migration-list-DVHVmMrf.mjs → migration-list-BOzQzJK4.mjs} +12 -12
  43. package/dist/migration-list-BOzQzJK4.mjs.map +1 -0
  44. package/dist/{migration-log-CBYftWK1.mjs → migration-log-q50rqMiU.mjs} +2 -2
  45. package/dist/{migration-log-CBYftWK1.mjs.map → migration-log-q50rqMiU.mjs.map} +1 -1
  46. package/dist/{migration-plan-o2aqjnfd.mjs → migration-plan-BwJLPkwV.mjs} +19 -19
  47. package/dist/migration-plan-BwJLPkwV.mjs.map +1 -0
  48. package/dist/{migration-status-BLa0SheI.mjs → migration-status-27NzO2SC.mjs} +10 -10
  49. package/dist/migration-status-27NzO2SC.mjs.map +1 -0
  50. package/dist/{types-qETCCNbe.d.mts → types-CKgJmH02.d.mts} +25 -19
  51. package/dist/types-CKgJmH02.d.mts.map +1 -0
  52. package/dist/{verify-CLw3YRbl.mjs → verify-DC14_nPu.mjs} +23 -5
  53. package/dist/verify-DC14_nPu.mjs.map +1 -0
  54. package/package.json +21 -21
  55. package/src/commands/db-verify.ts +31 -19
  56. package/src/commands/migrate.ts +44 -41
  57. package/src/commands/migration-check.ts +5 -5
  58. package/src/commands/migration-graph.ts +4 -4
  59. package/src/commands/migration-list.ts +12 -12
  60. package/src/commands/migration-plan.ts +3 -3
  61. package/src/commands/migration-status.ts +9 -9
  62. package/src/control-api/operations/db-init.ts +2 -2
  63. package/src/control-api/operations/db-run.ts +7 -7
  64. package/src/control-api/operations/db-verify.ts +37 -33
  65. package/src/control-api/operations/migrate.ts +66 -66
  66. package/src/control-api/operations/run-migration.ts +2 -2
  67. package/src/control-api/types.ts +7 -7
  68. package/src/utils/combine-verify-results.ts +113 -0
  69. package/src/utils/extension-pack-inputs.ts +1 -1
  70. package/src/utils/formatters/verify.ts +36 -3
  71. package/src/utils/plan-resolution.ts +18 -18
  72. package/dist/client-VA66WgBx.mjs.map +0 -1
  73. package/dist/db-verify-r157atLp.mjs.map +0 -1
  74. package/dist/migration-check-CTcJfCC4.mjs.map +0 -1
  75. package/dist/migration-list-DVHVmMrf.mjs.map +0 -1
  76. package/dist/migration-plan-o2aqjnfd.mjs.map +0 -1
  77. package/dist/migration-status-BLa0SheI.mjs.map +0 -1
  78. package/dist/types-qETCCNbe.d.mts.map +0 -1
  79. package/dist/verify-CLw3YRbl.mjs.map +0 -1
  80. package/src/utils/combine-schema-results.ts +0 -87
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Backs the `migrate` command. Strategy: graph-walk-all-members, replay-only (no introspect/synth/planner).
2
+ * Backs the `migrate` command. Strategy: graph-walk-all-spaces, replay-only (no introspect/synth/planner).
3
3
  */
4
4
 
5
5
  import type { Contract } from '@prisma-next/contract/types';
@@ -11,10 +11,10 @@ import type {
11
11
  TargetMigrationsCapability,
12
12
  } from '@prisma-next/framework-components/control';
13
13
  import {
14
+ type AggregateContractSpace,
14
15
  buildSynthMigrationEdge,
15
16
  type ContractMarkerRecordLike,
16
17
  type ContractSpaceAggregate,
17
- type ContractSpaceMember,
18
18
  graphWalkStrategy,
19
19
  type PerSpacePlan,
20
20
  requireHeadRef,
@@ -62,8 +62,8 @@ export interface ExecuteMigrateOptions<TFamilyId extends string, TTargetId exten
62
62
  readonly extensionPacks: ReadonlyArray<ControlExtensionDescriptor<TFamilyId, TTargetId>>;
63
63
  readonly targetId: TTargetId;
64
64
  /**
65
- * Optional app-space ref override. When provided, the app member's
66
- * graph-walk targets this hash instead of `member.headRef.hash`.
65
+ * Optional app-space ref override. When provided, the app space's
66
+ * graph-walk targets this hash instead of `space.headRef.hash`.
67
67
  * Extensions are unaffected — they always walk to their own head.
68
68
  */
69
69
  readonly refHash?: string;
@@ -72,7 +72,7 @@ export interface ExecuteMigrateOptions<TFamilyId extends string, TTargetId exten
72
72
  * Threaded into the graph-walk's `required` calculation so the
73
73
  * planner picks an invariant-bearing path and surfaces the
74
74
  * required/satisfied set on the success envelope. When `refHash`
75
- * is absent the file's `member.headRef.invariants` are used.
75
+ * is absent the file's `space.headRef.invariants` are used.
76
76
  */
77
77
  readonly refInvariants?: readonly string[];
78
78
  /**
@@ -89,16 +89,16 @@ export interface ExecuteMigrateOptions<TFamilyId extends string, TTargetId exten
89
89
  /**
90
90
  * Apply pending migrations across every contract space (app +
91
91
  * extensions). Replay-only: graph-walk against the on-disk graph for
92
- * every member; no synth, no introspection.
92
+ * every contract space; no synth, no introspection.
93
93
  *
94
94
  * Pipeline:
95
95
  *
96
96
  * 1. Load aggregate from disk (loader hydrates extension graphs;
97
97
  * caller provides app-space packages).
98
98
  * 2. Read live marker rows per space (`familyInstance.readAllMarkers`).
99
- * 3. Per member: `graphWalkStrategy` plots the path from the live
100
- * marker to `member.headRef.hash` (or `refHash` for the app
101
- * member when provided). Empty-graph members fail loudly — a
99
+ * 3. Per space: `graphWalkStrategy` plots the path from the live
100
+ * marker to `space.headRef.hash` (or `refHash` for the app
101
+ * space when provided). Empty-graph spaces fail loudly — a
102
102
  * "never planned" space is a user-error condition for replay.
103
103
  * 4. Hand off to {@link runMigration} (the runner-driving tail
104
104
  * shared with `db init` / `db update`). Marker advancement is
@@ -140,41 +140,41 @@ export async function executeMigrate<TFamilyId extends string, TTargetId extends
140
140
 
141
141
  const markerRows = await familyInstance.readAllMarkers({ driver });
142
142
 
143
- // Plan every member via graph-walk. App member targets `refHash`
143
+ // Plan every space via graph-walk. App space targets `refHash`
144
144
  // when provided, otherwise its own head; extensions always walk
145
145
  // to their own head ref.
146
- const allMembers: ReadonlyArray<ContractSpaceMember> = [aggregate.app, ...aggregate.extensions];
146
+ const allSpaces: ReadonlyArray<AggregateContractSpace> = [aggregate.app, ...aggregate.extensions];
147
147
  const perSpacePlans = new Map<string, PerSpacePlan>();
148
- // Already-at-head empty-graph members (typically extensions whose
148
+ // Already-at-head empty-graph spaces (typically extensions whose
149
149
  // head ref is the empty sentinel, or whose live marker already
150
150
  // matches the target). Kept out of the runner schedule so we don't
151
151
  // write spurious markers for greenfield extensions, but merged back
152
- // into the success envelope so every loaded member is represented.
152
+ // into the success envelope so every loaded space is represented.
153
153
  const atHeadResolutions = new Map<string, PerSpacePlan>();
154
- for (const member of allMembers) {
155
- const isAppMember = member.spaceId === aggregate.app.spaceId;
156
- // The aggregate passed the integrity gate, so every member's head ref
154
+ for (const space of allSpaces) {
155
+ const isAppSpace = space.spaceId === aggregate.app.spaceId;
156
+ // The aggregate passed the integrity gate, so every space's head ref
157
157
  // is resolved (the app's is synthesised from the live contract).
158
- const headRef = requireHeadRef(member);
159
- const memberTargetHash = isAppMember && refHash !== undefined ? refHash : headRef.hash;
160
- const memberRefInvariants = isAppMember && refHash !== undefined ? refInvariants : undefined;
161
- const liveMarker = markerRows.get(member.spaceId) ?? null;
158
+ const headRef = requireHeadRef(space);
159
+ const spaceTargetHash = isAppSpace && refHash !== undefined ? refHash : headRef.hash;
160
+ const spaceRefInvariants = isAppSpace && refHash !== undefined ? refInvariants : undefined;
161
+ const liveMarker = markerRows.get(space.spaceId) ?? null;
162
162
 
163
- const outcome = planMemberPath({
164
- member,
163
+ const outcome = planSpacePath({
164
+ space,
165
165
  aggregate,
166
- targetHash: memberTargetHash,
167
- refInvariants: memberRefInvariants,
166
+ targetHash: spaceTargetHash,
167
+ refInvariants: spaceRefInvariants,
168
168
  liveMarker,
169
- ...(isAppMember ? { refName } : {}),
169
+ ...(isAppSpace ? { refName } : {}),
170
170
  });
171
171
 
172
172
  if (outcome.kind === 'at-head') {
173
- // Empty-graph member whose live marker already matches the target.
173
+ // Empty-graph space whose live marker already matches the target.
174
174
  // Kept out of the runner schedule so we don't write spurious markers
175
175
  // for greenfield extensions, but merged back into the success envelope
176
- // so every loaded member is represented.
177
- atHeadResolutions.set(member.spaceId, outcome.plan);
176
+ // so every loaded space is represented.
177
+ atHeadResolutions.set(space.spaceId, outcome.plan);
178
178
  continue;
179
179
  }
180
180
  if (outcome.kind === 'never-planned') {
@@ -197,9 +197,9 @@ export async function executeMigrate<TFamilyId extends string, TTargetId extends
197
197
  // is never a graph node, producing an empty `structuralPath` and
198
198
  // a less actionable diagnostic.
199
199
  const structural = findPathWithDecision(
200
- outcome.targetMember.graph(),
200
+ outcome.targetSpace.graph(),
201
201
  outcome.liveHash,
202
- memberTargetHash,
202
+ spaceTargetHash,
203
203
  { required: new Set<string>() },
204
204
  );
205
205
  const structuralPath =
@@ -220,14 +220,14 @@ export async function executeMigrate<TFamilyId extends string, TTargetId extends
220
220
  });
221
221
  }
222
222
 
223
- perSpacePlans.set(member.spaceId, outcome.plan);
223
+ perSpacePlans.set(space.spaceId, outcome.plan);
224
224
  }
225
225
 
226
226
  const canonicalOrder = [...aggregate.extensions.map((m) => m.spaceId), aggregate.app.spaceId];
227
227
  const applyOrder = canonicalOrder.filter((spaceId) => perSpacePlans.has(spaceId));
228
228
 
229
229
  // Short-circuit: nothing pending across any space (no runner-bound
230
- // plans). Surfaces every loaded member — including at-head empty-
230
+ // plans). Surfaces every loaded space — including at-head empty-
231
231
  // graph extensions — in `perSpace[]` so the result reflects the
232
232
  // full aggregate, not just the spaces the runner would have touched.
233
233
  const totalPlannedOps = sumPlannedOps(applyOrder, perSpacePlans);
@@ -285,7 +285,7 @@ export async function executeMigrate<TFamilyId extends string, TTargetId extends
285
285
  }
286
286
 
287
287
  // Merge at-head zero-op resolutions back into the canonical order
288
- // so the success envelope surfaces every loaded member, not just
288
+ // so the success envelope surfaces every loaded space, not just
289
289
  // those the runner executed.
290
290
  const orderedAll = canonicalOrder
291
291
  .filter((spaceId) => perSpacePlans.has(spaceId) || atHeadResolutions.has(spaceId))
@@ -321,7 +321,7 @@ export async function executeMigrate<TFamilyId extends string, TTargetId extends
321
321
  }
322
322
 
323
323
  /**
324
- * Outcome variants for one member's path computation.
324
+ * Outcome variants for one space's path computation.
325
325
  *
326
326
  * Callers switch on `kind` and map to their own error representation:
327
327
  * `executeMigrate` throws / returns `notOk`; `executeMigrateShowCommand`
@@ -330,7 +330,7 @@ export async function executeMigrate<TFamilyId extends string, TTargetId extends
330
330
  *
331
331
  * @internal Exported for `executeMigrateShowCommand` to call.
332
332
  */
333
- export type MemberPathOutcome =
333
+ export type SpacePathOutcome =
334
334
  | { readonly kind: 'ok'; readonly plan: PerSpacePlan }
335
335
  | { readonly kind: 'at-head'; readonly plan: PerSpacePlan }
336
336
  | { readonly kind: 'never-planned'; readonly spaceId: string; readonly targetHash: string }
@@ -343,16 +343,16 @@ export type MemberPathOutcome =
343
343
  | {
344
344
  readonly kind: 'unsatisfiable';
345
345
  readonly spaceId: string;
346
- readonly isAppMember: boolean;
346
+ readonly isAppSpace: boolean;
347
347
  readonly missing: readonly string[];
348
348
  readonly targetInvariants: readonly string[];
349
- readonly targetMember: ContractSpaceMember;
349
+ readonly targetSpace: AggregateContractSpace;
350
350
  readonly liveHash: string;
351
351
  readonly refName: string | undefined;
352
352
  };
353
353
 
354
354
  /**
355
- * Compute the graph-walk path for one contract-space member.
355
+ * Compute the graph-walk path for one contract space.
356
356
  *
357
357
  * Encapsulates the invariant-correct input assembly that both
358
358
  * `executeMigrate` and `executeMigrateShowCommand` must use:
@@ -361,71 +361,71 @@ export type MemberPathOutcome =
361
361
  * - `targetInvariants` uses the caller-supplied `refInvariants` when a
362
362
  * `--to` ref was resolved (not always the file head ref's invariants).
363
363
  *
364
- * Both callers map the returned `MemberPathOutcome` to their own error
364
+ * Both callers map the returned `SpacePathOutcome` to their own error
365
365
  * representation; the path-compute logic is shared and identical.
366
366
  *
367
367
  * @internal Exported for `executeMigrateShowCommand`.
368
368
  */
369
- export function planMemberPath({
370
- member,
369
+ export function planSpacePath({
370
+ space,
371
371
  aggregate,
372
372
  targetHash,
373
373
  refInvariants,
374
374
  liveMarker,
375
375
  refName,
376
376
  }: {
377
- readonly member: ContractSpaceMember;
377
+ readonly space: AggregateContractSpace;
378
378
  readonly aggregate: Pick<ContractSpaceAggregate, 'targetId' | 'app'>;
379
379
  readonly targetHash: string;
380
380
  readonly refInvariants: readonly string[] | undefined;
381
381
  readonly liveMarker: ContractMarkerRecordLike | null;
382
382
  readonly refName?: string;
383
- }): MemberPathOutcome {
384
- const isAppMember = member.spaceId === aggregate.app.spaceId;
385
- const headRef = requireHeadRef(member);
383
+ }): SpacePathOutcome {
384
+ const isAppSpace = space.spaceId === aggregate.app.spaceId;
385
+ const headRef = requireHeadRef(space);
386
386
 
387
- if (member.graph().nodes.size === 0) {
387
+ if (space.graph().nodes.size === 0) {
388
388
  const liveHash = liveMarker?.storageHash;
389
389
  if (targetHash === liveHash || (liveHash === undefined && targetHash === EMPTY_CONTRACT_HASH)) {
390
390
  return {
391
391
  kind: 'at-head',
392
392
  plan: buildAtHeadResolution({
393
393
  aggregateTargetId: aggregate.targetId,
394
- member,
394
+ space,
395
395
  targetHash,
396
396
  liveMarker,
397
397
  }),
398
398
  };
399
399
  }
400
- return { kind: 'never-planned', spaceId: member.spaceId, targetHash };
400
+ return { kind: 'never-planned', spaceId: space.spaceId, targetHash };
401
401
  }
402
402
 
403
403
  const targetInvariants =
404
- isAppMember && refInvariants !== undefined ? refInvariants : headRef.invariants;
405
- const targetMember: ContractSpaceMember =
404
+ isAppSpace && refInvariants !== undefined ? refInvariants : headRef.invariants;
405
+ const targetSpace: AggregateContractSpace =
406
406
  targetHash === headRef.hash && targetInvariants === headRef.invariants
407
- ? member
408
- : { ...member, headRef: { hash: targetHash, invariants: targetInvariants } };
407
+ ? space
408
+ : { ...space, headRef: { hash: targetHash, invariants: targetInvariants } };
409
409
 
410
410
  const walked = graphWalkStrategy({
411
411
  aggregateTargetId: aggregate.targetId,
412
- member: targetMember,
412
+ space: targetSpace,
413
413
  currentMarker: liveMarker,
414
- ...(isAppMember && refName !== undefined ? { refName } : {}),
414
+ ...(isAppSpace && refName !== undefined ? { refName } : {}),
415
415
  });
416
416
 
417
417
  if (walked.kind === 'unreachable') {
418
- return { kind: 'unreachable', spaceId: member.spaceId, liveMarker, targetHash };
418
+ return { kind: 'unreachable', spaceId: space.spaceId, liveMarker, targetHash };
419
419
  }
420
420
  if (walked.kind === 'unsatisfiable') {
421
421
  const liveHash = liveMarker?.storageHash ?? EMPTY_CONTRACT_HASH;
422
422
  return {
423
423
  kind: 'unsatisfiable',
424
- spaceId: member.spaceId,
425
- isAppMember,
424
+ spaceId: space.spaceId,
425
+ isAppSpace,
426
426
  missing: walked.missing,
427
427
  targetInvariants,
428
- targetMember,
428
+ targetSpace,
429
429
  liveHash,
430
430
  refName,
431
431
  };
@@ -435,29 +435,29 @@ export function planMemberPath({
435
435
 
436
436
  /**
437
437
  * Build a zero-op {@link PerSpacePlan} for an empty-graph
438
- * member whose live marker already matches the target. Lets the apply
439
- * pipeline thread the member through `perSpacePlans` -> `applyOrder`
438
+ * space whose live marker already matches the target. Lets the apply
439
+ * pipeline thread the space through `perSpacePlans` -> `applyOrder`
440
440
  * -> the success envelope's `perSpace[]` block so the result reflects
441
441
  * every loaded space, even when there is nothing to execute.
442
442
  */
443
443
  function buildAtHeadResolution(args: {
444
444
  readonly aggregateTargetId: string;
445
- readonly member: ContractSpaceMember;
445
+ readonly space: AggregateContractSpace;
446
446
  readonly targetHash: string;
447
447
  readonly liveMarker: ContractMarkerRecordLike | null;
448
448
  }): PerSpacePlan {
449
- const { aggregateTargetId, member, targetHash, liveMarker } = args;
449
+ const { aggregateTargetId, space, targetHash, liveMarker } = args;
450
450
  return {
451
451
  plan: {
452
452
  targetId: aggregateTargetId,
453
- spaceId: member.spaceId,
453
+ spaceId: space.spaceId,
454
454
  origin: liveMarker === null ? null : { storageHash: liveMarker.storageHash },
455
455
  destination: { storageHash: targetHash },
456
456
  operations: [],
457
457
  providedInvariants: [],
458
458
  },
459
459
  displayOps: [],
460
- destinationContract: member.contract(),
460
+ destinationContract: space.contract(),
461
461
  strategy: 'graph-walk',
462
462
  migrationEdges: [
463
463
  buildSynthMigrationEdge({
@@ -494,7 +494,7 @@ interface BuildSuccessArgs {
494
494
  }
495
495
 
496
496
  function buildSuccess(args: BuildSuccessArgs): MigrateSuccess {
497
- // The marker hash surfaced at the top level is the **app member's**
497
+ // The marker hash surfaced at the top level is the **app space's**
498
498
  // post-migrate marker (the top-level `markerHash` field).
499
499
  // Per-space markers live on `perSpace[].marker.storageHash`.
500
500
  const appResolution = args.orderedResolutions.find(
@@ -586,7 +586,7 @@ export function buildPathNotFoundFailure(
586
586
  const fromHash = marker?.storageHash ?? '<empty>';
587
587
  // The app-case phrasing names the user-visible condition (a
588
588
  // contract has been emitted that no on-disk migration reaches) so
589
- // the error reads naturally for the app member. Extension spaces
589
+ // the error reads naturally for the app space. Extension spaces
590
590
  // see the same condition expressed against the offending space.
591
591
  const summary =
592
592
  spaceId === 'app'
@@ -48,7 +48,7 @@ export interface RunMigrationInputs<TFamilyId extends string, TTargetId extends
48
48
  * {@link planMigration} pipeline (`db init` / `db update` — synth
49
49
  * for the app, graph-walk for extensions) or by direct
50
50
  * {@link graphWalkStrategy} calls (`migrate` — graph-walk
51
- * for every member). Either way, the runner consumes the same shape.
51
+ * for every contract space). Either way, the runner consumes the same shape.
52
52
  */
53
53
  readonly perSpacePlans: ReadonlyMap<string, PerSpacePlan>;
54
54
  /**
@@ -229,7 +229,7 @@ export function buildPerSpaceBreakdown(
229
229
 
230
230
  /**
231
231
  * Materialise the `applyOrder` ordering into resolved per-space
232
- * entries. Throws if the planner output is missing a member listed
232
+ * entries. Throws if the planner output is missing a contract space listed
233
233
  * in `applyOrder` — a wiring bug that should never reach runtime.
234
234
  *
235
235
  * Exported so callers building their own success envelopes after a
@@ -557,9 +557,9 @@ export interface MigrateOptions {
557
557
  /** Migrations root directory (`migrations/` under the project). */
558
558
  readonly migrationsDir: string;
559
559
  /**
560
- * Optional app-space ref override. When provided, the app member's
560
+ * Optional app-space ref override. When provided, the app space's
561
561
  * graph-walk targets this hash instead of `contract.storage.storageHash`.
562
- * Extension members always walk to their own `headRef.hash`.
562
+ * Extension spaces always walk to their own `headRef.hash`.
563
563
  */
564
564
  readonly refHash?: string;
565
565
  /**
@@ -631,17 +631,17 @@ export interface MigrateRanEntry {
631
631
 
632
632
  /**
633
633
  * Successful migrate result. Carries both the top-level fields
634
- * (`markerHash` is the **app member's** post-migrate marker) and the
634
+ * (`markerHash` is the **app space's** post-migrate marker) and the
635
635
  * per-space breakdown (`perSpace` — markers / operations in canonical
636
636
  * schedule order).
637
637
  */
638
638
  /**
639
- * Path-decision summary for the **app member** post-migrate. Surfaced
639
+ * Path-decision summary for the **app space** post-migrate. Surfaced
640
640
  * at the top level (and consumed by the cli-journeys suite, which
641
641
  * inspects `requiredInvariants`/`satisfiedInvariants`/
642
642
  * `selectedPath` to validate invariant routing).
643
643
  *
644
- * Per-space path decisions for extension members are not surfaced —
644
+ * Per-space path decisions for extension spaces are not surfaced —
645
645
  * extensions own their own ref/invariant control.
646
646
  */
647
647
  export interface MigratePathDecision {
@@ -673,7 +673,7 @@ export interface MigrateSuccess {
673
673
  */
674
674
  readonly perSpace: ReadonlyArray<PerSpaceExecutionEntry>;
675
675
  /**
676
- * Path-decision data for the app member. Present whenever the
676
+ * Path-decision data for the app space. Present whenever the
677
677
  * graph-walk strategy ran for the app (i.e. always for the
678
678
  * aggregate-walking migrate path). Absent only for the no-op
679
679
  * "Already up to date" early return when the app has no plan.
@@ -864,7 +864,7 @@ export interface ControlClient {
864
864
  * markers, and (unless `skipSchema` is true) per-space schema
865
865
  * verification with pre-projection (closes F23).
866
866
  *
867
- * @returns Result pattern: per-space schema results on success;
867
+ * @returns Result pattern: per-space verify results on success;
868
868
  * structured CLI error on marker / loader failure.
869
869
  * @throws If not connected or infrastructure failure
870
870
  */
@@ -0,0 +1,113 @@
1
+ import type { VerifyDatabaseSchemaResult } from '@prisma-next/framework-components/control';
2
+
3
+ /**
4
+ * The combined per-space contract-satisfaction result plus the standalone
5
+ * unclaimed-elements list, reported once. The CLI renders
6
+ * both; `unclaimed` is never folded into the combined tree or its issues.
7
+ */
8
+ export interface CombinedVerifyResult {
9
+ readonly result: VerifyDatabaseSchemaResult;
10
+ readonly unclaimed: readonly string[];
11
+ }
12
+
13
+ /**
14
+ * Collapse the aggregate verifier's per-space contract-satisfaction results
15
+ * into a single {@link VerifyDatabaseSchemaResult} for the existing CLI
16
+ * display surface, and carry the deduplicated unclaimed-elements list
17
+ * alongside it. Concatenates issues across spaces; sums counts; uses the app
18
+ * space's result as the structural envelope (storage hash, target). Extras are
19
+ * already stripped from each per-space result, so nothing here duplicates an
20
+ * unclaimed element per space.
21
+ *
22
+ * **Unclaimed disposition.** In strict mode a non-empty `unclaimed` list fails
23
+ * the combined verdict (`ok: false`); in lenient mode it is carried for
24
+ * informational rendering only. The list itself is returned unchanged for the
25
+ * renderer.
26
+ *
27
+ * **Summary policy.** Preserve the per-family phrasing whenever the
28
+ * combined `ok` flag agrees with the app space's `ok` flag — this is
29
+ * the common case (single-family deployments, single-app deployments)
30
+ * and the family's "satisfies / does not satisfy contract" phrasing
31
+ * stays user-visible. When the app passes but an extension fails (or
32
+ * vice versa) the app's summary contradicts the envelope, so fall back
33
+ * to the first failing space's summary. This keeps family phrasing
34
+ * intact and the envelope internally consistent (`ok: false` ↔ failure
35
+ * summary).
36
+ */
37
+ export function combineVerifyResults(
38
+ perSpace: ReadonlyMap<string, VerifyDatabaseSchemaResult>,
39
+ appSpaceId: string,
40
+ strict: boolean,
41
+ unclaimed: readonly string[],
42
+ ): CombinedVerifyResult {
43
+ const appResult = perSpace.get(appSpaceId) ?? perSpace.values().next().value;
44
+ if (appResult === undefined) {
45
+ throw new Error(
46
+ 'Aggregate verifier returned no per-space verify results — this is a wiring bug.',
47
+ );
48
+ }
49
+
50
+ let okAll = true;
51
+ let firstFailure: VerifyDatabaseSchemaResult | undefined;
52
+ let issues: VerifyDatabaseSchemaResult['schema']['issues'] = [];
53
+ let schemaDiffIssues: VerifyDatabaseSchemaResult['schema']['schemaDiffIssues'] = [];
54
+ const counts = { pass: 0, warn: 0, fail: 0, totalNodes: 0 };
55
+ const childRoots: Array<VerifyDatabaseSchemaResult['schema']['root']> = [];
56
+ for (const [, result] of perSpace) {
57
+ if (!result.ok) {
58
+ okAll = false;
59
+ if (firstFailure === undefined) firstFailure = result;
60
+ }
61
+ issues = [...issues, ...result.schema.issues];
62
+ schemaDiffIssues = [...schemaDiffIssues, ...result.schema.schemaDiffIssues];
63
+ counts.pass += result.schema.counts.pass;
64
+ counts.warn += result.schema.counts.warn;
65
+ counts.fail += result.schema.counts.fail;
66
+ counts.totalNodes += result.schema.counts.totalNodes;
67
+ childRoots.push(result.schema.root);
68
+ }
69
+
70
+ const unclaimedFails = strict && unclaimed.length > 0;
71
+ const ok = okAll && !unclaimedFails;
72
+
73
+ // Prefer a failing space's family phrasing; else, when only the unclaimed list
74
+ // fails the verdict, say so; else keep the app space's phrasing. When `okAll`
75
+ // is false the loop assigned `firstFailure`, so the `?? appResult.summary`
76
+ // fallback is unreachable — it exists only to keep the read cast-free.
77
+ const summary = okAll
78
+ ? unclaimedFails
79
+ ? `Database schema has ${unclaimed.length} unclaimed element${unclaimed.length === 1 ? '' : 's'} (not in any contract)`
80
+ : appResult.summary
81
+ : appResult.ok
82
+ ? (firstFailure?.summary ?? appResult.summary)
83
+ : appResult.summary;
84
+
85
+ return {
86
+ result: {
87
+ ok,
88
+ ...(ok ? {} : { code: appResult.code ?? 'PN-RUN-3010' }),
89
+ summary,
90
+ contract: appResult.contract,
91
+ target: appResult.target,
92
+ schema: {
93
+ issues,
94
+ schemaDiffIssues,
95
+ root: {
96
+ status: ok ? 'pass' : 'fail',
97
+ kind: 'aggregate',
98
+ name: 'aggregate',
99
+ contractPath: '',
100
+ code: 'AGGREGATE',
101
+ message: ok ? 'Aggregate schema matches' : 'Aggregate schema mismatch',
102
+ expected: undefined,
103
+ actual: undefined,
104
+ children: childRoots,
105
+ },
106
+ counts,
107
+ },
108
+ meta: { strict },
109
+ timings: { total: 0 },
110
+ },
111
+ unclaimed,
112
+ };
113
+ }
@@ -111,7 +111,7 @@ export function toExtensionInputs(
111
111
  * {@link import('./contract-space-aggregate-loader').buildContractSpaceAggregate}.
112
112
  *
113
113
  * Codec-only extensions (no `contractSpace` declaration) are filtered
114
- * out: they are not contract-space members, so the aggregate loader
114
+ * out: they contribute no contract space, so the aggregate loader
115
115
  * has nothing to do with them. Filtering happens at this descriptor-
116
116
  * import boundary so the loader stays oblivious to that distinction —
117
117
  * every entry it sees expects an on-disk `migrations/<id>/` directory.
@@ -30,6 +30,12 @@ export interface DbVerifyCommandSuccessResult {
30
30
  readonly counts: VerifyDatabaseSchemaResult['schema']['counts'];
31
31
  readonly strict: boolean;
32
32
  };
33
+ /**
34
+ * Live element names no contract space declares. In full success this is
35
+ * only ever non-empty in lenient mode — strict mode fails on it — and is
36
+ * rendered informationally.
37
+ */
38
+ readonly unclaimed?: readonly string[];
33
39
  readonly warning?: string;
34
40
  readonly meta?:
35
41
  | (NonNullable<VerifyDatabaseResult['meta']> & {
@@ -76,6 +82,14 @@ export function formatVerifyOutput(
76
82
  `${formatDimText(` schema: pass=${result.schema.counts.pass} warn=${result.schema.counts.warn} fail=${result.schema.counts.fail}`)}`,
77
83
  );
78
84
  }
85
+ if (result.unclaimed && result.unclaimed.length > 0) {
86
+ lines.push('');
87
+ lines.push(formatYellow('Unclaimed elements (declared by no contract):'));
88
+ for (const name of result.unclaimed) {
89
+ lines.push(` ${formatYellow('⚠')} ${name}`);
90
+ }
91
+ }
92
+
79
93
  if (result.warning) {
80
94
  lines.push('');
81
95
  lines.push(`${formatYellow('⚠')} ${result.warning}`);
@@ -107,6 +121,7 @@ export function formatVerifyJson(result: DbVerifyCommandSuccessResult): string {
107
121
  ...ifDefined('missingCodecs', result.missingCodecs),
108
122
  ...ifDefined('codecCoverageSkipped', result.codecCoverageSkipped),
109
123
  ...ifDefined('schema', result.schema),
124
+ unclaimed: result.unclaimed ?? [],
110
125
  ...ifDefined('warning', result.warning),
111
126
  ...ifDefined('meta', result.meta),
112
127
  timings: result.timings,
@@ -517,6 +532,7 @@ function renderSchemaVerificationTree(
517
532
  export function formatSchemaVerifyOutput(
518
533
  result: VerifyDatabaseSchemaResult,
519
534
  flags: GlobalFlags,
535
+ unclaimed: readonly string[] = [],
520
536
  ): string {
521
537
  if (flags.quiet) {
522
538
  return '';
@@ -527,6 +543,7 @@ export function formatSchemaVerifyOutput(
527
543
  const useColor = flags.color !== false;
528
544
  const formatGreen = createColorFormatter(useColor, green);
529
545
  const formatRed = createColorFormatter(useColor, red);
546
+ const formatYellow = createColorFormatter(useColor, yellow);
530
547
  const formatDimText = (text: string) => formatDim(useColor, text);
531
548
 
532
549
  // Render verification tree first
@@ -547,6 +564,17 @@ export function formatSchemaVerifyOutput(
547
564
  }
548
565
  }
549
566
 
567
+ if (unclaimed.length > 0) {
568
+ const strict = result.meta?.strict ?? false;
569
+ lines.push('');
570
+ lines.push(
571
+ (strict ? formatRed : formatYellow)('Unclaimed elements (declared by no contract):'),
572
+ );
573
+ for (const name of unclaimed) {
574
+ lines.push(` ${(strict ? formatRed : formatYellow)(strict ? '✖' : '⚠')} ${name}`);
575
+ }
576
+ }
577
+
550
578
  // Add counts and timings in verbose mode
551
579
  if (isVerbose(flags, 1)) {
552
580
  lines.push(`${formatDimText(` Total time: ${result.timings.total}ms`)}`);
@@ -570,10 +598,15 @@ export function formatSchemaVerifyOutput(
570
598
  }
571
599
 
572
600
  /**
573
- * Formats JSON output for database schema verification.
601
+ * Formats JSON output for database schema verification. The unclaimed-elements
602
+ * list is a top-level field alongside the combined result, reported once for
603
+ * the whole database.
574
604
  */
575
- export function formatSchemaVerifyJson(result: VerifyDatabaseSchemaResult): string {
576
- return JSON.stringify(result, null, 2);
605
+ export function formatSchemaVerifyJson(
606
+ result: VerifyDatabaseSchemaResult,
607
+ unclaimed: readonly string[] = [],
608
+ ): string {
609
+ return JSON.stringify({ ...result, unclaimed }, null, 2);
577
610
  }
578
611
 
579
612
  // ============================================================================