@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.
- package/dist/cli.mjs +8 -8
- package/dist/{client-VA66WgBx.mjs → client-CWTNqJdj.mjs} +68 -67
- package/dist/client-CWTNqJdj.mjs.map +1 -0
- package/dist/commands/contract-infer.mjs +1 -1
- package/dist/commands/db-init.mjs +2 -2
- package/dist/commands/db-schema.mjs +2 -2
- package/dist/commands/db-sign.mjs +2 -2
- package/dist/commands/db-update.mjs +2 -2
- package/dist/commands/db-verify.mjs +1 -1
- package/dist/commands/migrate.d.mts +1 -1
- package/dist/commands/migrate.d.mts.map +1 -1
- package/dist/commands/migrate.mjs +31 -31
- package/dist/commands/migrate.mjs.map +1 -1
- package/dist/commands/migration-check.d.mts +1 -1
- package/dist/commands/migration-check.mjs +1 -1
- package/dist/commands/migration-graph.d.mts +1 -1
- package/dist/commands/migration-graph.mjs +5 -5
- package/dist/commands/migration-graph.mjs.map +1 -1
- package/dist/commands/migration-list.d.mts +6 -6
- package/dist/commands/migration-list.d.mts.map +1 -1
- package/dist/commands/migration-list.mjs +1 -1
- package/dist/commands/migration-log.d.mts +1 -1
- package/dist/commands/migration-log.mjs +1 -1
- package/dist/commands/migration-plan.mjs +1 -1
- package/dist/commands/migration-show.mjs +1 -1
- package/dist/commands/migration-status.d.mts +1 -1
- package/dist/commands/migration-status.mjs +1 -1
- package/dist/commands/ref.d.mts +1 -1
- package/dist/{contract-infer-Bmq_szvX.mjs → contract-infer-B__56PGb.mjs} +2 -2
- package/dist/{contract-infer-Bmq_szvX.mjs.map → contract-infer-B__56PGb.mjs.map} +1 -1
- package/dist/{db-verify-r157atLp.mjs → db-verify-DlqHvkkA.mjs} +65 -48
- package/dist/db-verify-DlqHvkkA.mjs.map +1 -0
- package/dist/exports/control-api.d.mts +3 -3
- package/dist/exports/control-api.mjs +1 -1
- package/dist/extension-pack-inputs-1ySHqxKG.mjs.map +1 -1
- package/dist/{inspect-live-schema-DBF3Rx17.mjs → inspect-live-schema-Dd1Ft0dw.mjs} +2 -2
- package/dist/{inspect-live-schema-DBF3Rx17.mjs.map → inspect-live-schema-Dd1Ft0dw.mjs.map} +1 -1
- package/dist/{migration-check-CTcJfCC4.mjs → migration-check-UoHMvsP3.mjs} +6 -6
- package/dist/migration-check-UoHMvsP3.mjs.map +1 -0
- package/dist/{migration-command-scaffold-bxooJ8Jl.mjs → migration-command-scaffold-DW2BpjZW.mjs} +2 -2
- package/dist/{migration-command-scaffold-bxooJ8Jl.mjs.map → migration-command-scaffold-DW2BpjZW.mjs.map} +1 -1
- package/dist/{migration-list-DVHVmMrf.mjs → migration-list-BOzQzJK4.mjs} +12 -12
- package/dist/migration-list-BOzQzJK4.mjs.map +1 -0
- package/dist/{migration-log-CBYftWK1.mjs → migration-log-q50rqMiU.mjs} +2 -2
- package/dist/{migration-log-CBYftWK1.mjs.map → migration-log-q50rqMiU.mjs.map} +1 -1
- package/dist/{migration-plan-o2aqjnfd.mjs → migration-plan-BwJLPkwV.mjs} +19 -19
- package/dist/migration-plan-BwJLPkwV.mjs.map +1 -0
- package/dist/{migration-status-BLa0SheI.mjs → migration-status-27NzO2SC.mjs} +10 -10
- package/dist/migration-status-27NzO2SC.mjs.map +1 -0
- package/dist/{types-qETCCNbe.d.mts → types-CKgJmH02.d.mts} +25 -19
- package/dist/types-CKgJmH02.d.mts.map +1 -0
- package/dist/{verify-CLw3YRbl.mjs → verify-DC14_nPu.mjs} +23 -5
- package/dist/verify-DC14_nPu.mjs.map +1 -0
- package/package.json +21 -21
- package/src/commands/db-verify.ts +31 -19
- package/src/commands/migrate.ts +44 -41
- package/src/commands/migration-check.ts +5 -5
- package/src/commands/migration-graph.ts +4 -4
- package/src/commands/migration-list.ts +12 -12
- package/src/commands/migration-plan.ts +3 -3
- package/src/commands/migration-status.ts +9 -9
- package/src/control-api/operations/db-init.ts +2 -2
- package/src/control-api/operations/db-run.ts +7 -7
- package/src/control-api/operations/db-verify.ts +37 -33
- package/src/control-api/operations/migrate.ts +66 -66
- package/src/control-api/operations/run-migration.ts +2 -2
- package/src/control-api/types.ts +7 -7
- package/src/utils/combine-verify-results.ts +113 -0
- package/src/utils/extension-pack-inputs.ts +1 -1
- package/src/utils/formatters/verify.ts +36 -3
- package/src/utils/plan-resolution.ts +18 -18
- package/dist/client-VA66WgBx.mjs.map +0 -1
- package/dist/db-verify-r157atLp.mjs.map +0 -1
- package/dist/migration-check-CTcJfCC4.mjs.map +0 -1
- package/dist/migration-list-DVHVmMrf.mjs.map +0 -1
- package/dist/migration-plan-o2aqjnfd.mjs.map +0 -1
- package/dist/migration-status-BLa0SheI.mjs.map +0 -1
- package/dist/types-qETCCNbe.d.mts.map +0 -1
- package/dist/verify-CLw3YRbl.mjs.map +0 -1
- package/src/utils/combine-schema-results.ts +0 -87
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Backs the `migrate` command. Strategy: graph-walk-all-
|
|
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
|
|
66
|
-
* graph-walk targets this hash instead of `
|
|
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 `
|
|
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
|
|
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
|
|
100
|
-
* marker to `
|
|
101
|
-
*
|
|
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
|
|
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
|
|
146
|
+
const allSpaces: ReadonlyArray<AggregateContractSpace> = [aggregate.app, ...aggregate.extensions];
|
|
147
147
|
const perSpacePlans = new Map<string, PerSpacePlan>();
|
|
148
|
-
// Already-at-head empty-graph
|
|
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
|
|
152
|
+
// into the success envelope so every loaded space is represented.
|
|
153
153
|
const atHeadResolutions = new Map<string, PerSpacePlan>();
|
|
154
|
-
for (const
|
|
155
|
-
const
|
|
156
|
-
// The aggregate passed the integrity gate, so every
|
|
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(
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
const liveMarker = markerRows.get(
|
|
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 =
|
|
164
|
-
|
|
163
|
+
const outcome = planSpacePath({
|
|
164
|
+
space,
|
|
165
165
|
aggregate,
|
|
166
|
-
targetHash:
|
|
167
|
-
refInvariants:
|
|
166
|
+
targetHash: spaceTargetHash,
|
|
167
|
+
refInvariants: spaceRefInvariants,
|
|
168
168
|
liveMarker,
|
|
169
|
-
...(
|
|
169
|
+
...(isAppSpace ? { refName } : {}),
|
|
170
170
|
});
|
|
171
171
|
|
|
172
172
|
if (outcome.kind === 'at-head') {
|
|
173
|
-
// Empty-graph
|
|
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
|
|
177
|
-
atHeadResolutions.set(
|
|
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.
|
|
200
|
+
outcome.targetSpace.graph(),
|
|
201
201
|
outcome.liveHash,
|
|
202
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
346
|
+
readonly isAppSpace: boolean;
|
|
347
347
|
readonly missing: readonly string[];
|
|
348
348
|
readonly targetInvariants: readonly string[];
|
|
349
|
-
readonly
|
|
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
|
|
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 `
|
|
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
|
|
370
|
-
|
|
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
|
|
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
|
-
}):
|
|
384
|
-
const
|
|
385
|
-
const headRef = requireHeadRef(
|
|
383
|
+
}): SpacePathOutcome {
|
|
384
|
+
const isAppSpace = space.spaceId === aggregate.app.spaceId;
|
|
385
|
+
const headRef = requireHeadRef(space);
|
|
386
386
|
|
|
387
|
-
if (
|
|
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
|
-
|
|
394
|
+
space,
|
|
395
395
|
targetHash,
|
|
396
396
|
liveMarker,
|
|
397
397
|
}),
|
|
398
398
|
};
|
|
399
399
|
}
|
|
400
|
-
return { kind: 'never-planned', spaceId:
|
|
400
|
+
return { kind: 'never-planned', spaceId: space.spaceId, targetHash };
|
|
401
401
|
}
|
|
402
402
|
|
|
403
403
|
const targetInvariants =
|
|
404
|
-
|
|
405
|
-
const
|
|
404
|
+
isAppSpace && refInvariants !== undefined ? refInvariants : headRef.invariants;
|
|
405
|
+
const targetSpace: AggregateContractSpace =
|
|
406
406
|
targetHash === headRef.hash && targetInvariants === headRef.invariants
|
|
407
|
-
?
|
|
408
|
-
: { ...
|
|
407
|
+
? space
|
|
408
|
+
: { ...space, headRef: { hash: targetHash, invariants: targetInvariants } };
|
|
409
409
|
|
|
410
410
|
const walked = graphWalkStrategy({
|
|
411
411
|
aggregateTargetId: aggregate.targetId,
|
|
412
|
-
|
|
412
|
+
space: targetSpace,
|
|
413
413
|
currentMarker: liveMarker,
|
|
414
|
-
...(
|
|
414
|
+
...(isAppSpace && refName !== undefined ? { refName } : {}),
|
|
415
415
|
});
|
|
416
416
|
|
|
417
417
|
if (walked.kind === 'unreachable') {
|
|
418
|
-
return { kind: 'unreachable', spaceId:
|
|
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:
|
|
425
|
-
|
|
424
|
+
spaceId: space.spaceId,
|
|
425
|
+
isAppSpace,
|
|
426
426
|
missing: walked.missing,
|
|
427
427
|
targetInvariants,
|
|
428
|
-
|
|
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
|
-
*
|
|
439
|
-
* pipeline thread the
|
|
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
|
|
445
|
+
readonly space: AggregateContractSpace;
|
|
446
446
|
readonly targetHash: string;
|
|
447
447
|
readonly liveMarker: ContractMarkerRecordLike | null;
|
|
448
448
|
}): PerSpacePlan {
|
|
449
|
-
const { aggregateTargetId,
|
|
449
|
+
const { aggregateTargetId, space, targetHash, liveMarker } = args;
|
|
450
450
|
return {
|
|
451
451
|
plan: {
|
|
452
452
|
targetId: aggregateTargetId,
|
|
453
|
-
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
package/src/control-api/types.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
576
|
-
|
|
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
|
// ============================================================================
|