@prisma-next/migration-tools 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +4 -4
  2. package/dist/{errors-DGYwcwXs.mjs → errors-vFROOhCR.mjs} +46 -21
  3. package/dist/errors-vFROOhCR.mjs.map +1 -0
  4. package/dist/exports/aggregate.d.mts +328 -204
  5. package/dist/exports/aggregate.d.mts.map +1 -1
  6. package/dist/exports/aggregate.mjs +480 -243
  7. package/dist/exports/aggregate.mjs.map +1 -1
  8. package/dist/exports/errors.d.mts +2 -2
  9. package/dist/exports/errors.d.mts.map +1 -1
  10. package/dist/exports/errors.mjs +1 -1
  11. package/dist/exports/graph.d.mts +1 -1
  12. package/dist/exports/hash.d.mts +8 -9
  13. package/dist/exports/hash.d.mts.map +1 -1
  14. package/dist/exports/hash.mjs +1 -1
  15. package/dist/exports/invariants.d.mts +1 -1
  16. package/dist/exports/invariants.d.mts.map +1 -1
  17. package/dist/exports/invariants.mjs +1 -1
  18. package/dist/exports/io.d.mts +2 -83
  19. package/dist/exports/io.mjs +1 -1
  20. package/dist/exports/metadata.d.mts +2 -2
  21. package/dist/exports/migration-graph.d.mts +9 -2
  22. package/dist/exports/migration-graph.d.mts.map +1 -0
  23. package/dist/exports/migration-graph.mjs +3 -2
  24. package/dist/exports/migration-ts.d.mts.map +1 -1
  25. package/dist/exports/migration-ts.mjs.map +1 -1
  26. package/dist/exports/migration.d.mts +5 -6
  27. package/dist/exports/migration.d.mts.map +1 -1
  28. package/dist/exports/migration.mjs +14 -32
  29. package/dist/exports/migration.mjs.map +1 -1
  30. package/dist/exports/package.d.mts +1 -1
  31. package/dist/exports/ref-resolution.d.mts +2 -2
  32. package/dist/exports/ref-resolution.d.mts.map +1 -1
  33. package/dist/exports/ref-resolution.mjs +1 -1
  34. package/dist/exports/ref-resolution.mjs.map +1 -1
  35. package/dist/exports/refs.d.mts +15 -2
  36. package/dist/exports/refs.d.mts.map +1 -0
  37. package/dist/exports/refs.mjs +3 -2
  38. package/dist/exports/spaces.d.mts +31 -132
  39. package/dist/exports/spaces.d.mts.map +1 -1
  40. package/dist/exports/spaces.mjs +13 -9
  41. package/dist/exports/spaces.mjs.map +1 -1
  42. package/dist/{graph-BrLXqoUc.d.mts → graph-3dLMZp5l.d.mts} +1 -2
  43. package/dist/graph-3dLMZp5l.d.mts.map +1 -0
  44. package/dist/graph-membership-BV23F1IV.mjs +15 -0
  45. package/dist/graph-membership-BV23F1IV.mjs.map +1 -0
  46. package/dist/{hash-Cr4WIr4Z.mjs → hash--Y7vCpN3.mjs} +8 -9
  47. package/dist/hash--Y7vCpN3.mjs.map +1 -0
  48. package/dist/{invariants-0daYEzyo.mjs → invariants-C23nXy1c.mjs} +2 -2
  49. package/dist/{invariants-0daYEzyo.mjs.map → invariants-C23nXy1c.mjs.map} +1 -1
  50. package/dist/{io-BPLfzvZe.mjs → io-BGlPOt9b.mjs} +100 -13
  51. package/dist/io-BGlPOt9b.mjs.map +1 -0
  52. package/dist/io-BH4G3F-i.d.mts +124 -0
  53. package/dist/io-BH4G3F-i.d.mts.map +1 -0
  54. package/dist/metadata-Bp9X04gM.d.mts +2 -0
  55. package/dist/{migration-graph-nlS4TRpn.mjs → migration-graph-BMAqSfv9.mjs} +6 -26
  56. package/dist/migration-graph-BMAqSfv9.mjs.map +1 -0
  57. package/dist/{migration-graph-De0dUZoC.d.mts → migration-graph-CWEM2SLR.d.mts} +6 -6
  58. package/dist/migration-graph-CWEM2SLR.d.mts.map +1 -0
  59. package/dist/op-schema-D5qkXfEf.mjs.map +1 -1
  60. package/dist/{package-DZj8YvD0.d.mts → package-Ca-J_z_0.d.mts} +1 -1
  61. package/dist/package-Ca-J_z_0.d.mts.map +1 -0
  62. package/dist/{read-contract-space-contract-DRueB4Aa.mjs → read-contract-space-contract-TbeXuJXL.mjs} +32 -5
  63. package/dist/read-contract-space-contract-TbeXuJXL.mjs.map +1 -0
  64. package/dist/{refs-BDHo5l_g.mjs → refs-C-_WUrPw.mjs} +97 -4
  65. package/dist/refs-C-_WUrPw.mjs.map +1 -0
  66. package/dist/refs-C7wuYFqZ.d.mts +42 -0
  67. package/dist/refs-C7wuYFqZ.d.mts.map +1 -0
  68. package/dist/snapshot-Bazwo13S.mjs +137 -0
  69. package/dist/snapshot-Bazwo13S.mjs.map +1 -0
  70. package/dist/verify-contract-spaces-BdysZdQk.d.mts +132 -0
  71. package/dist/verify-contract-spaces-BdysZdQk.d.mts.map +1 -0
  72. package/package.json +18 -9
  73. package/src/aggregate/aggregate.ts +266 -0
  74. package/src/aggregate/check-integrity.ts +243 -0
  75. package/src/aggregate/loader.ts +161 -334
  76. package/src/aggregate/planner-types.ts +14 -14
  77. package/src/aggregate/planner.ts +20 -23
  78. package/src/aggregate/project-schema-to-space.ts +3 -8
  79. package/src/aggregate/strategies/graph-walk.ts +15 -10
  80. package/src/aggregate/strategies/synth.ts +4 -4
  81. package/src/aggregate/types.ts +81 -62
  82. package/src/aggregate/verifier.ts +23 -23
  83. package/src/assert-descriptor-self-consistency.ts +6 -0
  84. package/src/compute-extension-space-apply-path.ts +1 -1
  85. package/src/emit-contract-space-artefacts.ts +4 -3
  86. package/src/errors.ts +58 -2
  87. package/src/exports/aggregate.ts +29 -19
  88. package/src/exports/io.ts +2 -0
  89. package/src/exports/metadata.ts +1 -1
  90. package/src/exports/migration-graph.ts +1 -0
  91. package/src/exports/refs.ts +11 -0
  92. package/src/exports/spaces.ts +3 -0
  93. package/src/graph-membership.ts +17 -0
  94. package/src/graph.ts +0 -1
  95. package/src/hash.ts +7 -8
  96. package/src/integrity-violation.ts +114 -0
  97. package/src/io.ts +139 -14
  98. package/src/metadata.ts +1 -1
  99. package/src/migration-base.ts +10 -30
  100. package/src/migration-graph.ts +7 -35
  101. package/src/read-contract-space-head-ref.ts +5 -2
  102. package/src/refs/snapshot.ts +199 -0
  103. package/src/refs.ts +124 -1
  104. package/src/space-layout.ts +30 -0
  105. package/dist/errors-DGYwcwXs.mjs.map +0 -1
  106. package/dist/exports/io.d.mts.map +0 -1
  107. package/dist/graph-BrLXqoUc.d.mts.map +0 -1
  108. package/dist/hash-Cr4WIr4Z.mjs.map +0 -1
  109. package/dist/io-BPLfzvZe.mjs.map +0 -1
  110. package/dist/metadata-BFX0xdz8.d.mts +0 -2
  111. package/dist/migration-graph-De0dUZoC.d.mts.map +0 -1
  112. package/dist/migration-graph-nlS4TRpn.mjs.map +0 -1
  113. package/dist/package-DZj8YvD0.d.mts.map +0 -1
  114. package/dist/read-contract-space-contract-DRueB4Aa.mjs.map +0 -1
  115. package/dist/refs-BDHo5l_g.mjs.map +0 -1
  116. package/dist/refs-CDaNerhT.d.mts +0 -16
  117. package/dist/refs-CDaNerhT.d.mts.map +0 -1
  118. package/src/aggregate/extract-storage-element-names.ts +0 -75
@@ -1,10 +1,6 @@
1
1
  import { notOk, ok } from '@prisma-next/utils/result';
2
- import type {
3
- AggregatePerSpacePlan,
4
- AggregatePlannerError,
5
- AggregatePlannerInput,
6
- AggregatePlannerOutput,
7
- } from './planner-types';
2
+ import { requireHeadRef } from './aggregate';
3
+ import type { PerSpacePlan, PlannerError, PlannerInput, PlannerOutput } from './planner-types';
8
4
  import { graphWalkStrategy } from './strategies/graph-walk';
9
5
  import { synthStrategy } from './strategies/synth';
10
6
  import type { ContractSpaceMember } from './types';
@@ -12,12 +8,12 @@ import type { ContractSpaceMember } from './types';
12
8
  export type {
13
9
  AggregateCurrentDBState,
14
10
  AggregateMigrationEdgeRef,
15
- AggregatePerSpacePlan,
16
- AggregatePlannerError,
17
- AggregatePlannerInput,
18
- AggregatePlannerOutput,
19
- AggregatePlannerSuccess,
20
11
  CallerPolicy,
12
+ PerSpacePlan,
13
+ PlannerError,
14
+ PlannerInput,
15
+ PlannerOutput,
16
+ PlannerSuccess,
21
17
  } from './planner-types';
22
18
 
23
19
  /**
@@ -28,7 +24,7 @@ export type {
28
24
  * 1. If `callerPolicy.ignoreGraphFor.has(member.spaceId)`:
29
25
  * - If `member.headRef.invariants` is empty → synth.
30
26
  * - Else → `policyConflict` (synth cannot satisfy authored invariants).
31
- * 2. Else if `member.migrations.graph` is non-empty AND graph-walk
27
+ * 2. Else if `member.graph()` is non-empty AND graph-walk
32
28
  * succeeds → graph-walk.
33
29
  * 3. Else if `member.headRef.invariants` is empty → synth.
34
30
  * 4. Else → graph-walk failure → `extensionPathUnreachable` /
@@ -37,18 +33,18 @@ export type {
37
33
  * Output `applyOrder` is `[...aggregate.extensions.map(spaceId), aggregate.app.spaceId]`
38
34
  * — extensions alphabetical, then app — matching today's
39
35
  * `concatenateSpaceApplyInputs` ordering. This preserves
40
- * `MultiSpaceRunnerFailure.failingSpace` attribution byte-for-byte.
36
+ * `MigrationRunnerFailure.failingSpace` attribution byte-for-byte.
41
37
  *
42
38
  * Every emitted `MigrationPlan` has `targetId = aggregate.targetId`.
43
39
  * No placeholder cast; no patch step.
44
40
  */
45
- export async function planAggregate<TFamilyId extends string, TTargetId extends string>(
46
- input: AggregatePlannerInput<TFamilyId, TTargetId>,
47
- ): Promise<AggregatePlannerOutput> {
41
+ export async function planMigration<TFamilyId extends string, TTargetId extends string>(
42
+ input: PlannerInput<TFamilyId, TTargetId>,
43
+ ): Promise<PlannerOutput> {
48
44
  const { aggregate, currentDBState, callerPolicy } = input;
49
45
  const allMembers: ReadonlyArray<ContractSpaceMember> = [aggregate.app, ...aggregate.extensions];
50
46
 
51
- const perSpace = new Map<string, AggregatePerSpacePlan>();
47
+ const perSpace = new Map<string, PerSpacePlan>();
52
48
 
53
49
  // Iterate in apply order so a per-member error short-circuits the
54
50
  // walk in the same order the runner would walk inputs.
@@ -60,15 +56,16 @@ export async function planAggregate<TFamilyId extends string, TTargetId extends
60
56
  for (const member of orderedMembers) {
61
57
  const otherMembers = allMembers.filter((m) => m.spaceId !== member.spaceId);
62
58
  const currentMarker = currentDBState.markersBySpaceId.get(member.spaceId) ?? null;
59
+ const headRef = requireHeadRef(member);
63
60
 
64
61
  const ignoreGraph = callerPolicy.ignoreGraphFor.has(member.spaceId);
65
- const invariantsRequired = member.headRef.invariants.length > 0;
62
+ const invariantsRequired = headRef.invariants.length > 0;
66
63
 
67
64
  if (ignoreGraph && invariantsRequired) {
68
- const conflict: AggregatePlannerError = {
65
+ const conflict: PlannerError = {
69
66
  kind: 'policyConflict',
70
67
  spaceId: member.spaceId,
71
- detail: `\`callerPolicy.ignoreGraphFor\` requested for space "${member.spaceId}", but the member declares non-empty head-ref invariants (${member.headRef.invariants.join(', ')}). Synthesising a plan from the contract IR cannot satisfy authored invariants — the graph must be walked. Either remove "${member.spaceId}" from \`ignoreGraphFor\` or amend the on-disk head ref to declare zero invariants.`,
68
+ detail: `\`callerPolicy.ignoreGraphFor\` requested for space "${member.spaceId}", but the member declares non-empty head-ref invariants (${headRef.invariants.join(', ')}). Synthesising a plan from the contract IR cannot satisfy authored invariants — the graph must be walked. Either remove "${member.spaceId}" from \`ignoreGraphFor\` or amend the on-disk head ref to declare zero invariants.`,
72
69
  };
73
70
  return notOk(conflict);
74
71
  }
@@ -97,7 +94,7 @@ export async function planAggregate<TFamilyId extends string, TTargetId extends
97
94
 
98
95
  // Try graph-walk first when the graph has nodes; fall back to synth
99
96
  // when the graph is empty AND no invariants are required.
100
- if (member.migrations.graph.nodes.size > 0) {
97
+ if (member.graph().nodes.size > 0) {
101
98
  const walked = graphWalkStrategy({
102
99
  aggregateTargetId: aggregate.targetId,
103
100
  member,
@@ -111,7 +108,7 @@ export async function planAggregate<TFamilyId extends string, TTargetId extends
111
108
  return notOk({
112
109
  kind: 'extensionPathUnreachable',
113
110
  spaceId: member.spaceId,
114
- target: member.headRef.hash,
111
+ target: headRef.hash,
115
112
  });
116
113
  }
117
114
  // unsatisfiable — surface
@@ -128,7 +125,7 @@ export async function planAggregate<TFamilyId extends string, TTargetId extends
128
125
  return notOk({
129
126
  kind: 'extensionPathUnsatisfiable',
130
127
  spaceId: member.spaceId,
131
- missingInvariants: [...member.headRef.invariants].sort(),
128
+ missingInvariants: [...headRef.invariants].sort(),
132
129
  });
133
130
  }
134
131
 
@@ -1,4 +1,4 @@
1
- import { extractStorageElementNames } from './extract-storage-element-names';
1
+ import { elementCoordinates } from '@prisma-next/framework-components/ir';
2
2
  import type { ContractSpaceMember } from './types';
3
3
 
4
4
  /**
@@ -90,11 +90,6 @@ export function projectSchemaToSpace(
90
90
  return schema;
91
91
  }
92
92
 
93
- /**
94
- * Collect the set of storage element names claimed by other members.
95
- * Reuses the loader's `extractStorageElementNames` helper so the
96
- * tables/collections walk lives in exactly one place.
97
- */
98
93
  function collectOwnedNames(
99
94
  member: ContractSpaceMember,
100
95
  otherMembers: ReadonlyArray<ContractSpaceMember>,
@@ -102,8 +97,8 @@ function collectOwnedNames(
102
97
  const owned = new Set<string>();
103
98
  for (const other of otherMembers) {
104
99
  if (other.spaceId === member.spaceId) continue;
105
- for (const name of extractStorageElementNames(other.contract)) {
106
- owned.add(name);
100
+ for (const { entityName } of elementCoordinates(other.contract().storage)) {
101
+ owned.add(entityName);
107
102
  }
108
103
  }
109
104
  return owned;
@@ -1,21 +1,22 @@
1
1
  import type { MigrationPlan } from '@prisma-next/framework-components/control';
2
2
  import { EMPTY_CONTRACT_HASH } from '../../constants';
3
3
  import { findPathWithDecision } from '../../migration-graph';
4
- import type { MigrationOps } from '../../package';
4
+ import type { MigrationOps, OnDiskMigrationPackage } from '../../package';
5
+ import { requireHeadRef } from '../aggregate';
5
6
  import type { ContractMarkerRecordLike } from '../marker-types';
6
- import type { AggregatePerSpacePlan } from '../planner-types';
7
+ import type { PerSpacePlan } from '../planner-types';
7
8
  import type { ContractSpaceMember } from '../types';
8
9
 
9
10
  /**
10
11
  * Outcome variants for the graph-walk strategy. Mirrors
11
12
  * {@link import('../../compute-extension-space-apply-path').ExtensionSpaceApplyPathOutcome}
12
- * but operates against the **already-hydrated** `member.migrations.graph`
13
+ * but operates against the member's lazily-reconstructed `graph()`
13
14
  * instead of re-reading from disk. The aggregate planner converts
14
- * these into {@link import('../planner-types').AggregatePlannerError}
15
+ * these into {@link import('../planner-types').PlannerError}
15
16
  * variants.
16
17
  */
17
18
  export type GraphWalkOutcome =
18
- | { readonly kind: 'ok'; readonly result: AggregatePerSpacePlan }
19
+ | { readonly kind: 'ok'; readonly result: PerSpacePlan }
19
20
  | { readonly kind: 'unreachable' }
20
21
  | { readonly kind: 'unsatisfiable'; readonly missing: readonly string[] };
21
22
 
@@ -49,13 +50,17 @@ export interface GraphWalkStrategyInputs {
49
50
  */
50
51
  export function graphWalkStrategy(input: GraphWalkStrategyInputs): GraphWalkOutcome {
51
52
  const { aggregateTargetId, member, currentMarker, refName } = input;
52
- const { graph, packagesByMigrationHash } = member.migrations;
53
+ const headRef = requireHeadRef(member);
54
+ const graph = member.graph();
55
+ const packagesByMigrationHash = new Map<string, OnDiskMigrationPackage>(
56
+ member.packages.map((pkg) => [pkg.metadata.migrationHash, pkg]),
57
+ );
53
58
 
54
59
  const fromHash = currentMarker?.storageHash ?? EMPTY_CONTRACT_HASH;
55
60
  const markerInvariants = new Set(currentMarker?.invariants ?? []);
56
- const required = new Set(member.headRef.invariants.filter((id) => !markerInvariants.has(id)));
61
+ const required = new Set(headRef.invariants.filter((id) => !markerInvariants.has(id)));
57
62
 
58
- const outcome = findPathWithDecision(graph, fromHash, member.headRef.hash, {
63
+ const outcome = findPathWithDecision(graph, fromHash, headRef.hash, {
59
64
  required,
60
65
  ...(refName !== undefined ? { refName } : {}),
61
66
  });
@@ -98,7 +103,7 @@ export function graphWalkStrategy(input: GraphWalkStrategyInputs): GraphWalkOutc
98
103
  targetId: aggregateTargetId,
99
104
  spaceId: member.spaceId,
100
105
  origin: currentMarker === null ? null : { storageHash: currentMarker.storageHash },
101
- destination: { storageHash: member.headRef.hash },
106
+ destination: { storageHash: headRef.hash },
102
107
  operations: pathOps,
103
108
  providedInvariants: [...providedInvariantsSet].sort(),
104
109
  };
@@ -108,7 +113,7 @@ export function graphWalkStrategy(input: GraphWalkStrategyInputs): GraphWalkOutc
108
113
  result: {
109
114
  plan,
110
115
  displayOps: pathOps,
111
- destinationContract: member.contract,
116
+ destinationContract: member.contract(),
112
117
  strategy: 'graph-walk',
113
118
  migrationEdges: edgeRefs,
114
119
  pathDecision: outcome.decision,
@@ -7,7 +7,7 @@ import type {
7
7
  MigrationPlannerResult,
8
8
  TargetMigrationsCapability,
9
9
  } from '@prisma-next/framework-components/control';
10
- import type { AggregatePerSpacePlan } from '../planner-types';
10
+ import type { PerSpacePlan } from '../planner-types';
11
11
  import { projectSchemaToSpace } from '../project-schema-to-space';
12
12
  import type { ContractSpaceMember } from '../types';
13
13
 
@@ -27,7 +27,7 @@ export interface SynthStrategyInputs<TFamilyId extends string, TTargetId extends
27
27
  }
28
28
 
29
29
  export type SynthStrategyOutcome =
30
- | { readonly kind: 'ok'; readonly result: AggregatePerSpacePlan }
30
+ | { readonly kind: 'ok'; readonly result: PerSpacePlan }
31
31
  | { readonly kind: 'failure'; readonly conflicts: readonly MigrationPlannerConflict[] };
32
32
 
33
33
  /**
@@ -73,7 +73,7 @@ export async function synthStrategy<TFamilyId extends string, TTargetId extends
73
73
 
74
74
  const planner = input.migrations.createPlanner(input.familyInstance);
75
75
  const plannerResult: MigrationPlannerResult = await (planner.plan({
76
- contract: input.member.contract,
76
+ contract: input.member.contract(),
77
77
  schema: projectedSchema,
78
78
  policy: input.operationPolicy,
79
79
  fromContract: null,
@@ -114,7 +114,7 @@ export async function synthStrategy<TFamilyId extends string, TTargetId extends
114
114
  result: {
115
115
  plan,
116
116
  displayOps: synthedPlan.operations,
117
- destinationContract: input.member.contract,
117
+ destinationContract: input.member.contract(),
118
118
  strategy: 'synth',
119
119
  },
120
120
  };
@@ -1,89 +1,108 @@
1
1
  import type { Contract } from '@prisma-next/contract/types';
2
2
  import type { MigrationGraph } from '../graph';
3
+ import type { IntegrityQueryOptions, IntegrityViolation } from '../integrity-violation';
3
4
  import type { OnDiskMigrationPackage } from '../package';
5
+ import type { Refs } from '../refs';
6
+ import type { ContractSpaceHeadRecord } from '../verify-contract-spaces';
4
7
 
5
- /**
6
- * Hydrated migration graph for a single contract space.
7
- *
8
- * `graph` is the structural shortest-path graph (forward / reverse chain,
9
- * deterministic tie-break order) reconstructed from a set of on-disk
10
- * migration packages. `packagesByMigrationHash` is the lookup table the
11
- * graph-walk strategy uses to resolve a path's edge sequence back to the
12
- * concrete `OnDiskMigrationPackage` (and therefore the operation list) for
13
- * apply.
14
- *
15
- * Eagerly hydrated by the loader. Once a `ContractSpaceAggregate` exists,
16
- * downstream consumers do **not** touch the filesystem to walk graphs or
17
- * resolve packages — the aggregate is the boundary.
18
- */
19
- export interface HydratedMigrationGraph {
20
- readonly graph: MigrationGraph;
21
- readonly packagesByMigrationHash: ReadonlyMap<string, OnDiskMigrationPackage>;
8
+ export interface ContractAtOptions {
9
+ readonly refName?: string;
22
10
  }
23
11
 
12
+ export type ContractAtResult =
13
+ | {
14
+ readonly provenance: 'snapshot';
15
+ readonly hash: string;
16
+ readonly contractJson: unknown;
17
+ readonly contractDts: string;
18
+ readonly contract: Contract;
19
+ }
20
+ | {
21
+ readonly provenance: 'graph-node';
22
+ readonly sourceDir: string;
23
+ readonly hash: string;
24
+ readonly contractJson: unknown;
25
+ readonly contractDts: string;
26
+ readonly contract: Contract;
27
+ };
28
+
24
29
  /**
25
30
  * One contract space — app or extension — as a member of a
26
31
  * {@link ContractSpaceAggregate}. Every member has the same shape.
27
32
  *
33
+ * A member is a tolerant snapshot of one space's on-disk state, not a
34
+ * validated value: `packages` is the raw migration-package list as read
35
+ * from disk (a hash- or invariants-mismatched package is retained here;
36
+ * a genuinely unparseable one is omitted), and integrity is judged
37
+ * separately by {@link ContractSpaceAggregate.checkIntegrity}.
38
+ *
28
39
  * - `spaceId`: `'app'` for the application, otherwise the extension's
29
40
  * id (validated against `[a-z][a-z0-9_-]{0,63}`).
30
- * - `contract`: the validated contract value for this member. For the
31
- * app, the user's authored contract; for an extension, the on-disk
32
- * `migrations/<spaceId>/contract.json`. Both have already passed the
33
- * family's `deserializeContract` at the loader boundary.
34
- * - `headRef.hash`: the storage hash this member is targeting. For the
35
- * app, equals `contract.storage.storageHash`. For extensions, the
36
- * on-disk `refs/head.json.hash`.
37
- * - `headRef.invariants`: alphabetically sorted, deduplicated invariant
38
- * ids declared on the head ref. Empty for the app member (the app's
39
- * plan is synthesised from the contract IR, no invariants required).
40
- * - `migrations`: the hydrated migration graph for this space. Possibly
41
- * empty (an extension whose on-disk head ref points at the
42
- * empty-contract sentinel and ships no migrations yet, or the app
43
- * when the user hasn't authored any).
41
+ * - `packages`: raw on-disk migration packages, as read; never
42
+ * integrity-validated at load.
43
+ * - `refs`: the user-authored refs under `migrations/<spaceId>/refs/*.json`.
44
+ * - `headRef`: the system head ref read from
45
+ * `migrations/<spaceId>/refs/head.json`, or `null` when absent
46
+ * (represented as a `headRefMissing` violation, never fatal). The app
47
+ * member's head ref is always synthesised from its live contract's
48
+ * storage hash, so it is never `null`.
49
+ * - `graph()`: the migration graph this space's packages induce
50
+ * lazily reconstructed on first call and memoised. Pure structure: a
51
+ * `from === to` self-edge is represented, not rejected.
52
+ * - `contract()`: the deserialized contract for this member lazily
53
+ * produced on first call and memoised. For the app it is the live
54
+ * contract the caller supplied; for an extension it is the on-disk
55
+ * `migrations/<spaceId>/contract.json` run through the family's
56
+ * `deserializeContract`. Throws if the on-disk contract is missing or
57
+ * undeserializable (surfaced as `contractUnreadable` by `checkIntegrity`
58
+ * under `checkContracts`); callers gate before querying it.
59
+ * - `contractAt(hash, opts?)`: materializes the contract at an arbitrary
60
+ * graph node — when `opts.refName` is set, prefer the ref's paired
61
+ * snapshot; else find the package whose `metadata.to === hash` and read
62
+ * its `end-contract.*`. Lazy per `(hash, refName?)` memoisation; throws
63
+ * typed {@link MigrationToolsError} values compatible with CLI mappers.
44
64
  */
45
65
  export interface ContractSpaceMember {
46
66
  readonly spaceId: string;
47
- readonly contract: Contract;
48
- readonly headRef: {
49
- readonly hash: string;
50
- readonly invariants: readonly string[];
51
- };
52
- readonly migrations: HydratedMigrationGraph;
67
+ readonly packages: readonly OnDiskMigrationPackage[];
68
+ readonly refs: Refs;
69
+ readonly headRef: ContractSpaceHeadRecord | null;
70
+ graph(): MigrationGraph;
71
+ contract(): Contract;
72
+ contractAt(hash: string, opts?: ContractAtOptions): Promise<ContractAtResult>;
53
73
  }
54
74
 
55
75
  /**
56
- * Typed value carrying the user's app contract plus every loaded
57
- * extension contract space, fully hydrated and internally consistent.
76
+ * Tolerant, queryable snapshot of a project's on-disk migration state:
77
+ * the app contract space plus every extension contract space, each a
78
+ * {@link ContractSpaceMember}.
58
79
  *
59
80
  * Produced once per CLI invocation by `loadContractSpaceAggregate`.
60
- * Every downstream component (planner, verifier, runner adapter)
61
- * consumes this value rather than rebuilding state from disk.
62
- *
63
- * Invariants the loader enforces at construction:
64
- *
65
- * 1. `targetId` is consistent across every member (`contract.target`
66
- * matches `aggregate.targetId`). The aggregate's `targetId` is the
67
- * `Config.adapter.targetId` value the loader was told to use.
68
- * 2. `aggregate.extensions` is sorted alphabetically by `spaceId`.
69
- * Mirrors {@link import('../concatenate-space-apply-inputs').concatenateSpaceApplyInputs}'s
70
- * extension ordering convention so downstream apply order matches
71
- * today's behaviour byte-for-byte.
72
- * 3. No two members claim the same storage element (table / type / etc.).
73
- * 4. For each extension member: `member.headRef.hash` is reachable from
74
- * the empty-contract sentinel in `member.migrations.graph` (or the
75
- * graph is empty and `member.headRef.hash === EMPTY_CONTRACT_HASH`).
76
- * 5. For the app member: `member.headRef.hash` equals
77
- * `member.contract.storage.storageHash`. The app's `migrations`
78
- * is hydrated from the user's authored `migrations/` (or empty if
79
- * none).
81
+ * Building the aggregate never throws on disk content; every consumer
82
+ * obtains spaces / packages / refs / graphs from this one value rather
83
+ * than re-deriving them from disk.
80
84
  *
81
- * The aggregate is **type-uniform** post-construction: app/extension
82
- * distinguishability survives only at the caller-policy layer
83
- * (`ignoreGraphFor: new Set([appSpaceId])`), not on member shape.
85
+ * - `targetId`: the app contract's target; every member is expected to
86
+ * share it (a mismatch surfaces as a `targetMismatch` violation under
87
+ * `checkContracts`).
88
+ * - `app` / `extensions`: retained as fields for the existing planner /
89
+ * verifier / runner consumers. `extensions` is sorted alphabetically
90
+ * by `spaceId` (the apply-ordering convention).
91
+ * - `listSpaces()` / `hasSpace()` / `space()` / `spaces()`: the query
92
+ * surface the read commands consume — `app` first, then extension ids
93
+ * lex-ascending.
94
+ * - `checkIntegrity()`: judges the loaded model and returns every
95
+ * violation (never bailing at the first). Config/contract-dependent
96
+ * checks run only when the matching {@link IntegrityQueryOptions} opt
97
+ * is set.
84
98
  */
85
99
  export interface ContractSpaceAggregate {
86
100
  readonly targetId: string;
87
101
  readonly app: ContractSpaceMember;
88
102
  readonly extensions: readonly ContractSpaceMember[];
103
+ listSpaces(): readonly string[];
104
+ hasSpace(id: string): boolean;
105
+ space(id: string): ContractSpaceMember | undefined;
106
+ spaces(): readonly ContractSpaceMember[];
107
+ checkIntegrity(opts?: IntegrityQueryOptions): readonly IntegrityViolation[];
89
108
  }
@@ -1,17 +1,18 @@
1
+ import { elementCoordinates } from '@prisma-next/framework-components/ir';
1
2
  import type { Result } from '@prisma-next/utils/result';
2
3
  import { notOk, ok } from '@prisma-next/utils/result';
3
- import { extractStorageElementNames } from './extract-storage-element-names';
4
+ import { requireHeadRef } from './aggregate';
4
5
  import type { ContractMarkerRecordLike } from './marker-types';
5
6
  import { projectSchemaToSpace } from './project-schema-to-space';
6
7
  import type { ContractSpaceAggregate, ContractSpaceMember } from './types';
7
8
 
8
9
  /**
9
- * Caller policy for the aggregate verifier. Today's only knob is
10
+ * Caller policy for the verifier. Today's only knob is
10
11
  * `mode`: `strict` treats orphan elements (live tables not claimed by
11
12
  * any aggregate member) as errors; `lenient` treats them as
12
13
  * informational. Maps directly to `db verify --strict`.
13
14
  */
14
- export interface AggregateVerifierInput<TSchemaResult> {
15
+ export interface VerifierInput<TSchemaResult> {
15
16
  readonly aggregate: ContractSpaceAggregate;
16
17
  readonly markersBySpaceId: ReadonlyMap<string, ContractMarkerRecordLike | null>;
17
18
  readonly schemaIntrospection: unknown;
@@ -19,7 +20,7 @@ export interface AggregateVerifierInput<TSchemaResult> {
19
20
  /**
20
21
  * Caller-supplied per-space schema verifier. The CLI wires this to
21
22
  * the family's `verifySqlSchema` (SQL) / equivalent (other
22
- * families). The aggregate verifier projects the schema to the
23
+ * families). The verifier projects the schema to the
23
24
  * member's slice via {@link projectSchemaToSpace} before invoking
24
25
  * the callback, so single-contract semantics are preserved.
25
26
  *
@@ -61,7 +62,7 @@ export interface MarkerCheckSection {
61
62
 
62
63
  /**
63
64
  * A live storage element (today: a top-level table) not claimed by any
64
- * member of the aggregate. The aggregate verifier always reports these;
65
+ * member of the aggregate. The verifier always reports these;
65
66
  * the caller decides what to do — `db verify --strict` treats them as
66
67
  * errors, the lenient default treats them as informational.
67
68
  *
@@ -80,20 +81,17 @@ export interface SchemaCheckSection<TSchemaResult> {
80
81
  readonly orphanElements: readonly OrphanElement[];
81
82
  }
82
83
 
83
- export interface AggregateVerifierSuccess<TSchemaResult> {
84
+ export interface VerifierSuccess<TSchemaResult> {
84
85
  readonly markerCheck: MarkerCheckSection;
85
86
  readonly schemaCheck: SchemaCheckSection<TSchemaResult>;
86
87
  }
87
88
 
88
- export type AggregateVerifierError = {
89
+ export type VerifierError = {
89
90
  readonly kind: 'introspectionFailure';
90
91
  readonly detail: string;
91
92
  };
92
93
 
93
- export type AggregateVerifierOutput<TSchemaResult> = Result<
94
- AggregateVerifierSuccess<TSchemaResult>,
95
- AggregateVerifierError
96
- >;
94
+ export type VerifierOutput<TSchemaResult> = Result<VerifierSuccess<TSchemaResult>, VerifierError>;
97
95
 
98
96
  /**
99
97
  * Verify a {@link ContractSpaceAggregate} against the live database
@@ -117,11 +115,11 @@ export type AggregateVerifierOutput<TSchemaResult> = Result<
117
115
  * Pure synchronous function; no I/O. The caller (CLI) gathers
118
116
  * `markersBySpaceId` and `schemaIntrospection` ahead of the call.
119
117
  */
120
- export function verifyAggregate<TSchemaResult>(
121
- input: AggregateVerifierInput<TSchemaResult>,
122
- ): AggregateVerifierOutput<TSchemaResult> {
118
+ export function verifyMigration<TSchemaResult>(
119
+ input: VerifierInput<TSchemaResult>,
120
+ ): VerifierOutput<TSchemaResult> {
123
121
  try {
124
- return runVerifyAggregate(input);
122
+ return runVerifyMigration(input);
125
123
  } catch (error) {
126
124
  return notOk({
127
125
  kind: 'introspectionFailure',
@@ -130,9 +128,9 @@ export function verifyAggregate<TSchemaResult>(
130
128
  }
131
129
  }
132
130
 
133
- function runVerifyAggregate<TSchemaResult>(
134
- input: AggregateVerifierInput<TSchemaResult>,
135
- ): AggregateVerifierOutput<TSchemaResult> {
131
+ function runVerifyMigration<TSchemaResult>(
132
+ input: VerifierInput<TSchemaResult>,
133
+ ): VerifierOutput<TSchemaResult> {
136
134
  const { aggregate, markersBySpaceId, schemaIntrospection, mode, verifySchemaForMember } = input;
137
135
  const allMembers: ReadonlyArray<ContractSpaceMember> = [aggregate.app, ...aggregate.extensions];
138
136
  const memberSpaceIds = new Set(allMembers.map((m) => m.spaceId));
@@ -145,16 +143,17 @@ function runVerifyAggregate<TSchemaResult>(
145
143
  markerPerSpace.set(member.spaceId, { kind: 'absent' });
146
144
  continue;
147
145
  }
148
- if (marker.storageHash !== member.headRef.hash) {
146
+ const headRef = requireHeadRef(member);
147
+ if (marker.storageHash !== headRef.hash) {
149
148
  markerPerSpace.set(member.spaceId, {
150
149
  kind: 'hashMismatch',
151
150
  markerHash: marker.storageHash,
152
- expected: member.headRef.hash,
151
+ expected: headRef.hash,
153
152
  });
154
153
  continue;
155
154
  }
156
155
  const markerInvariants = new Set(marker.invariants);
157
- const missing = member.headRef.invariants.filter((id) => !markerInvariants.has(id));
156
+ const missing = headRef.invariants.filter((id) => !markerInvariants.has(id));
158
157
  if (missing.length > 0) {
159
158
  markerPerSpace.set(member.spaceId, {
160
159
  kind: 'missingInvariants',
@@ -211,8 +210,9 @@ function detectOrphanElements(
211
210
 
212
211
  const claimedTables = new Set<string>();
213
212
  for (const member of members) {
214
- for (const name of extractStorageElementNames(member.contract)) {
215
- claimedTables.add(name);
213
+ const contract = member.contract();
214
+ for (const { entityName } of elementCoordinates(contract.storage)) {
215
+ claimedTables.add(entityName);
216
216
  }
217
217
  }
218
218
 
@@ -1,4 +1,6 @@
1
+ import type { PreserveEmptyPredicate, StorageSort } from '@prisma-next/contract/hashing';
1
2
  import { computeStorageHash } from '@prisma-next/contract/hashing';
3
+ import { ifDefined } from '@prisma-next/utils/defined';
2
4
  import { errorDescriptorHeadHashMismatch } from './errors';
3
5
 
4
6
  function stripNamespaceKinds(storage: Record<string, unknown>): Record<string, unknown> {
@@ -35,6 +37,8 @@ export interface DescriptorSelfConsistencyInputs {
35
37
  */
36
38
  readonly storage: unknown;
37
39
  readonly headRefHash: string;
40
+ readonly shouldPreserveEmpty?: PreserveEmptyPredicate;
41
+ readonly sortStorage?: StorageSort;
38
42
  }
39
43
 
40
44
  /**
@@ -82,6 +86,8 @@ export function assertDescriptorSelfConsistency(inputs: DescriptorSelfConsistenc
82
86
  target: inputs.target,
83
87
  targetFamily: inputs.targetFamily,
84
88
  storage: normalizedStorage,
89
+ ...ifDefined('shouldPreserveEmpty', inputs.shouldPreserveEmpty),
90
+ ...ifDefined('sortStorage', inputs.sortStorage),
85
91
  });
86
92
  if (recomputed !== inputs.headRefHash) {
87
93
  throw errorDescriptorHeadHashMismatch({
@@ -97,7 +97,7 @@ export async function computeExtensionSpaceApplyPath(
97
97
  }
98
98
 
99
99
  const spaceDir = spaceMigrationDirectory(projectMigrationsDir, spaceId);
100
- const packages = await readMigrationsDir(spaceDir);
100
+ const { packages } = await readMigrationsDir(spaceDir);
101
101
  const graph = reconstructGraph(packages);
102
102
 
103
103
  // Live-marker layer encodes "no prior state" as EMPTY_CONTRACT_HASH;
@@ -2,7 +2,7 @@ import { mkdir, writeFile } from 'node:fs/promises';
2
2
  import { canonicalizeJson } from '@prisma-next/framework-components/utils';
3
3
  import { join } from 'pathe';
4
4
  import type { ContractSpaceHeadRef } from './read-contract-space-head-ref';
5
- import { assertValidSpaceId } from './space-layout';
5
+ import { assertValidSpaceId, spaceRefsDirectory } from './space-layout';
6
6
 
7
7
  /**
8
8
  * Inputs for {@link emitContractSpaceArtefacts}.
@@ -56,7 +56,8 @@ export async function emitContractSpaceArtefacts(
56
56
  assertValidSpaceId(spaceId);
57
57
 
58
58
  const dir = join(projectMigrationsDir, spaceId);
59
- await mkdir(join(dir, 'refs'), { recursive: true });
59
+ const refsDir = spaceRefsDirectory(dir);
60
+ await mkdir(refsDir, { recursive: true });
60
61
 
61
62
  await writeFile(join(dir, 'contract.json'), `${canonicalizeJson(inputs.contract)}\n`);
62
63
  await writeFile(join(dir, 'contract.d.ts'), inputs.contractDts);
@@ -66,5 +67,5 @@ export async function emitContractSpaceArtefacts(
66
67
  hash: inputs.headRef.hash,
67
68
  invariants: sortedInvariants,
68
69
  });
69
- await writeFile(join(dir, 'refs', 'head.json'), `${headJson}\n`);
70
+ await writeFile(join(refsDir, 'head.json'), `${headJson}\n`);
70
71
  }