@prisma-next/migration-tools 0.11.0 → 0.12.0-dev.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) 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 +338 -207
  5. package/dist/exports/aggregate.d.mts.map +1 -1
  6. package/dist/exports/aggregate.mjs +511 -254
  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/ledger-origin.d.mts +5 -0
  21. package/dist/exports/ledger-origin.d.mts.map +1 -0
  22. package/dist/exports/ledger-origin.mjs +10 -0
  23. package/dist/exports/ledger-origin.mjs.map +1 -0
  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 +3 -2
  28. package/dist/exports/migration-ts.d.mts.map +1 -1
  29. package/dist/exports/migration-ts.mjs.map +1 -1
  30. package/dist/exports/migration.d.mts +5 -6
  31. package/dist/exports/migration.d.mts.map +1 -1
  32. package/dist/exports/migration.mjs +14 -32
  33. package/dist/exports/migration.mjs.map +1 -1
  34. package/dist/exports/package.d.mts +1 -1
  35. package/dist/exports/ref-resolution.d.mts +2 -2
  36. package/dist/exports/ref-resolution.d.mts.map +1 -1
  37. package/dist/exports/ref-resolution.mjs +1 -1
  38. package/dist/exports/ref-resolution.mjs.map +1 -1
  39. package/dist/exports/refs.d.mts +15 -2
  40. package/dist/exports/refs.d.mts.map +1 -0
  41. package/dist/exports/refs.mjs +3 -2
  42. package/dist/exports/spaces.d.mts +31 -132
  43. package/dist/exports/spaces.d.mts.map +1 -1
  44. package/dist/exports/spaces.mjs +13 -9
  45. package/dist/exports/spaces.mjs.map +1 -1
  46. package/dist/{graph-BrLXqoUc.d.mts → graph-3dLMZp5l.d.mts} +1 -2
  47. package/dist/graph-3dLMZp5l.d.mts.map +1 -0
  48. package/dist/graph-membership-BV23F1IV.mjs +15 -0
  49. package/dist/graph-membership-BV23F1IV.mjs.map +1 -0
  50. package/dist/{hash-Cr4WIr4Z.mjs → hash--Y7vCpN3.mjs} +8 -9
  51. package/dist/hash--Y7vCpN3.mjs.map +1 -0
  52. package/dist/{invariants-0daYEzyo.mjs → invariants-C23nXy1c.mjs} +2 -2
  53. package/dist/{invariants-0daYEzyo.mjs.map → invariants-C23nXy1c.mjs.map} +1 -1
  54. package/dist/{io-BPLfzvZe.mjs → io-BGlPOt9b.mjs} +100 -13
  55. package/dist/io-BGlPOt9b.mjs.map +1 -0
  56. package/dist/io-BH4G3F-i.d.mts +124 -0
  57. package/dist/io-BH4G3F-i.d.mts.map +1 -0
  58. package/dist/metadata-Bp9X04gM.d.mts +2 -0
  59. package/dist/{migration-graph-nlS4TRpn.mjs → migration-graph-BMAqSfv9.mjs} +6 -26
  60. package/dist/migration-graph-BMAqSfv9.mjs.map +1 -0
  61. package/dist/{migration-graph-De0dUZoC.d.mts → migration-graph-CWEM2SLR.d.mts} +6 -6
  62. package/dist/migration-graph-CWEM2SLR.d.mts.map +1 -0
  63. package/dist/op-schema-D5qkXfEf.mjs.map +1 -1
  64. package/dist/{package-DZj8YvD0.d.mts → package-Ca-J_z_0.d.mts} +1 -1
  65. package/dist/package-Ca-J_z_0.d.mts.map +1 -0
  66. package/dist/{read-contract-space-contract-DRueB4Aa.mjs → read-contract-space-contract-TbeXuJXL.mjs} +32 -5
  67. package/dist/read-contract-space-contract-TbeXuJXL.mjs.map +1 -0
  68. package/dist/{refs-BDHo5l_g.mjs → refs-C-_WUrPw.mjs} +97 -4
  69. package/dist/refs-C-_WUrPw.mjs.map +1 -0
  70. package/dist/refs-C7wuYFqZ.d.mts +42 -0
  71. package/dist/refs-C7wuYFqZ.d.mts.map +1 -0
  72. package/dist/snapshot-Bazwo13S.mjs +137 -0
  73. package/dist/snapshot-Bazwo13S.mjs.map +1 -0
  74. package/dist/verify-contract-spaces-BdysZdQk.d.mts +132 -0
  75. package/dist/verify-contract-spaces-BdysZdQk.d.mts.map +1 -0
  76. package/package.json +22 -9
  77. package/src/aggregate/aggregate.ts +266 -0
  78. package/src/aggregate/check-integrity.ts +243 -0
  79. package/src/aggregate/loader.ts +161 -334
  80. package/src/aggregate/planner-types.ts +17 -17
  81. package/src/aggregate/planner.ts +22 -23
  82. package/src/aggregate/project-schema-to-space.ts +3 -8
  83. package/src/aggregate/strategies/graph-walk.ts +15 -10
  84. package/src/aggregate/strategies/synth.ts +15 -4
  85. package/src/aggregate/synth-migration-edge.ts +15 -0
  86. package/src/aggregate/types.ts +81 -62
  87. package/src/aggregate/verifier.ts +23 -23
  88. package/src/assert-descriptor-self-consistency.ts +6 -0
  89. package/src/compute-extension-space-apply-path.ts +1 -1
  90. package/src/emit-contract-space-artefacts.ts +4 -3
  91. package/src/errors.ts +58 -2
  92. package/src/exports/aggregate.ts +30 -19
  93. package/src/exports/io.ts +2 -0
  94. package/src/exports/ledger-origin.ts +1 -0
  95. package/src/exports/metadata.ts +1 -1
  96. package/src/exports/migration-graph.ts +1 -0
  97. package/src/exports/refs.ts +11 -0
  98. package/src/exports/spaces.ts +3 -0
  99. package/src/graph-membership.ts +17 -0
  100. package/src/graph.ts +0 -1
  101. package/src/hash.ts +7 -8
  102. package/src/integrity-violation.ts +114 -0
  103. package/src/io.ts +139 -14
  104. package/src/ledger-origin.ts +8 -0
  105. package/src/metadata.ts +1 -1
  106. package/src/migration-base.ts +10 -30
  107. package/src/migration-graph.ts +7 -35
  108. package/src/read-contract-space-head-ref.ts +5 -2
  109. package/src/refs/snapshot.ts +199 -0
  110. package/src/refs.ts +124 -1
  111. package/src/space-layout.ts +30 -0
  112. package/dist/errors-DGYwcwXs.mjs.map +0 -1
  113. package/dist/exports/io.d.mts.map +0 -1
  114. package/dist/graph-BrLXqoUc.d.mts.map +0 -1
  115. package/dist/hash-Cr4WIr4Z.mjs.map +0 -1
  116. package/dist/io-BPLfzvZe.mjs.map +0 -1
  117. package/dist/metadata-BFX0xdz8.d.mts +0 -2
  118. package/dist/migration-graph-De0dUZoC.d.mts.map +0 -1
  119. package/dist/migration-graph-nlS4TRpn.mjs.map +0 -1
  120. package/dist/package-DZj8YvD0.d.mts.map +0 -1
  121. package/dist/read-contract-space-contract-DRueB4Aa.mjs.map +0 -1
  122. package/dist/refs-BDHo5l_g.mjs.map +0 -1
  123. package/dist/refs-CDaNerhT.d.mts +0 -16
  124. package/dist/refs-CDaNerhT.d.mts.map +0 -1
  125. package/src/aggregate/extract-storage-element-names.ts +0 -75
@@ -1,248 +1,483 @@
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 { E as errorSnapshotMissing, f as errorInvalidJson, i as errorContractDeserializationFailed, l as errorHashNotInGraph, r as errorBundleNotFoundForGraphNode, t as MigrationToolsError, x as errorMissingFile } from "../errors-vFROOhCR.mjs";
2
+ import { s as readMigrationsDir } from "../io-BGlPOt9b.mjs";
3
+ import { t as EMPTY_CONTRACT_HASH } from "../constants-DWV9_o2Z.mjs";
4
+ import { n as isGraphNode } from "../graph-membership-BV23F1IV.mjs";
5
+ import { l as reconstructGraph, o as findPathWithDecision } from "../migration-graph-BMAqSfv9.mjs";
6
+ import { a as readRefsTolerant, t as HEAD_REF_NAME } from "../refs-C-_WUrPw.mjs";
7
+ import { r as readRefSnapshot } from "../snapshot-Bazwo13S.mjs";
8
+ import { a as APP_SPACE_ID, d as spaceRefsDirectory, i as readContractSpaceHeadRef, l as isValidSpaceId, n as listContractSpaceDirectories, o as RESERVED_SPACE_SUBDIR_NAMES, t as readContractSpaceContract, u as spaceMigrationDirectory } from "../read-contract-space-contract-TbeXuJXL.mjs";
9
+ import { join } from "pathe";
10
+ import { readFile } from "node:fs/promises";
6
11
  import { notOk, ok } from "@prisma-next/utils/result";
7
- //#region src/aggregate/extract-storage-element-names.ts
12
+ import { elementCoordinates } from "@prisma-next/framework-components/ir";
13
+ //#region src/aggregate/aggregate.ts
14
+ function hasErrnoCode(error, code) {
15
+ return error instanceof Error && error.code === code;
16
+ }
17
+ function contractAtMemoKey(hash, refName) {
18
+ return `${hash}\0${refName ?? ""}`;
19
+ }
20
+ function deserializeContractAtPath(filePath, contractJson, deserializeContract) {
21
+ try {
22
+ return deserializeContract(contractJson);
23
+ } catch (error) {
24
+ if (MigrationToolsError.is(error)) throw error;
25
+ throw errorContractDeserializationFailed(filePath, error instanceof Error ? error.message : String(error));
26
+ }
27
+ }
28
+ async function readGraphNodeEndContract(packageDir, deserializeContract) {
29
+ const jsonPath = join(packageDir, "end-contract.json");
30
+ const dtsPath = join(packageDir, "end-contract.d.ts");
31
+ let rawJson;
32
+ try {
33
+ rawJson = await readFile(jsonPath, "utf-8");
34
+ } catch (error) {
35
+ if (hasErrnoCode(error, "ENOENT")) throw errorMissingFile("end-contract.json", packageDir);
36
+ throw error;
37
+ }
38
+ let contractJson;
39
+ try {
40
+ contractJson = JSON.parse(rawJson);
41
+ } catch (error) {
42
+ throw errorInvalidJson(jsonPath, error instanceof Error ? error.message : String(error));
43
+ }
44
+ let contractDts;
45
+ try {
46
+ contractDts = await readFile(dtsPath, "utf-8");
47
+ } catch (error) {
48
+ if (hasErrnoCode(error, "ENOENT")) throw errorMissingFile("end-contract.d.ts", packageDir);
49
+ throw error;
50
+ }
51
+ const contract = deserializeContractAtPath(jsonPath, contractJson, deserializeContract);
52
+ return {
53
+ contractJson,
54
+ contractDts,
55
+ contract
56
+ };
57
+ }
58
+ async function resolveContractAt(args) {
59
+ const { hash, opts, refsDir, packages, graph, deserializeContract } = args;
60
+ const refName = opts?.refName;
61
+ if (refName !== void 0) {
62
+ const snapshot = await readRefSnapshot(refsDir, refName);
63
+ if (snapshot) {
64
+ const jsonPath = join(refsDir, `${refName}.contract.json`);
65
+ return {
66
+ hash,
67
+ contractJson: snapshot.contract,
68
+ contractDts: snapshot.contractDts,
69
+ contract: deserializeContractAtPath(jsonPath, snapshot.contract, deserializeContract),
70
+ provenance: "snapshot"
71
+ };
72
+ }
73
+ if (isGraphNode(hash, graph)) return resolveGraphNodeContractAt({
74
+ hash,
75
+ packages,
76
+ deserializeContract,
77
+ explicitLabel: refName
78
+ });
79
+ throw errorSnapshotMissing(refName);
80
+ }
81
+ if (isGraphNode(hash, graph)) return resolveGraphNodeContractAt({
82
+ hash,
83
+ packages,
84
+ deserializeContract
85
+ });
86
+ throw errorHashNotInGraph(hash, graph);
87
+ }
88
+ async function resolveGraphNodeContractAt(args) {
89
+ const { hash, packages, deserializeContract, explicitLabel } = args;
90
+ const matchingBundle = packages.find((pkg) => pkg.metadata.to === hash);
91
+ if (!matchingBundle) throw errorBundleNotFoundForGraphNode(hash, explicitLabel);
92
+ const { contractJson, contractDts, contract } = await readGraphNodeEndContract(matchingBundle.dirPath, deserializeContract);
93
+ return {
94
+ hash,
95
+ contractJson,
96
+ contractDts,
97
+ contract,
98
+ provenance: "graph-node",
99
+ sourceDir: matchingBundle.dirPath
100
+ };
101
+ }
8
102
  /**
9
- * Extract the set of top-level storage element names a contract claims.
10
- *
11
- * Used by the aggregate loader's disjointness check and by
12
- * `projectSchemaToSpace`'s "names owned by other members" walk.
13
- *
14
- * **Stopgap known layering violation.** This helper duck-types the
15
- * storage shape from framework-domain code that has no business naming
16
- * family-specific storage idioms. The framework lacks a typed primitive
17
- * for storage *topology* the structural backbone of "what named things
18
- * does this contract claim?" independent of what those things are.
19
- *
20
- * Behavioural notes for the lifetime of this helper:
103
+ * Resolve a member's head ref, asserting it is present. The apply/verify
104
+ * engine only runs after `checkIntegrity` has refused on `headRefMissing`,
105
+ * so a member reaching the planner / verifier without a head ref is a
106
+ * programming error (the integrity gate was skipped), not a user-facing
107
+ * state. The app member's head ref is always synthesised, so this only
108
+ * ever guards an ungated extension space.
109
+ */
110
+ function requireHeadRef(member) {
111
+ 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.`);
112
+ return member.headRef;
113
+ }
114
+ /**
115
+ * Build a {@link ContractSpaceMember} with lazily-memoised `graph()`,
116
+ * `contract()`, and `contractAt()` facets.
21
117
  *
22
- * - SQL contracts contribute table names from every
23
- * `storage.namespaces[namespaceId].tables` map.
24
- * - Mongo contracts contribute names from each namespace's `tables`
25
- * (or `collections`, depending on the per-target Namespace's slot
26
- * choice). Per-namespace `collections` may appear as a record or as
27
- * an array of `{ name }` entries; both are accepted defensively.
28
- * - Root-level `tables` / `collections` records (when present) are
29
- * also unioned. These root-level walks are belt-and-suspenders for a
30
- * defensive helper operating on `unknown`; no in-tree contract emits
31
- * the root shape post-namespace flip.
32
- * - Unrecognised shapes contribute nothing beyond the walks above.
33
- * - Record-shape detection excludes arrays so array-shaped values aren't
34
- * walked as records via numeric keys.
35
- * - Names that appear in multiple places are deduplicated by the returned
36
- * `Set`.
118
+ * `graph()` reconstructs the migration graph from `packages` on first
119
+ * call and caches it. `contract()` calls `resolveContract` on first call
120
+ * and caches the result; a throwing `resolveContract` (e.g. a missing or
121
+ * undeserializable on-disk contract) re-throws on each call rather than
122
+ * caching a value — `checkIntegrity` surfaces that as `contractUnreadable`.
123
+ * `contractAt()` materializes the contract at an arbitrary graph node with
124
+ * the same resolution order as plan-time ref resolution: ref snapshot first
125
+ * (when `opts.refName` is set), else the matching package's `end-contract.*`.
37
126
  */
38
- function extractStorageElementNames(contract) {
39
- const names = /* @__PURE__ */ new Set();
40
- if (typeof contract !== "object" || contract === null) return names;
41
- const storage = contract.storage;
42
- if (typeof storage !== "object" || storage === null) return names;
43
- const storageObj = storage;
44
- if (typeof storageObj.namespaces === "object" && storageObj.namespaces !== null && !Array.isArray(storageObj.namespaces)) for (const ns of Object.values(storageObj.namespaces)) {
45
- if (typeof ns !== "object" || ns === null) continue;
46
- const nsObj = ns;
47
- addRecordKeys(nsObj.tables, names);
48
- if (Array.isArray(nsObj.collections)) {
49
- for (const entry of nsObj.collections) if (typeof entry === "object" && entry !== null) {
50
- const name = entry.name;
51
- if (typeof name === "string") names.add(name);
52
- }
53
- } else addRecordKeys(nsObj.collections, names);
127
+ function createContractSpaceMember(args) {
128
+ const { spaceId, packages, refs, headRef, refsDir, resolveContract, deserializeContract } = args;
129
+ let graphMemo;
130
+ let contractMemo;
131
+ const contractAtMemo = /* @__PURE__ */ new Map();
132
+ function memberGraph() {
133
+ graphMemo ??= reconstructGraph(packages);
134
+ return graphMemo;
54
135
  }
55
- addRecordKeys(storageObj.tables, names);
56
- addRecordKeys(storageObj.collections, names);
57
- return names;
136
+ return {
137
+ spaceId,
138
+ packages,
139
+ refs,
140
+ headRef,
141
+ graph: memberGraph,
142
+ contract() {
143
+ contractMemo ??= resolveContract();
144
+ return contractMemo;
145
+ },
146
+ async contractAt(hash, opts) {
147
+ const key = contractAtMemoKey(hash, opts?.refName);
148
+ const cached = contractAtMemo.get(key);
149
+ if (cached) return cached;
150
+ const result = await resolveContractAt({
151
+ hash,
152
+ opts,
153
+ refsDir,
154
+ packages,
155
+ graph: memberGraph(),
156
+ deserializeContract
157
+ });
158
+ contractAtMemo.set(key, result);
159
+ return result;
160
+ }
161
+ };
58
162
  }
59
- function addRecordKeys(value, names) {
60
- if (typeof value === "object" && value !== null && !Array.isArray(value)) for (const name of Object.keys(value)) names.add(name);
163
+ /**
164
+ * Assemble a {@link ContractSpaceAggregate} value from its members and a
165
+ * `checkIntegrity` implementation. The query methods (`listSpaces` /
166
+ * `hasSpace` / `space` / `spaces`) are derived here so every aggregate —
167
+ * loader-built or test-built — shares one query surface: `app` first,
168
+ * then `extensions` in the order supplied (the loader sorts them
169
+ * lex-ascending by `spaceId`).
170
+ */
171
+ function createContractSpaceAggregate(args) {
172
+ const { targetId, app, extensions, checkIntegrity } = args;
173
+ const ordered = [app, ...extensions];
174
+ const byId = new Map(ordered.map((m) => [m.spaceId, m]));
175
+ return {
176
+ targetId,
177
+ app,
178
+ extensions,
179
+ listSpaces: () => ordered.map((m) => m.spaceId),
180
+ hasSpace: (id) => byId.has(id),
181
+ space: (id) => byId.get(id),
182
+ spaces: () => ordered,
183
+ checkIntegrity
184
+ };
61
185
  }
62
186
  //#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
- }
187
+ //#region src/aggregate/check-integrity.ts
69
188
  /**
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.
189
+ * Walk the loaded model and return **every** integrity violation — never
190
+ * bailing at the first. Structurally-derivable violations (load-time
191
+ * problems, self-edges, missing / unreachable head refs) are always
192
+ * produced; layout-drift checks require `declaredExtensions`, and
193
+ * contract / target / disjointness checks require `checkContracts`.
87
194
  */
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
195
+ function computeIntegrityViolations(input, opts) {
196
+ const violations = [];
197
+ for (const { member, problems, refProblems, headRefProblem, isApp } of input.spaces) {
198
+ const { spaceId } = member;
199
+ for (const problem of problems) violations.push(loadProblemToViolation(spaceId, problem));
200
+ for (const refProblem of refProblems) violations.push({
201
+ kind: "refUnreadable",
202
+ spaceId,
203
+ refName: refProblem.refName,
204
+ detail: refProblem.detail
205
+ });
206
+ if (headRefProblem !== null) violations.push({
207
+ kind: "refUnreadable",
208
+ spaceId,
209
+ refName: headRefProblem.refName,
210
+ detail: headRefProblem.detail
211
+ });
212
+ for (const pkg of member.packages) {
213
+ const from = pkg.metadata.from ?? "sha256:empty";
214
+ const isSelfEdge = from === pkg.metadata.to;
215
+ const hasDataOp = pkg.ops.some((op) => op.operationClass === "data");
216
+ if (isSelfEdge && !hasDataOp) violations.push({
217
+ kind: "sameSourceAndTarget",
218
+ spaceId,
219
+ dirName: pkg.dirName,
220
+ hash: from
221
+ });
222
+ }
223
+ violations.push(...duplicateMigrationHashViolations(spaceId, member.packages));
224
+ if (!isApp && headRefProblem === null) {
225
+ if (member.headRef === null) violations.push({
226
+ kind: "headRefMissing",
227
+ spaceId
228
+ });
229
+ else if (!headRefPresentInGraph(member, member.headRef.hash)) violations.push({
230
+ kind: "headRefNotInGraph",
231
+ spaceId,
232
+ hash: member.headRef.hash
233
+ });
234
+ }
235
+ }
236
+ if (opts?.declaredExtensions !== void 0) violations.push(...layoutViolations(input.spaces, opts.declaredExtensions));
237
+ if (opts?.checkContracts === true) violations.push(...contractViolations(input));
238
+ return violations;
239
+ }
240
+ function loadProblemToViolation(spaceId, problem) {
241
+ switch (problem.kind) {
242
+ case "hashMismatch": return {
243
+ kind: "hashMismatch",
244
+ spaceId,
245
+ dirName: problem.dirName,
246
+ stored: problem.stored,
247
+ computed: problem.computed
248
+ };
249
+ case "providedInvariantsMismatch": return {
250
+ kind: "providedInvariantsMismatch",
251
+ spaceId,
252
+ dirName: problem.dirName
253
+ };
254
+ case "packageUnloadable": return {
255
+ kind: "packageUnloadable",
256
+ spaceId,
257
+ dirName: problem.dirName,
258
+ detail: problem.detail
259
+ };
260
+ }
261
+ }
262
+ function duplicateMigrationHashViolations(spaceId, packages) {
263
+ const dirNamesByHash = /* @__PURE__ */ new Map();
264
+ for (const pkg of packages) {
265
+ const hash = pkg.metadata.migrationHash;
266
+ const dirNames = dirNamesByHash.get(hash);
267
+ if (dirNames) dirNames.push(pkg.dirName);
268
+ else dirNamesByHash.set(hash, [pkg.dirName]);
269
+ }
270
+ const out = [];
271
+ for (const [migrationHash, dirNames] of dirNamesByHash) if (dirNames.length > 1) out.push({
272
+ kind: "duplicateMigrationHash",
273
+ spaceId,
274
+ migrationHash,
275
+ dirNames: [...dirNames].sort()
101
276
  });
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({
277
+ return out;
278
+ }
279
+ /**
280
+ * Whether a space's head-ref hash is present in its reconstructed graph.
281
+ * An empty graph is reachable only by the empty-contract sentinel.
282
+ */
283
+ function headRefPresentInGraph(member, headHash) {
284
+ const graph = member.graph();
285
+ if (graph.nodes.size === 0) return headHash === EMPTY_CONTRACT_HASH;
286
+ return graph.nodes.has(headHash);
287
+ }
288
+ function layoutViolations(spaces, declaredExtensions) {
289
+ const out = [];
290
+ const extensionSpaceIds = new Set(spaces.filter((s) => !s.isApp).map((s) => s.member.spaceId));
291
+ const declaredIds = new Set(declaredExtensions.map((d) => d.id));
292
+ for (const id of [...extensionSpaceIds].sort()) if (!declaredIds.has(id)) out.push({
107
293
  kind: "orphanSpaceDir",
108
- spaceId: dir
294
+ spaceId: id
109
295
  });
110
- for (const id of [...declaredSpaceIds].sort()) if (!spaceDirSet.has(id)) layoutViolations.push({
296
+ for (const id of [...declaredIds].sort()) if (!extensionSpaceIds.has(id)) out.push({
111
297
  kind: "declaredButUnmigrated",
112
298
  spaceId: id
113
299
  });
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;
300
+ return out;
301
+ }
302
+ function contractViolations(input) {
303
+ const out = [];
304
+ const elementClaimedBy = /* @__PURE__ */ new Map();
305
+ for (const { member } of input.spaces) {
306
+ let contract;
137
307
  try {
138
- spaceContract = input.deserializeContract(spaceContractRaw);
308
+ contract = member.contract();
139
309
  } catch (error) {
140
- return notOk({
141
- kind: "validationFailure",
142
- spaceId: entry.id,
143
- detail: integrityDetail(error)
310
+ out.push({
311
+ kind: "contractUnreadable",
312
+ spaceId: member.spaceId,
313
+ detail: detailOf$1(error)
144
314
  });
315
+ continue;
145
316
  }
146
- if (spaceContract.target !== input.targetId) return notOk({
317
+ if (contract.target !== input.targetId) out.push({
147
318
  kind: "targetMismatch",
148
- spaceId: entry.id,
319
+ spaceId: member.spaceId,
149
320
  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
- }
321
+ actual: contract.target
193
322
  });
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) {
323
+ for (const { entityName: elementName } of elementCoordinates(contract.storage)) {
231
324
  const claimers = elementClaimedBy.get(elementName);
232
325
  if (claimers) claimers.push(member.spaceId);
233
326
  else elementClaimedBy.set(elementName, [member.spaceId]);
234
327
  }
235
328
  }
236
- for (const [element, claimedBy] of elementClaimedBy) if (claimedBy.length > 1) return notOk({
237
- kind: "disjointnessViolation",
329
+ const disjointness = [];
330
+ for (const [element, claimedBy] of elementClaimedBy) if (claimedBy.length > 1) disjointness.push({
331
+ kind: "disjointness",
238
332
  element,
239
333
  claimedBy: [...claimedBy].sort()
240
334
  });
241
- return ok({ aggregate: {
242
- targetId: input.targetId,
243
- app: appMember,
244
- extensions: extensionMembers
245
- } });
335
+ disjointness.sort((a, b) => a.kind === "disjointness" && b.kind === "disjointness" ? a.element.localeCompare(b.element) : 0);
336
+ out.push(...disjointness);
337
+ return out;
338
+ }
339
+ function detailOf$1(error) {
340
+ if (MigrationToolsError.is(error)) return error.why;
341
+ if (error instanceof Error) return error.message;
342
+ return String(error);
343
+ }
344
+ //#endregion
345
+ //#region src/aggregate/loader.ts
346
+ /**
347
+ * Build a tolerant, queryable {@link ContractSpaceAggregate} from on-disk
348
+ * migration state plus the caller's live app contract.
349
+ *
350
+ * Building **never throws on disk content**: a hash- or
351
+ * invariants-mismatched package is retained, an unparseable package is
352
+ * omitted, a missing extension head ref leaves `headRef: null`, and an
353
+ * unreadable on-disk contract defers its failure to `member.contract()`.
354
+ * Every such problem is judged by {@link ContractSpaceAggregate.checkIntegrity}
355
+ * rather than aborting the load. The only rejections are catastrophic I/O
356
+ * (a `migrations/` that exists but is unreadable for reasons other than
357
+ * absence).
358
+ *
359
+ * The app space's head ref is synthesised from the live contract's
360
+ * storage hash (the app contract is authored independently of the
361
+ * migration graph), and `app.contract()` returns the supplied contract.
362
+ * Extension spaces read their contract, refs, and head ref from disk.
363
+ */
364
+ async function loadContractSpaceAggregate(input) {
365
+ const { migrationsDir, deserializeContract, appContract } = input;
366
+ const targetId = appContract.target;
367
+ const appState = await loadAppSpace(migrationsDir, appContract, deserializeContract);
368
+ const extensionStates = await loadExtensionSpaces(migrationsDir, deserializeContract);
369
+ const spaces = [appState, ...extensionStates];
370
+ return createContractSpaceAggregate({
371
+ targetId,
372
+ app: appState.member,
373
+ extensions: extensionStates.map((state) => state.member),
374
+ checkIntegrity: (opts) => computeIntegrityViolations({
375
+ targetId,
376
+ spaces
377
+ }, opts)
378
+ });
379
+ }
380
+ async function loadAppSpace(migrationsDir, appContract, deserializeContract) {
381
+ const spaceDir = spaceMigrationDirectory(migrationsDir, APP_SPACE_ID);
382
+ const { packages, problems } = await readMigrationsDir(spaceDir);
383
+ const { refs, problems: refProblems } = await readRefsTolerant(spaceRefsDirectory(spaceDir));
384
+ return {
385
+ member: createContractSpaceMember({
386
+ spaceId: APP_SPACE_ID,
387
+ packages,
388
+ refs,
389
+ headRef: {
390
+ hash: appContract.storage.storageHash,
391
+ invariants: []
392
+ },
393
+ refsDir: spaceRefsDirectory(spaceDir),
394
+ resolveContract: () => appContract,
395
+ deserializeContract
396
+ }),
397
+ problems,
398
+ refProblems,
399
+ headRefProblem: null,
400
+ isApp: true
401
+ };
402
+ }
403
+ async function loadExtensionSpaces(migrationsDir, deserializeContract) {
404
+ const extensionIds = (await listContractSpaceDirectories(migrationsDir)).filter((name) => name !== APP_SPACE_ID).filter((name) => !RESERVED_SPACE_SUBDIR_NAMES.has(name)).filter(isValidSpaceId).sort();
405
+ const states = [];
406
+ for (const spaceId of extensionIds) states.push(await loadExtensionSpace(migrationsDir, spaceId, deserializeContract));
407
+ return states;
408
+ }
409
+ async function loadExtensionSpace(migrationsDir, spaceId, deserializeContract) {
410
+ const spaceDir = spaceMigrationDirectory(migrationsDir, spaceId);
411
+ const { packages, problems } = await readMigrationsDir(spaceDir);
412
+ const { refs, problems: refProblems } = await readRefsTolerant(spaceRefsDirectory(spaceDir));
413
+ const { headRef, problem: headRefProblem } = await readHeadRefTolerant(migrationsDir, spaceId);
414
+ const rawContract = await readRawContractDeferred(migrationsDir, spaceId);
415
+ return {
416
+ member: createContractSpaceMember({
417
+ spaceId,
418
+ packages,
419
+ refs,
420
+ headRef,
421
+ refsDir: spaceRefsDirectory(spaceDir),
422
+ resolveContract: () => deserializeContract(rawContract()),
423
+ deserializeContract
424
+ }),
425
+ problems,
426
+ refProblems,
427
+ headRefProblem,
428
+ isApp: false
429
+ };
430
+ }
431
+ /**
432
+ * Read an extension's head ref, distinguishing a *genuinely absent*
433
+ * `head.json` (`headRef: null`, no problem — judged `headRefMissing`)
434
+ * from one that *exists but cannot be parsed* (`headRef: null` plus a
435
+ * problem — judged `refUnreadable`, not `headRefMissing`).
436
+ * `readContractSpaceHeadRef` already returns `null` only for ENOENT and
437
+ * throws for unparseable / schema-invalid content, so the throw is the
438
+ * corruption signal. Construction never throws on disk content.
439
+ */
440
+ function isToleratedRefHeadReadError(error) {
441
+ if (MigrationToolsError.is(error)) return true;
442
+ if (!(error instanceof Error)) return false;
443
+ const code = error.code;
444
+ return code === "ENOENT" || code === "EISDIR";
445
+ }
446
+ async function readHeadRefTolerant(migrationsDir, spaceId) {
447
+ try {
448
+ return {
449
+ headRef: await readContractSpaceHeadRef(migrationsDir, spaceId),
450
+ problem: null
451
+ };
452
+ } catch (error) {
453
+ if (!isToleratedRefHeadReadError(error)) throw error;
454
+ return {
455
+ headRef: null,
456
+ problem: {
457
+ refName: HEAD_REF_NAME,
458
+ detail: detailOf(error)
459
+ }
460
+ };
461
+ }
462
+ }
463
+ function detailOf(error) {
464
+ return error instanceof Error ? error.message : String(error);
465
+ }
466
+ /**
467
+ * Read the raw on-disk contract eagerly (cheap I/O) but defer its
468
+ * (throwing) failure to call time, so a missing or unparseable
469
+ * `contract.json` becomes a `contract()` throw — surfaced as
470
+ * `contractUnreadable` — rather than a construction failure.
471
+ */
472
+ async function readRawContractDeferred(migrationsDir, spaceId) {
473
+ try {
474
+ const raw = await readContractSpaceContract(migrationsDir, spaceId);
475
+ return () => raw;
476
+ } catch (error) {
477
+ return () => {
478
+ throw error;
479
+ };
480
+ }
246
481
  }
247
482
  //#endregion
248
483
  //#region src/aggregate/strategies/graph-walk.ts
@@ -263,11 +498,13 @@ async function loadContractSpaceAggregate(input) {
263
498
  */
264
499
  function graphWalkStrategy(input) {
265
500
  const { aggregateTargetId, member, currentMarker, refName } = input;
266
- const { graph, packagesByMigrationHash } = member.migrations;
501
+ const headRef = requireHeadRef(member);
502
+ const graph = member.graph();
503
+ const packagesByMigrationHash = new Map(member.packages.map((pkg) => [pkg.metadata.migrationHash, pkg]));
267
504
  const fromHash = currentMarker?.storageHash ?? "sha256:empty";
268
505
  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, {
506
+ const required = new Set(headRef.invariants.filter((id) => !markerInvariants.has(id)));
507
+ const outcome = findPathWithDecision(graph, fromHash, headRef.hash, {
271
508
  required,
272
509
  ...refName !== void 0 ? { refName } : {}
273
510
  });
@@ -299,12 +536,12 @@ function graphWalkStrategy(input) {
299
536
  targetId: aggregateTargetId,
300
537
  spaceId: member.spaceId,
301
538
  origin: currentMarker === null ? null : { storageHash: currentMarker.storageHash },
302
- destination: { storageHash: member.headRef.hash },
539
+ destination: { storageHash: headRef.hash },
303
540
  operations: pathOps,
304
541
  providedInvariants: [...providedInvariantsSet].sort()
305
542
  },
306
543
  displayOps: pathOps,
307
- destinationContract: member.contract,
544
+ destinationContract: member.contract(),
308
545
  strategy: "graph-walk",
309
546
  migrationEdges: edgeRefs,
310
547
  pathDecision: outcome.decision
@@ -377,16 +614,11 @@ function projectSchemaToSpace(schema, member, otherMembers) {
377
614
  if (typeof schemaObj.collections === "object" && schemaObj.collections !== null && !Array.isArray(schemaObj.collections)) return pruneRecord(schemaObj, "collections", ownedByOthers);
378
615
  return schema;
379
616
  }
380
- /**
381
- * Collect the set of storage element names claimed by other members.
382
- * Reuses the loader's `extractStorageElementNames` helper so the
383
- * tables/collections walk lives in exactly one place.
384
- */
385
617
  function collectOwnedNames(member, otherMembers) {
386
618
  const owned = /* @__PURE__ */ new Set();
387
619
  for (const other of otherMembers) {
388
620
  if (other.spaceId === member.spaceId) continue;
389
- for (const name of extractStorageElementNames(other.contract)) owned.add(name);
621
+ for (const { entityName } of elementCoordinates(other.contract().storage)) owned.add(entityName);
390
622
  }
391
623
  return owned;
392
624
  }
@@ -423,6 +655,17 @@ function pruneCollectionsArray(schemaObj, ownedByOthers) {
423
655
  };
424
656
  }
425
657
  //#endregion
658
+ //#region src/aggregate/synth-migration-edge.ts
659
+ function buildSynthMigrationEdge(args) {
660
+ return {
661
+ dirName: "",
662
+ migrationHash: args.destinationStorageHash,
663
+ from: args.currentMarkerStorageHash ?? "",
664
+ to: args.destinationStorageHash,
665
+ operationCount: args.operationCount
666
+ };
667
+ }
668
+ //#endregion
426
669
  //#region src/aggregate/strategies/synth.ts
427
670
  /**
428
671
  * Synthesise a migration plan for a single member by projecting the
@@ -450,7 +693,7 @@ function pruneCollectionsArray(schemaObj, ownedByOthers) {
450
693
  async function synthStrategy(input) {
451
694
  const projectedSchema = projectSchemaToSpace(input.schemaIntrospection, input.member, input.otherMembers);
452
695
  const plannerResult = await input.migrations.createPlanner(input.familyInstance).plan({
453
- contract: input.member.contract,
696
+ contract: input.member.contract(),
454
697
  schema: projectedSchema,
455
698
  policy: input.operationPolicy,
456
699
  fromContract: null,
@@ -462,22 +705,29 @@ async function synthStrategy(input) {
462
705
  conflicts: plannerResult.conflicts
463
706
  };
464
707
  const synthedPlan = plannerResult.plan;
708
+ const plan = new Proxy(synthedPlan, {
709
+ get(target, prop) {
710
+ if (prop === "targetId") return input.aggregateTargetId;
711
+ return Reflect.get(target, prop, target);
712
+ },
713
+ has(target, prop) {
714
+ if (prop === "targetId") return true;
715
+ return Reflect.has(target, prop);
716
+ }
717
+ });
718
+ const destinationStorageHash = synthedPlan.destination.storageHash;
465
719
  return {
466
720
  kind: "ok",
467
721
  result: {
468
- plan: new Proxy(synthedPlan, {
469
- get(target, prop) {
470
- if (prop === "targetId") return input.aggregateTargetId;
471
- return Reflect.get(target, prop, target);
472
- },
473
- has(target, prop) {
474
- if (prop === "targetId") return true;
475
- return Reflect.has(target, prop);
476
- }
477
- }),
722
+ plan,
478
723
  displayOps: synthedPlan.operations,
479
- destinationContract: input.member.contract,
480
- strategy: "synth"
724
+ destinationContract: input.member.contract(),
725
+ strategy: "synth",
726
+ migrationEdges: [buildSynthMigrationEdge({
727
+ currentMarkerStorageHash: input.currentMarker?.storageHash,
728
+ destinationStorageHash,
729
+ operationCount: synthedPlan.operations.length
730
+ })]
481
731
  }
482
732
  };
483
733
  }
@@ -491,7 +741,7 @@ async function synthStrategy(input) {
491
741
  * 1. If `callerPolicy.ignoreGraphFor.has(member.spaceId)`:
492
742
  * - If `member.headRef.invariants` is empty → synth.
493
743
  * - Else → `policyConflict` (synth cannot satisfy authored invariants).
494
- * 2. Else if `member.migrations.graph` is non-empty AND graph-walk
744
+ * 2. Else if `member.graph()` is non-empty AND graph-walk
495
745
  * succeeds → graph-walk.
496
746
  * 3. Else if `member.headRef.invariants` is empty → synth.
497
747
  * 4. Else → graph-walk failure → `extensionPathUnreachable` /
@@ -500,12 +750,12 @@ async function synthStrategy(input) {
500
750
  * Output `applyOrder` is `[...aggregate.extensions.map(spaceId), aggregate.app.spaceId]`
501
751
  * — extensions alphabetical, then app — matching today's
502
752
  * `concatenateSpaceApplyInputs` ordering. This preserves
503
- * `MultiSpaceRunnerFailure.failingSpace` attribution byte-for-byte.
753
+ * `MigrationRunnerFailure.failingSpace` attribution byte-for-byte.
504
754
  *
505
755
  * Every emitted `MigrationPlan` has `targetId = aggregate.targetId`.
506
756
  * No placeholder cast; no patch step.
507
757
  */
508
- async function planAggregate(input) {
758
+ async function planMigration(input) {
509
759
  const { aggregate, currentDBState, callerPolicy } = input;
510
760
  const allMembers = [aggregate.app, ...aggregate.extensions];
511
761
  const perSpace = /* @__PURE__ */ new Map();
@@ -513,16 +763,18 @@ async function planAggregate(input) {
513
763
  for (const member of orderedMembers) {
514
764
  const otherMembers = allMembers.filter((m) => m.spaceId !== member.spaceId);
515
765
  const currentMarker = currentDBState.markersBySpaceId.get(member.spaceId) ?? null;
766
+ const headRef = requireHeadRef(member);
516
767
  const ignoreGraph = callerPolicy.ignoreGraphFor.has(member.spaceId);
517
- const invariantsRequired = member.headRef.invariants.length > 0;
768
+ const invariantsRequired = headRef.invariants.length > 0;
518
769
  if (ignoreGraph && invariantsRequired) return notOk({
519
770
  kind: "policyConflict",
520
771
  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.`
772
+ 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
773
  });
523
774
  if (ignoreGraph) {
524
775
  const synthOutcome = await synthStrategy({
525
776
  aggregateTargetId: aggregate.targetId,
777
+ currentMarker,
526
778
  member,
527
779
  otherMembers,
528
780
  schemaIntrospection: currentDBState.schemaIntrospection,
@@ -539,7 +791,7 @@ async function planAggregate(input) {
539
791
  perSpace.set(member.spaceId, synthOutcome.result);
540
792
  continue;
541
793
  }
542
- if (member.migrations.graph.nodes.size > 0) {
794
+ if (member.graph().nodes.size > 0) {
543
795
  const walked = graphWalkStrategy({
544
796
  aggregateTargetId: aggregate.targetId,
545
797
  member,
@@ -552,7 +804,7 @@ async function planAggregate(input) {
552
804
  if (walked.kind === "unreachable") return notOk({
553
805
  kind: "extensionPathUnreachable",
554
806
  spaceId: member.spaceId,
555
- target: member.headRef.hash
807
+ target: headRef.hash
556
808
  });
557
809
  return notOk({
558
810
  kind: "extensionPathUnsatisfiable",
@@ -563,10 +815,11 @@ async function planAggregate(input) {
563
815
  if (invariantsRequired) return notOk({
564
816
  kind: "extensionPathUnsatisfiable",
565
817
  spaceId: member.spaceId,
566
- missingInvariants: [...member.headRef.invariants].sort()
818
+ missingInvariants: [...headRef.invariants].sort()
567
819
  });
568
820
  const synthOutcome = await synthStrategy({
569
821
  aggregateTargetId: aggregate.targetId,
822
+ currentMarker,
570
823
  member,
571
824
  otherMembers,
572
825
  schemaIntrospection: currentDBState.schemaIntrospection,
@@ -611,9 +864,9 @@ async function planAggregate(input) {
611
864
  * Pure synchronous function; no I/O. The caller (CLI) gathers
612
865
  * `markersBySpaceId` and `schemaIntrospection` ahead of the call.
613
866
  */
614
- function verifyAggregate(input) {
867
+ function verifyMigration(input) {
615
868
  try {
616
- return runVerifyAggregate(input);
869
+ return runVerifyMigration(input);
617
870
  } catch (error) {
618
871
  return notOk({
619
872
  kind: "introspectionFailure",
@@ -621,7 +874,7 @@ function verifyAggregate(input) {
621
874
  });
622
875
  }
623
876
  }
624
- function runVerifyAggregate(input) {
877
+ function runVerifyMigration(input) {
625
878
  const { aggregate, markersBySpaceId, schemaIntrospection, mode, verifySchemaForMember } = input;
626
879
  const allMembers = [aggregate.app, ...aggregate.extensions];
627
880
  const memberSpaceIds = new Set(allMembers.map((m) => m.spaceId));
@@ -632,16 +885,17 @@ function runVerifyAggregate(input) {
632
885
  markerPerSpace.set(member.spaceId, { kind: "absent" });
633
886
  continue;
634
887
  }
635
- if (marker.storageHash !== member.headRef.hash) {
888
+ const headRef = requireHeadRef(member);
889
+ if (marker.storageHash !== headRef.hash) {
636
890
  markerPerSpace.set(member.spaceId, {
637
891
  kind: "hashMismatch",
638
892
  markerHash: marker.storageHash,
639
- expected: member.headRef.hash
893
+ expected: headRef.hash
640
894
  });
641
895
  continue;
642
896
  }
643
897
  const markerInvariants = new Set(marker.invariants);
644
- const missing = member.headRef.invariants.filter((id) => !markerInvariants.has(id));
898
+ const missing = headRef.invariants.filter((id) => !markerInvariants.has(id));
645
899
  if (missing.length > 0) {
646
900
  markerPerSpace.set(member.spaceId, {
647
901
  kind: "missingInvariants",
@@ -684,7 +938,10 @@ function detectOrphanElements(schemaIntrospection, members) {
684
938
  const liveTables = schemaIntrospection.tables;
685
939
  if (typeof liveTables !== "object" || liveTables === null) return [];
686
940
  const claimedTables = /* @__PURE__ */ new Set();
687
- for (const member of members) for (const name of extractStorageElementNames(member.contract)) claimedTables.add(name);
941
+ for (const member of members) {
942
+ const contract = member.contract();
943
+ for (const { entityName } of elementCoordinates(contract.storage)) claimedTables.add(entityName);
944
+ }
688
945
  const orphans = [];
689
946
  for (const tableName of Object.keys(liveTables)) if (!claimedTables.has(tableName)) orphans.push({
690
947
  kind: "table",
@@ -694,6 +951,6 @@ function detectOrphanElements(schemaIntrospection, members) {
694
951
  return orphans;
695
952
  }
696
953
  //#endregion
697
- export { graphWalkStrategy, loadContractSpaceAggregate, planAggregate, projectSchemaToSpace, verifyAggregate };
954
+ export { buildSynthMigrationEdge, computeIntegrityViolations, createContractSpaceAggregate, createContractSpaceMember, graphWalkStrategy, loadContractSpaceAggregate, loadProblemToViolation, planMigration, projectSchemaToSpace, requireHeadRef, verifyMigration };
698
955
 
699
956
  //# sourceMappingURL=aggregate.mjs.map