@prisma-next/migration-tools 0.11.0-dev.5 → 0.11.0-dev.51

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 (133) hide show
  1. package/README.md +4 -4
  2. package/dist/{errors-DGYwcwXs.mjs → errors-4YabujxZ.mjs} +15 -21
  3. package/dist/errors-4YabujxZ.mjs.map +1 -0
  4. package/dist/exports/aggregate.d.mts +275 -179
  5. package/dist/exports/aggregate.d.mts.map +1 -1
  6. package/dist/exports/aggregate.mjs +363 -184
  7. package/dist/exports/aggregate.mjs.map +1 -1
  8. package/dist/exports/enumerate-migration-spaces.d.mts +53 -0
  9. package/dist/exports/enumerate-migration-spaces.d.mts.map +1 -0
  10. package/dist/exports/enumerate-migration-spaces.mjs +107 -0
  11. package/dist/exports/enumerate-migration-spaces.mjs.map +1 -0
  12. package/dist/exports/errors.d.mts +2 -2
  13. package/dist/exports/errors.d.mts.map +1 -1
  14. package/dist/exports/errors.mjs +1 -1
  15. package/dist/exports/graph.d.mts +1 -1
  16. package/dist/exports/hash.d.mts +8 -9
  17. package/dist/exports/hash.d.mts.map +1 -1
  18. package/dist/exports/hash.mjs +1 -1
  19. package/dist/exports/invariants.d.mts +1 -1
  20. package/dist/exports/invariants.d.mts.map +1 -1
  21. package/dist/exports/invariants.mjs +1 -1
  22. package/dist/exports/io.d.mts +2 -83
  23. package/dist/exports/io.mjs +1 -1
  24. package/dist/exports/metadata.d.mts +2 -2
  25. package/dist/exports/migration-graph.d.mts +9 -2
  26. package/dist/exports/migration-graph.d.mts.map +1 -0
  27. package/dist/exports/migration-graph.mjs +16 -2
  28. package/dist/exports/migration-graph.mjs.map +1 -0
  29. package/dist/exports/migration-list-graph-topology.d.mts +13 -0
  30. package/dist/exports/migration-list-graph-topology.d.mts.map +1 -0
  31. package/dist/exports/migration-list-graph-topology.mjs +105 -0
  32. package/dist/exports/migration-list-graph-topology.mjs.map +1 -0
  33. package/dist/exports/migration-list-types.d.mts +2 -0
  34. package/dist/exports/migration-list-types.mjs +1 -0
  35. package/dist/exports/migration-ts.d.mts.map +1 -1
  36. package/dist/exports/migration-ts.mjs.map +1 -1
  37. package/dist/exports/migration.d.mts +5 -6
  38. package/dist/exports/migration.d.mts.map +1 -1
  39. package/dist/exports/migration.mjs +14 -32
  40. package/dist/exports/migration.mjs.map +1 -1
  41. package/dist/exports/package.d.mts +1 -1
  42. package/dist/exports/ref-resolution.d.mts +2 -2
  43. package/dist/exports/ref-resolution.d.mts.map +1 -1
  44. package/dist/exports/ref-resolution.mjs +1 -1
  45. package/dist/exports/ref-resolution.mjs.map +1 -1
  46. package/dist/exports/refs.d.mts +15 -2
  47. package/dist/exports/refs.d.mts.map +1 -0
  48. package/dist/exports/refs.mjs +137 -2
  49. package/dist/exports/refs.mjs.map +1 -0
  50. package/dist/exports/spaces.d.mts +31 -132
  51. package/dist/exports/spaces.d.mts.map +1 -1
  52. package/dist/exports/spaces.mjs +14 -9
  53. package/dist/exports/spaces.mjs.map +1 -1
  54. package/dist/{graph-BrLXqoUc.d.mts → graph-BUZuUeBC.d.mts} +1 -2
  55. package/dist/graph-BUZuUeBC.d.mts.map +1 -0
  56. package/dist/{hash-Cr4WIr4Z.mjs → hash--Y7vCpN3.mjs} +8 -9
  57. package/dist/hash--Y7vCpN3.mjs.map +1 -0
  58. package/dist/{invariants-0daYEzyo.mjs → invariants-CCOAyg6c.mjs} +2 -2
  59. package/dist/{invariants-0daYEzyo.mjs.map → invariants-CCOAyg6c.mjs.map} +1 -1
  60. package/dist/{io-BPLfzvZe.mjs → io-BHl0amF0.mjs} +100 -13
  61. package/dist/io-BHl0amF0.mjs.map +1 -0
  62. package/dist/io-nqFXSSTN.d.mts +124 -0
  63. package/dist/io-nqFXSSTN.d.mts.map +1 -0
  64. package/dist/metadata-Bp9X04gM.d.mts +2 -0
  65. package/dist/{migration-graph-De0dUZoC.d.mts → migration-graph-DtNT-cqc.d.mts} +6 -6
  66. package/dist/migration-graph-DtNT-cqc.d.mts.map +1 -0
  67. package/dist/{migration-graph-nlS4TRpn.mjs → migration-graph-kGBkIZDa.mjs} +6 -26
  68. package/dist/migration-graph-kGBkIZDa.mjs.map +1 -0
  69. package/dist/migration-list-types-BRTuXR8i.d.mts +23 -0
  70. package/dist/migration-list-types-BRTuXR8i.d.mts.map +1 -0
  71. package/dist/op-schema-D5qkXfEf.mjs.map +1 -1
  72. package/dist/{package-DZj8YvD0.d.mts → package-DIttKL7X.d.mts} +1 -1
  73. package/dist/package-DIttKL7X.d.mts.map +1 -0
  74. package/dist/read-contract-space-contract-BS5Oxbgw.mjs +82 -0
  75. package/dist/read-contract-space-contract-BS5Oxbgw.mjs.map +1 -0
  76. package/dist/{refs-BDHo5l_g.mjs → refs-BBKNL45K.mjs} +76 -4
  77. package/dist/refs-BBKNL45K.mjs.map +1 -0
  78. package/dist/{refs-CDaNerhT.d.mts → refs-C8f2IGM8.d.mts} +12 -2
  79. package/dist/refs-C8f2IGM8.d.mts.map +1 -0
  80. package/dist/{read-contract-space-contract-DRueB4Aa.mjs → verify-contract-spaces-BJX5gqtD.mjs} +32 -80
  81. package/dist/verify-contract-spaces-BJX5gqtD.mjs.map +1 -0
  82. package/dist/verify-contract-spaces-T0aiJlBS.d.mts +132 -0
  83. package/dist/verify-contract-spaces-T0aiJlBS.d.mts.map +1 -0
  84. package/package.json +18 -6
  85. package/src/aggregate/aggregate.ts +90 -0
  86. package/src/aggregate/check-integrity.ts +243 -0
  87. package/src/aggregate/loader.ts +156 -334
  88. package/src/aggregate/planner.ts +8 -6
  89. package/src/aggregate/project-schema-to-space.ts +1 -1
  90. package/src/aggregate/strategies/graph-walk.ts +12 -7
  91. package/src/aggregate/strategies/synth.ts +2 -2
  92. package/src/aggregate/types.ts +56 -64
  93. package/src/aggregate/verifier.ts +6 -4
  94. package/src/assert-descriptor-self-consistency.ts +6 -0
  95. package/src/compute-extension-space-apply-path.ts +1 -1
  96. package/src/emit-contract-space-artefacts.ts +4 -3
  97. package/src/enumerate-migration-spaces.ts +127 -0
  98. package/src/errors.ts +17 -2
  99. package/src/exports/aggregate.ts +17 -12
  100. package/src/exports/enumerate-migration-spaces.ts +4 -0
  101. package/src/exports/io.ts +2 -0
  102. package/src/exports/metadata.ts +1 -1
  103. package/src/exports/migration-graph.ts +1 -0
  104. package/src/exports/migration-list-graph-topology.ts +5 -0
  105. package/src/exports/migration-list-types.ts +5 -0
  106. package/src/exports/refs.ts +8 -0
  107. package/src/exports/spaces.ts +3 -0
  108. package/src/graph-membership.ts +17 -0
  109. package/src/graph.ts +0 -1
  110. package/src/hash.ts +7 -8
  111. package/src/integrity-violation.ts +114 -0
  112. package/src/io.ts +139 -14
  113. package/src/metadata.ts +1 -1
  114. package/src/migration-base.ts +10 -30
  115. package/src/migration-graph.ts +7 -35
  116. package/src/migration-list-graph-topology.ts +158 -0
  117. package/src/migration-list-types.ts +21 -0
  118. package/src/read-contract-space-head-ref.ts +5 -2
  119. package/src/refs/snapshot.ts +197 -0
  120. package/src/refs.ts +97 -1
  121. package/src/space-layout.ts +30 -0
  122. package/dist/errors-DGYwcwXs.mjs.map +0 -1
  123. package/dist/exports/io.d.mts.map +0 -1
  124. package/dist/graph-BrLXqoUc.d.mts.map +0 -1
  125. package/dist/hash-Cr4WIr4Z.mjs.map +0 -1
  126. package/dist/io-BPLfzvZe.mjs.map +0 -1
  127. package/dist/metadata-BFX0xdz8.d.mts +0 -2
  128. package/dist/migration-graph-De0dUZoC.d.mts.map +0 -1
  129. package/dist/migration-graph-nlS4TRpn.mjs.map +0 -1
  130. package/dist/package-DZj8YvD0.d.mts.map +0 -1
  131. package/dist/read-contract-space-contract-DRueB4Aa.mjs.map +0 -1
  132. package/dist/refs-BDHo5l_g.mjs.map +0 -1
  133. package/dist/refs-CDaNerhT.d.mts.map +0 -1
@@ -1,9 +1,77 @@
1
- import { t as MigrationToolsError } from "../errors-DGYwcwXs.mjs";
2
- import { s as readMigrationsDir } from "../io-BPLfzvZe.mjs";
3
- import "../constants-DWV9_o2Z.mjs";
4
- import { l as reconstructGraph, o as findPathWithDecision } from "../migration-graph-nlS4TRpn.mjs";
5
- import { a as APP_SPACE_ID, c as spaceMigrationDirectory, i as readContractSpaceHeadRef, n as listContractSpaceDirectories, t as readContractSpaceContract } from "../read-contract-space-contract-DRueB4Aa.mjs";
1
+ import { t as MigrationToolsError } from "../errors-4YabujxZ.mjs";
2
+ import { s as readMigrationsDir } from "../io-BHl0amF0.mjs";
3
+ import { t as EMPTY_CONTRACT_HASH } from "../constants-DWV9_o2Z.mjs";
4
+ import { l as reconstructGraph, o as findPathWithDecision } from "../migration-graph-kGBkIZDa.mjs";
5
+ import { a as readRefsTolerant, t as HEAD_REF_NAME } from "../refs-BBKNL45K.mjs";
6
+ import { c as spaceMigrationDirectory, i as RESERVED_SPACE_SUBDIR_NAMES, l as spaceRefsDirectory, r as APP_SPACE_ID, s as isValidSpaceId, t as listContractSpaceDirectories } from "../verify-contract-spaces-BJX5gqtD.mjs";
7
+ import { n as readContractSpaceHeadRef, t as readContractSpaceContract } from "../read-contract-space-contract-BS5Oxbgw.mjs";
6
8
  import { notOk, ok } from "@prisma-next/utils/result";
9
+ //#region src/aggregate/aggregate.ts
10
+ /**
11
+ * Resolve a member's head ref, asserting it is present. The apply/verify
12
+ * engine only runs after `checkIntegrity` has refused on `headRefMissing`,
13
+ * so a member reaching the planner / verifier without a head ref is a
14
+ * programming error (the integrity gate was skipped), not a user-facing
15
+ * state. The app member's head ref is always synthesised, so this only
16
+ * ever guards an ungated extension space.
17
+ */
18
+ function requireHeadRef(member) {
19
+ if (member.headRef === null) throw new Error(`Contract space "${member.spaceId}" has no head ref; the integrity gate must refuse a missing head ref before planning or verifying.`);
20
+ return member.headRef;
21
+ }
22
+ /**
23
+ * Build a {@link ContractSpaceMember} with lazily-memoised `graph()` and
24
+ * `contract()` facets.
25
+ *
26
+ * `graph()` reconstructs the migration graph from `packages` on first
27
+ * call and caches it. `contract()` calls `resolveContract` on first call
28
+ * and caches the result; a throwing `resolveContract` (e.g. a missing or
29
+ * undeserializable on-disk contract) re-throws on each call rather than
30
+ * caching a value — `checkIntegrity` surfaces that as `contractUnreadable`.
31
+ */
32
+ function createContractSpaceMember(args) {
33
+ const { spaceId, packages, refs, headRef, resolveContract } = args;
34
+ let graphMemo;
35
+ let contractMemo;
36
+ return {
37
+ spaceId,
38
+ packages,
39
+ refs,
40
+ headRef,
41
+ graph() {
42
+ graphMemo ??= reconstructGraph(packages);
43
+ return graphMemo;
44
+ },
45
+ contract() {
46
+ contractMemo ??= resolveContract();
47
+ return contractMemo;
48
+ }
49
+ };
50
+ }
51
+ /**
52
+ * Assemble a {@link ContractSpaceAggregate} value from its members and a
53
+ * `checkIntegrity` implementation. The query methods (`listSpaces` /
54
+ * `hasSpace` / `space` / `spaces`) are derived here so every aggregate —
55
+ * loader-built or test-built — shares one query surface: `app` first,
56
+ * then `extensions` in the order supplied (the loader sorts them
57
+ * lex-ascending by `spaceId`).
58
+ */
59
+ function createContractSpaceAggregate(args) {
60
+ const { targetId, app, extensions, checkIntegrity } = args;
61
+ const ordered = [app, ...extensions];
62
+ const byId = new Map(ordered.map((m) => [m.spaceId, m]));
63
+ return {
64
+ targetId,
65
+ app,
66
+ extensions,
67
+ listSpaces: () => ordered.map((m) => m.spaceId),
68
+ hasSpace: (id) => byId.has(id),
69
+ space: (id) => byId.get(id),
70
+ spaces: () => ordered,
71
+ checkIntegrity
72
+ };
73
+ }
74
+ //#endregion
7
75
  //#region src/aggregate/extract-storage-element-names.ts
8
76
  /**
9
77
  * Extract the set of top-level storage element names a contract claims.
@@ -60,189 +128,296 @@ function addRecordKeys(value, names) {
60
128
  if (typeof value === "object" && value !== null && !Array.isArray(value)) for (const name of Object.keys(value)) names.add(name);
61
129
  }
62
130
  //#endregion
63
- //#region src/aggregate/loader.ts
64
- function integrityDetail(error) {
65
- if (MigrationToolsError.is(error)) return error.why;
66
- if (error instanceof Error) return error.message;
67
- return String(error);
68
- }
131
+ //#region src/aggregate/check-integrity.ts
69
132
  /**
70
- * Hydrate a {@link ContractSpaceAggregate} from on-disk state and
71
- * the app contract value the caller supplies.
72
- *
73
- * The loader is the **only** descriptor-import boundary at apply /
74
- * verify time, but it intentionally does **not** read the extension
75
- * descriptor's `contractJson` value. Each extension space's contract
76
- * is read from its on-disk `migrations/<id>/contract.json` mirror; the
77
- * descriptor's role is exhausted by the seed phase that wrote that
78
- * mirror in the first place. The loader composes existing
79
- * migration-tools primitives — layout precheck (via
80
- * {@link listContractSpaceDirectories}), integrity checks (via
81
- * {@link readMigrationsDir} / {@link readContractSpaceHeadRef} /
82
- * {@link readContractSpaceContract} / `deserializeContract`), and
83
- * disjointness — into a single typed value.
84
- *
85
- * Failure semantics: every failure variant in {@link LoadAggregateError}
86
- * short-circuits the load.
133
+ * Walk the loaded model and return **every** integrity violation — never
134
+ * bailing at the first. Structurally-derivable violations (load-time
135
+ * problems, self-edges, missing / unreachable head refs) are always
136
+ * produced; layout-drift checks require `declaredExtensions`, and
137
+ * contract / target / disjointness checks require `checkContracts`.
87
138
  */
88
- async function loadContractSpaceAggregate(input) {
89
- const appContractTarget = input.appContract.target;
90
- if (appContractTarget !== input.targetId) return notOk({
91
- kind: "targetMismatch",
92
- spaceId: APP_SPACE_ID,
93
- expected: input.targetId,
94
- actual: appContractTarget
95
- });
96
- for (const entry of input.declaredExtensions) if (entry.targetId !== input.targetId) return notOk({
97
- kind: "targetMismatch",
98
- spaceId: entry.id,
99
- expected: input.targetId,
100
- actual: entry.targetId
139
+ function computeIntegrityViolations(input, opts) {
140
+ const violations = [];
141
+ for (const { member, problems, refProblems, headRefProblem, isApp } of input.spaces) {
142
+ const { spaceId } = member;
143
+ for (const problem of problems) violations.push(loadProblemToViolation(spaceId, problem));
144
+ for (const refProblem of refProblems) violations.push({
145
+ kind: "refUnreadable",
146
+ spaceId,
147
+ refName: refProblem.refName,
148
+ detail: refProblem.detail
149
+ });
150
+ if (headRefProblem !== null) violations.push({
151
+ kind: "refUnreadable",
152
+ spaceId,
153
+ refName: headRefProblem.refName,
154
+ detail: headRefProblem.detail
155
+ });
156
+ for (const pkg of member.packages) {
157
+ const from = pkg.metadata.from ?? "sha256:empty";
158
+ const isSelfEdge = from === pkg.metadata.to;
159
+ const hasDataOp = pkg.ops.some((op) => op.operationClass === "data");
160
+ if (isSelfEdge && !hasDataOp) violations.push({
161
+ kind: "sameSourceAndTarget",
162
+ spaceId,
163
+ dirName: pkg.dirName,
164
+ hash: from
165
+ });
166
+ }
167
+ violations.push(...duplicateMigrationHashViolations(spaceId, member.packages));
168
+ if (!isApp && headRefProblem === null) {
169
+ if (member.headRef === null) violations.push({
170
+ kind: "headRefMissing",
171
+ spaceId
172
+ });
173
+ else if (!headRefPresentInGraph(member, member.headRef.hash)) violations.push({
174
+ kind: "headRefNotInGraph",
175
+ spaceId,
176
+ hash: member.headRef.hash
177
+ });
178
+ }
179
+ }
180
+ if (opts?.declaredExtensions !== void 0) violations.push(...layoutViolations(input.spaces, opts.declaredExtensions));
181
+ if (opts?.checkContracts === true) violations.push(...contractViolations(input));
182
+ return violations;
183
+ }
184
+ function loadProblemToViolation(spaceId, problem) {
185
+ switch (problem.kind) {
186
+ case "hashMismatch": return {
187
+ kind: "hashMismatch",
188
+ spaceId,
189
+ dirName: problem.dirName,
190
+ stored: problem.stored,
191
+ computed: problem.computed
192
+ };
193
+ case "providedInvariantsMismatch": return {
194
+ kind: "providedInvariantsMismatch",
195
+ spaceId,
196
+ dirName: problem.dirName
197
+ };
198
+ case "packageUnloadable": return {
199
+ kind: "packageUnloadable",
200
+ spaceId,
201
+ dirName: problem.dirName,
202
+ detail: problem.detail
203
+ };
204
+ }
205
+ }
206
+ function duplicateMigrationHashViolations(spaceId, packages) {
207
+ const dirNamesByHash = /* @__PURE__ */ new Map();
208
+ for (const pkg of packages) {
209
+ const hash = pkg.metadata.migrationHash;
210
+ const dirNames = dirNamesByHash.get(hash);
211
+ if (dirNames) dirNames.push(pkg.dirName);
212
+ else dirNamesByHash.set(hash, [pkg.dirName]);
213
+ }
214
+ const out = [];
215
+ for (const [migrationHash, dirNames] of dirNamesByHash) if (dirNames.length > 1) out.push({
216
+ kind: "duplicateMigrationHash",
217
+ spaceId,
218
+ migrationHash,
219
+ dirNames: [...dirNames].sort()
101
220
  });
102
- const declaredSpaceIds = new Set(input.declaredExtensions.map((e) => e.id));
103
- const extensionDirsOnDisk = (await listContractSpaceDirectories(input.migrationsDir)).filter((d) => d !== APP_SPACE_ID);
104
- const spaceDirSet = new Set(extensionDirsOnDisk);
105
- const layoutViolations = [];
106
- for (const dir of extensionDirsOnDisk) if (!declaredSpaceIds.has(dir)) layoutViolations.push({
221
+ return out;
222
+ }
223
+ /**
224
+ * Whether a space's head-ref hash is present in its reconstructed graph.
225
+ * An empty graph is reachable only by the empty-contract sentinel.
226
+ */
227
+ function headRefPresentInGraph(member, headHash) {
228
+ const graph = member.graph();
229
+ if (graph.nodes.size === 0) return headHash === EMPTY_CONTRACT_HASH;
230
+ return graph.nodes.has(headHash);
231
+ }
232
+ function layoutViolations(spaces, declaredExtensions) {
233
+ const out = [];
234
+ const extensionSpaceIds = new Set(spaces.filter((s) => !s.isApp).map((s) => s.member.spaceId));
235
+ const declaredIds = new Set(declaredExtensions.map((d) => d.id));
236
+ for (const id of [...extensionSpaceIds].sort()) if (!declaredIds.has(id)) out.push({
107
237
  kind: "orphanSpaceDir",
108
- spaceId: dir
238
+ spaceId: id
109
239
  });
110
- for (const id of [...declaredSpaceIds].sort()) if (!spaceDirSet.has(id)) layoutViolations.push({
240
+ for (const id of [...declaredIds].sort()) if (!extensionSpaceIds.has(id)) out.push({
111
241
  kind: "declaredButUnmigrated",
112
242
  spaceId: id
113
243
  });
114
- if (layoutViolations.length > 0) return notOk({
115
- kind: "layoutViolation",
116
- violations: layoutViolations
117
- });
118
- const loadedExtensions = [];
119
- for (const entry of [...input.declaredExtensions].sort((a, b) => a.id.localeCompare(b.id))) {
120
- const headRef = await readContractSpaceHeadRef(input.migrationsDir, entry.id);
121
- if (headRef === null) return notOk({
122
- kind: "integrityFailure",
123
- spaceId: entry.id,
124
- detail: `Head ref \`refs/head.json\` is missing for extension space "${entry.id}".`
125
- });
126
- let spaceContractRaw;
127
- try {
128
- spaceContractRaw = await readContractSpaceContract(input.migrationsDir, entry.id);
129
- } catch (error) {
130
- return notOk({
131
- kind: "integrityFailure",
132
- spaceId: entry.id,
133
- detail: integrityDetail(error)
134
- });
135
- }
136
- let spaceContract;
244
+ return out;
245
+ }
246
+ function contractViolations(input) {
247
+ const out = [];
248
+ const elementClaimedBy = /* @__PURE__ */ new Map();
249
+ for (const { member } of input.spaces) {
250
+ let contract;
137
251
  try {
138
- spaceContract = input.deserializeContract(spaceContractRaw);
252
+ contract = member.contract();
139
253
  } catch (error) {
140
- return notOk({
141
- kind: "validationFailure",
142
- spaceId: entry.id,
143
- detail: integrityDetail(error)
254
+ out.push({
255
+ kind: "contractUnreadable",
256
+ spaceId: member.spaceId,
257
+ detail: detailOf$1(error)
144
258
  });
259
+ continue;
145
260
  }
146
- if (spaceContract.target !== input.targetId) return notOk({
261
+ if (contract.target !== input.targetId) out.push({
147
262
  kind: "targetMismatch",
148
- spaceId: entry.id,
263
+ spaceId: member.spaceId,
149
264
  expected: input.targetId,
150
- actual: spaceContract.target
151
- });
152
- let packages;
153
- try {
154
- packages = await readMigrationsDir(spaceMigrationDirectory(input.migrationsDir, entry.id));
155
- } catch (error) {
156
- return notOk({
157
- kind: "integrityFailure",
158
- spaceId: entry.id,
159
- detail: integrityDetail(error)
160
- });
161
- }
162
- let graph;
163
- try {
164
- graph = reconstructGraph(packages);
165
- } catch (error) {
166
- return notOk({
167
- kind: "integrityFailure",
168
- spaceId: entry.id,
169
- detail: integrityDetail(error)
170
- });
171
- }
172
- if (graph.nodes.size === 0) {
173
- if (headRef.hash !== "sha256:empty") return notOk({
174
- kind: "integrityFailure",
175
- spaceId: entry.id,
176
- detail: `Head ref "${headRef.hash}" is not present in the (empty) on-disk migration graph.`
177
- });
178
- } else if (!graph.nodes.has(headRef.hash)) return notOk({
179
- kind: "integrityFailure",
180
- spaceId: entry.id,
181
- detail: `Head ref "${headRef.hash}" is not present in the on-disk migration graph.`
182
- });
183
- const packagesByMigrationHash = new Map(packages.map((p) => [p.metadata.migrationHash, p]));
184
- loadedExtensions.push({
185
- entry,
186
- contract: spaceContract,
187
- headRefHash: headRef.hash,
188
- headRefInvariants: [...headRef.invariants].sort(),
189
- migrations: {
190
- graph,
191
- packagesByMigrationHash
192
- }
265
+ actual: contract.target
193
266
  });
194
- }
195
- let appGraph;
196
- try {
197
- appGraph = reconstructGraph(input.appMigrationPackages);
198
- } catch (error) {
199
- return notOk({
200
- kind: "integrityFailure",
201
- spaceId: APP_SPACE_ID,
202
- detail: error instanceof Error ? error.message : String(error)
203
- });
204
- }
205
- const appPackagesByMigrationHash = new Map(input.appMigrationPackages.map((p) => [p.metadata.migrationHash, p]));
206
- const appMember = {
207
- spaceId: APP_SPACE_ID,
208
- contract: input.appContract,
209
- headRef: {
210
- hash: input.appContract.storage.storageHash,
211
- invariants: []
212
- },
213
- migrations: {
214
- graph: appGraph,
215
- packagesByMigrationHash: appPackagesByMigrationHash
216
- }
217
- };
218
- const extensionMembers = loadedExtensions.map((s) => ({
219
- spaceId: s.entry.id,
220
- contract: s.contract,
221
- headRef: {
222
- hash: s.headRefHash,
223
- invariants: s.headRefInvariants
224
- },
225
- migrations: s.migrations
226
- }));
227
- const elementClaimedBy = /* @__PURE__ */ new Map();
228
- for (const member of [appMember, ...extensionMembers]) {
229
- const elements = extractStorageElementNames(member.contract);
230
- for (const elementName of elements) {
267
+ for (const elementName of extractStorageElementNames(contract)) {
231
268
  const claimers = elementClaimedBy.get(elementName);
232
269
  if (claimers) claimers.push(member.spaceId);
233
270
  else elementClaimedBy.set(elementName, [member.spaceId]);
234
271
  }
235
272
  }
236
- for (const [element, claimedBy] of elementClaimedBy) if (claimedBy.length > 1) return notOk({
237
- kind: "disjointnessViolation",
273
+ const disjointness = [];
274
+ for (const [element, claimedBy] of elementClaimedBy) if (claimedBy.length > 1) disjointness.push({
275
+ kind: "disjointness",
238
276
  element,
239
277
  claimedBy: [...claimedBy].sort()
240
278
  });
241
- return ok({ aggregate: {
242
- targetId: input.targetId,
243
- app: appMember,
244
- extensions: extensionMembers
245
- } });
279
+ disjointness.sort((a, b) => a.kind === "disjointness" && b.kind === "disjointness" ? a.element.localeCompare(b.element) : 0);
280
+ out.push(...disjointness);
281
+ return out;
282
+ }
283
+ function detailOf$1(error) {
284
+ if (MigrationToolsError.is(error)) return error.why;
285
+ if (error instanceof Error) return error.message;
286
+ return String(error);
287
+ }
288
+ //#endregion
289
+ //#region src/aggregate/loader.ts
290
+ /**
291
+ * Build a tolerant, queryable {@link ContractSpaceAggregate} from on-disk
292
+ * migration state plus the caller's live app contract.
293
+ *
294
+ * Building **never throws on disk content**: a hash- or
295
+ * invariants-mismatched package is retained, an unparseable package is
296
+ * omitted, a missing extension head ref leaves `headRef: null`, and an
297
+ * unreadable on-disk contract defers its failure to `member.contract()`.
298
+ * Every such problem is judged by {@link ContractSpaceAggregate.checkIntegrity}
299
+ * rather than aborting the load. The only rejections are catastrophic I/O
300
+ * (a `migrations/` that exists but is unreadable for reasons other than
301
+ * absence).
302
+ *
303
+ * The app space's head ref is synthesised from the live contract's
304
+ * storage hash (the app contract is authored independently of the
305
+ * migration graph), and `app.contract()` returns the supplied contract.
306
+ * Extension spaces read their contract, refs, and head ref from disk.
307
+ */
308
+ async function loadContractSpaceAggregate(input) {
309
+ const { migrationsDir, deserializeContract, appContract } = input;
310
+ const targetId = appContract.target;
311
+ const appState = await loadAppSpace(migrationsDir, appContract);
312
+ const extensionStates = await loadExtensionSpaces(migrationsDir, deserializeContract);
313
+ const spaces = [appState, ...extensionStates];
314
+ return createContractSpaceAggregate({
315
+ targetId,
316
+ app: appState.member,
317
+ extensions: extensionStates.map((state) => state.member),
318
+ checkIntegrity: (opts) => computeIntegrityViolations({
319
+ targetId,
320
+ spaces
321
+ }, opts)
322
+ });
323
+ }
324
+ async function loadAppSpace(migrationsDir, appContract) {
325
+ const spaceDir = spaceMigrationDirectory(migrationsDir, APP_SPACE_ID);
326
+ const { packages, problems } = await readMigrationsDir(spaceDir);
327
+ const { refs, problems: refProblems } = await readRefsTolerant(spaceRefsDirectory(spaceDir));
328
+ return {
329
+ member: createContractSpaceMember({
330
+ spaceId: APP_SPACE_ID,
331
+ packages,
332
+ refs,
333
+ headRef: {
334
+ hash: appContract.storage.storageHash,
335
+ invariants: []
336
+ },
337
+ resolveContract: () => appContract
338
+ }),
339
+ problems,
340
+ refProblems,
341
+ headRefProblem: null,
342
+ isApp: true
343
+ };
344
+ }
345
+ async function loadExtensionSpaces(migrationsDir, deserializeContract) {
346
+ const extensionIds = (await listContractSpaceDirectories(migrationsDir)).filter((name) => name !== APP_SPACE_ID).filter((name) => !RESERVED_SPACE_SUBDIR_NAMES.has(name)).filter(isValidSpaceId).sort();
347
+ const states = [];
348
+ for (const spaceId of extensionIds) states.push(await loadExtensionSpace(migrationsDir, spaceId, deserializeContract));
349
+ return states;
350
+ }
351
+ async function loadExtensionSpace(migrationsDir, spaceId, deserializeContract) {
352
+ const spaceDir = spaceMigrationDirectory(migrationsDir, spaceId);
353
+ const { packages, problems } = await readMigrationsDir(spaceDir);
354
+ const { refs, problems: refProblems } = await readRefsTolerant(spaceRefsDirectory(spaceDir));
355
+ const { headRef, problem: headRefProblem } = await readHeadRefTolerant(migrationsDir, spaceId);
356
+ const rawContract = await readRawContractDeferred(migrationsDir, spaceId);
357
+ return {
358
+ member: createContractSpaceMember({
359
+ spaceId,
360
+ packages,
361
+ refs,
362
+ headRef,
363
+ resolveContract: () => deserializeContract(rawContract())
364
+ }),
365
+ problems,
366
+ refProblems,
367
+ headRefProblem,
368
+ isApp: false
369
+ };
370
+ }
371
+ /**
372
+ * Read an extension's head ref, distinguishing a *genuinely absent*
373
+ * `head.json` (`headRef: null`, no problem — judged `headRefMissing`)
374
+ * from one that *exists but cannot be parsed* (`headRef: null` plus a
375
+ * problem — judged `refUnreadable`, not `headRefMissing`).
376
+ * `readContractSpaceHeadRef` already returns `null` only for ENOENT and
377
+ * throws for unparseable / schema-invalid content, so the throw is the
378
+ * corruption signal. Construction never throws on disk content.
379
+ */
380
+ function isToleratedRefHeadReadError(error) {
381
+ if (MigrationToolsError.is(error)) return true;
382
+ if (!(error instanceof Error)) return false;
383
+ const code = error.code;
384
+ return code === "ENOENT" || code === "EISDIR";
385
+ }
386
+ async function readHeadRefTolerant(migrationsDir, spaceId) {
387
+ try {
388
+ return {
389
+ headRef: await readContractSpaceHeadRef(migrationsDir, spaceId),
390
+ problem: null
391
+ };
392
+ } catch (error) {
393
+ if (!isToleratedRefHeadReadError(error)) throw error;
394
+ return {
395
+ headRef: null,
396
+ problem: {
397
+ refName: HEAD_REF_NAME,
398
+ detail: detailOf(error)
399
+ }
400
+ };
401
+ }
402
+ }
403
+ function detailOf(error) {
404
+ return error instanceof Error ? error.message : String(error);
405
+ }
406
+ /**
407
+ * Read the raw on-disk contract eagerly (cheap I/O) but defer its
408
+ * (throwing) failure to call time, so a missing or unparseable
409
+ * `contract.json` becomes a `contract()` throw — surfaced as
410
+ * `contractUnreadable` — rather than a construction failure.
411
+ */
412
+ async function readRawContractDeferred(migrationsDir, spaceId) {
413
+ try {
414
+ const raw = await readContractSpaceContract(migrationsDir, spaceId);
415
+ return () => raw;
416
+ } catch (error) {
417
+ return () => {
418
+ throw error;
419
+ };
420
+ }
246
421
  }
247
422
  //#endregion
248
423
  //#region src/aggregate/strategies/graph-walk.ts
@@ -263,11 +438,13 @@ async function loadContractSpaceAggregate(input) {
263
438
  */
264
439
  function graphWalkStrategy(input) {
265
440
  const { aggregateTargetId, member, currentMarker, refName } = input;
266
- const { graph, packagesByMigrationHash } = member.migrations;
441
+ const headRef = requireHeadRef(member);
442
+ const graph = member.graph();
443
+ const packagesByMigrationHash = new Map(member.packages.map((pkg) => [pkg.metadata.migrationHash, pkg]));
267
444
  const fromHash = currentMarker?.storageHash ?? "sha256:empty";
268
445
  const markerInvariants = new Set(currentMarker?.invariants ?? []);
269
- const required = new Set(member.headRef.invariants.filter((id) => !markerInvariants.has(id)));
270
- const outcome = findPathWithDecision(graph, fromHash, member.headRef.hash, {
446
+ const required = new Set(headRef.invariants.filter((id) => !markerInvariants.has(id)));
447
+ const outcome = findPathWithDecision(graph, fromHash, headRef.hash, {
271
448
  required,
272
449
  ...refName !== void 0 ? { refName } : {}
273
450
  });
@@ -299,12 +476,12 @@ function graphWalkStrategy(input) {
299
476
  targetId: aggregateTargetId,
300
477
  spaceId: member.spaceId,
301
478
  origin: currentMarker === null ? null : { storageHash: currentMarker.storageHash },
302
- destination: { storageHash: member.headRef.hash },
479
+ destination: { storageHash: headRef.hash },
303
480
  operations: pathOps,
304
481
  providedInvariants: [...providedInvariantsSet].sort()
305
482
  },
306
483
  displayOps: pathOps,
307
- destinationContract: member.contract,
484
+ destinationContract: member.contract(),
308
485
  strategy: "graph-walk",
309
486
  migrationEdges: edgeRefs,
310
487
  pathDecision: outcome.decision
@@ -386,7 +563,7 @@ function collectOwnedNames(member, otherMembers) {
386
563
  const owned = /* @__PURE__ */ new Set();
387
564
  for (const other of otherMembers) {
388
565
  if (other.spaceId === member.spaceId) continue;
389
- for (const name of extractStorageElementNames(other.contract)) owned.add(name);
566
+ for (const name of extractStorageElementNames(other.contract())) owned.add(name);
390
567
  }
391
568
  return owned;
392
569
  }
@@ -450,7 +627,7 @@ function pruneCollectionsArray(schemaObj, ownedByOthers) {
450
627
  async function synthStrategy(input) {
451
628
  const projectedSchema = projectSchemaToSpace(input.schemaIntrospection, input.member, input.otherMembers);
452
629
  const plannerResult = await input.migrations.createPlanner(input.familyInstance).plan({
453
- contract: input.member.contract,
630
+ contract: input.member.contract(),
454
631
  schema: projectedSchema,
455
632
  policy: input.operationPolicy,
456
633
  fromContract: null,
@@ -476,7 +653,7 @@ async function synthStrategy(input) {
476
653
  }
477
654
  }),
478
655
  displayOps: synthedPlan.operations,
479
- destinationContract: input.member.contract,
656
+ destinationContract: input.member.contract(),
480
657
  strategy: "synth"
481
658
  }
482
659
  };
@@ -491,7 +668,7 @@ async function synthStrategy(input) {
491
668
  * 1. If `callerPolicy.ignoreGraphFor.has(member.spaceId)`:
492
669
  * - If `member.headRef.invariants` is empty → synth.
493
670
  * - Else → `policyConflict` (synth cannot satisfy authored invariants).
494
- * 2. Else if `member.migrations.graph` is non-empty AND graph-walk
671
+ * 2. Else if `member.graph()` is non-empty AND graph-walk
495
672
  * succeeds → graph-walk.
496
673
  * 3. Else if `member.headRef.invariants` is empty → synth.
497
674
  * 4. Else → graph-walk failure → `extensionPathUnreachable` /
@@ -513,12 +690,13 @@ async function planAggregate(input) {
513
690
  for (const member of orderedMembers) {
514
691
  const otherMembers = allMembers.filter((m) => m.spaceId !== member.spaceId);
515
692
  const currentMarker = currentDBState.markersBySpaceId.get(member.spaceId) ?? null;
693
+ const headRef = requireHeadRef(member);
516
694
  const ignoreGraph = callerPolicy.ignoreGraphFor.has(member.spaceId);
517
- const invariantsRequired = member.headRef.invariants.length > 0;
695
+ const invariantsRequired = headRef.invariants.length > 0;
518
696
  if (ignoreGraph && invariantsRequired) return notOk({
519
697
  kind: "policyConflict",
520
698
  spaceId: member.spaceId,
521
- 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.`
699
+ 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.`
522
700
  });
523
701
  if (ignoreGraph) {
524
702
  const synthOutcome = await synthStrategy({
@@ -539,7 +717,7 @@ async function planAggregate(input) {
539
717
  perSpace.set(member.spaceId, synthOutcome.result);
540
718
  continue;
541
719
  }
542
- if (member.migrations.graph.nodes.size > 0) {
720
+ if (member.graph().nodes.size > 0) {
543
721
  const walked = graphWalkStrategy({
544
722
  aggregateTargetId: aggregate.targetId,
545
723
  member,
@@ -552,7 +730,7 @@ async function planAggregate(input) {
552
730
  if (walked.kind === "unreachable") return notOk({
553
731
  kind: "extensionPathUnreachable",
554
732
  spaceId: member.spaceId,
555
- target: member.headRef.hash
733
+ target: headRef.hash
556
734
  });
557
735
  return notOk({
558
736
  kind: "extensionPathUnsatisfiable",
@@ -563,7 +741,7 @@ async function planAggregate(input) {
563
741
  if (invariantsRequired) return notOk({
564
742
  kind: "extensionPathUnsatisfiable",
565
743
  spaceId: member.spaceId,
566
- missingInvariants: [...member.headRef.invariants].sort()
744
+ missingInvariants: [...headRef.invariants].sort()
567
745
  });
568
746
  const synthOutcome = await synthStrategy({
569
747
  aggregateTargetId: aggregate.targetId,
@@ -632,16 +810,17 @@ function runVerifyAggregate(input) {
632
810
  markerPerSpace.set(member.spaceId, { kind: "absent" });
633
811
  continue;
634
812
  }
635
- if (marker.storageHash !== member.headRef.hash) {
813
+ const headRef = requireHeadRef(member);
814
+ if (marker.storageHash !== headRef.hash) {
636
815
  markerPerSpace.set(member.spaceId, {
637
816
  kind: "hashMismatch",
638
817
  markerHash: marker.storageHash,
639
- expected: member.headRef.hash
818
+ expected: headRef.hash
640
819
  });
641
820
  continue;
642
821
  }
643
822
  const markerInvariants = new Set(marker.invariants);
644
- const missing = member.headRef.invariants.filter((id) => !markerInvariants.has(id));
823
+ const missing = headRef.invariants.filter((id) => !markerInvariants.has(id));
645
824
  if (missing.length > 0) {
646
825
  markerPerSpace.set(member.spaceId, {
647
826
  kind: "missingInvariants",
@@ -684,7 +863,7 @@ function detectOrphanElements(schemaIntrospection, members) {
684
863
  const liveTables = schemaIntrospection.tables;
685
864
  if (typeof liveTables !== "object" || liveTables === null) return [];
686
865
  const claimedTables = /* @__PURE__ */ new Set();
687
- for (const member of members) for (const name of extractStorageElementNames(member.contract)) claimedTables.add(name);
866
+ for (const member of members) for (const name of extractStorageElementNames(member.contract())) claimedTables.add(name);
688
867
  const orphans = [];
689
868
  for (const tableName of Object.keys(liveTables)) if (!claimedTables.has(tableName)) orphans.push({
690
869
  kind: "table",
@@ -694,6 +873,6 @@ function detectOrphanElements(schemaIntrospection, members) {
694
873
  return orphans;
695
874
  }
696
875
  //#endregion
697
- export { graphWalkStrategy, loadContractSpaceAggregate, planAggregate, projectSchemaToSpace, verifyAggregate };
876
+ export { computeIntegrityViolations, createContractSpaceAggregate, createContractSpaceMember, graphWalkStrategy, loadContractSpaceAggregate, loadProblemToViolation, planAggregate, projectSchemaToSpace, requireHeadRef, verifyAggregate };
698
877
 
699
878
  //# sourceMappingURL=aggregate.mjs.map