@prisma-next/migration-tools 0.5.0-dev.9 → 0.5.1

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 (130) hide show
  1. package/README.md +34 -22
  2. package/dist/{constants-BRi0X7B_.mjs → constants-DWV9_o2Z.mjs} +2 -2
  3. package/dist/{constants-BRi0X7B_.mjs.map → constants-DWV9_o2Z.mjs.map} +1 -1
  4. package/dist/errors-EPL_9p9f.mjs +297 -0
  5. package/dist/errors-EPL_9p9f.mjs.map +1 -0
  6. package/dist/exports/aggregate.d.mts +599 -0
  7. package/dist/exports/aggregate.d.mts.map +1 -0
  8. package/dist/exports/aggregate.mjs +599 -0
  9. package/dist/exports/aggregate.mjs.map +1 -0
  10. package/dist/exports/constants.d.mts.map +1 -1
  11. package/dist/exports/constants.mjs +2 -3
  12. package/dist/exports/errors.d.mts +68 -0
  13. package/dist/exports/errors.d.mts.map +1 -0
  14. package/dist/exports/errors.mjs +2 -0
  15. package/dist/exports/graph.d.mts +2 -0
  16. package/dist/exports/graph.mjs +1 -0
  17. package/dist/exports/hash.d.mts +52 -0
  18. package/dist/exports/hash.d.mts.map +1 -0
  19. package/dist/exports/hash.mjs +2 -0
  20. package/dist/exports/invariants.d.mts +39 -0
  21. package/dist/exports/invariants.d.mts.map +1 -0
  22. package/dist/exports/invariants.mjs +2 -0
  23. package/dist/exports/io.d.mts +66 -6
  24. package/dist/exports/io.d.mts.map +1 -1
  25. package/dist/exports/io.mjs +2 -3
  26. package/dist/exports/metadata.d.mts +2 -0
  27. package/dist/exports/metadata.mjs +1 -0
  28. package/dist/exports/migration-graph.d.mts +2 -0
  29. package/dist/exports/migration-graph.mjs +2 -0
  30. package/dist/exports/migration-ts.d.mts.map +1 -1
  31. package/dist/exports/migration-ts.mjs +2 -4
  32. package/dist/exports/migration-ts.mjs.map +1 -1
  33. package/dist/exports/migration.d.mts +15 -14
  34. package/dist/exports/migration.d.mts.map +1 -1
  35. package/dist/exports/migration.mjs +70 -43
  36. package/dist/exports/migration.mjs.map +1 -1
  37. package/dist/exports/package.d.mts +3 -0
  38. package/dist/exports/package.mjs +1 -0
  39. package/dist/exports/refs.d.mts.map +1 -1
  40. package/dist/exports/refs.mjs +3 -4
  41. package/dist/exports/refs.mjs.map +1 -1
  42. package/dist/exports/spaces.d.mts +526 -0
  43. package/dist/exports/spaces.d.mts.map +1 -0
  44. package/dist/exports/spaces.mjs +266 -0
  45. package/dist/exports/spaces.mjs.map +1 -0
  46. package/dist/graph-HMWAldoR.d.mts +28 -0
  47. package/dist/graph-HMWAldoR.d.mts.map +1 -0
  48. package/dist/hash-By50zM_E.mjs +74 -0
  49. package/dist/hash-By50zM_E.mjs.map +1 -0
  50. package/dist/invariants-qgQGlsrV.mjs +57 -0
  51. package/dist/invariants-qgQGlsrV.mjs.map +1 -0
  52. package/dist/io-D5YYptRO.mjs +239 -0
  53. package/dist/io-D5YYptRO.mjs.map +1 -0
  54. package/dist/metadata-CFvm3ayn.d.mts +2 -0
  55. package/dist/migration-graph-DGNnKDY5.mjs +523 -0
  56. package/dist/migration-graph-DGNnKDY5.mjs.map +1 -0
  57. package/dist/migration-graph-DulOITvG.d.mts +124 -0
  58. package/dist/migration-graph-DulOITvG.d.mts.map +1 -0
  59. package/dist/op-schema-D5qkXfEf.mjs +13 -0
  60. package/dist/op-schema-D5qkXfEf.mjs.map +1 -0
  61. package/dist/package-BjiZ7KDy.d.mts +21 -0
  62. package/dist/package-BjiZ7KDy.d.mts.map +1 -0
  63. package/dist/read-contract-space-contract-Cme8KZk_.mjs +259 -0
  64. package/dist/read-contract-space-contract-Cme8KZk_.mjs.map +1 -0
  65. package/package.json +42 -17
  66. package/src/aggregate/loader.ts +379 -0
  67. package/src/aggregate/marker-types.ts +16 -0
  68. package/src/aggregate/planner-types.ts +171 -0
  69. package/src/aggregate/planner.ts +159 -0
  70. package/src/aggregate/project-schema-to-space.ts +64 -0
  71. package/src/aggregate/strategies/graph-walk.ts +118 -0
  72. package/src/aggregate/strategies/synth.ts +122 -0
  73. package/src/aggregate/types.ts +89 -0
  74. package/src/aggregate/verifier.ts +230 -0
  75. package/src/assert-descriptor-self-consistency.ts +70 -0
  76. package/src/compute-extension-space-apply-path.ts +152 -0
  77. package/src/concatenate-space-apply-inputs.ts +90 -0
  78. package/src/contract-space-from-json.ts +63 -0
  79. package/src/emit-contract-space-artefacts.ts +70 -0
  80. package/src/errors.ts +251 -17
  81. package/src/exports/aggregate.ts +42 -0
  82. package/src/exports/errors.ts +8 -0
  83. package/src/exports/graph.ts +1 -0
  84. package/src/exports/hash.ts +2 -0
  85. package/src/exports/invariants.ts +1 -0
  86. package/src/exports/io.ts +3 -1
  87. package/src/exports/metadata.ts +1 -0
  88. package/src/exports/{dag.ts → migration-graph.ts} +3 -2
  89. package/src/exports/migration.ts +0 -1
  90. package/src/exports/package.ts +2 -0
  91. package/src/exports/spaces.ts +45 -0
  92. package/src/gather-disk-contract-space-state.ts +62 -0
  93. package/src/graph-ops.ts +57 -30
  94. package/src/graph.ts +25 -0
  95. package/src/hash.ts +91 -0
  96. package/src/invariants.ts +61 -0
  97. package/src/io.ts +163 -40
  98. package/src/metadata.ts +1 -0
  99. package/src/migration-base.ts +97 -56
  100. package/src/migration-graph.ts +676 -0
  101. package/src/op-schema.ts +11 -0
  102. package/src/package.ts +21 -0
  103. package/src/plan-all-spaces.ts +76 -0
  104. package/src/read-contract-space-contract.ts +44 -0
  105. package/src/read-contract-space-head-ref.ts +63 -0
  106. package/src/space-layout.ts +48 -0
  107. package/src/verify-contract-spaces.ts +272 -0
  108. package/dist/attestation-BnzTb0Qp.mjs +0 -65
  109. package/dist/attestation-BnzTb0Qp.mjs.map +0 -1
  110. package/dist/errors-BmiSgz1j.mjs +0 -160
  111. package/dist/errors-BmiSgz1j.mjs.map +0 -1
  112. package/dist/exports/attestation.d.mts +0 -37
  113. package/dist/exports/attestation.d.mts.map +0 -1
  114. package/dist/exports/attestation.mjs +0 -4
  115. package/dist/exports/dag.d.mts +0 -51
  116. package/dist/exports/dag.d.mts.map +0 -1
  117. package/dist/exports/dag.mjs +0 -386
  118. package/dist/exports/dag.mjs.map +0 -1
  119. package/dist/exports/types.d.mts +0 -35
  120. package/dist/exports/types.d.mts.map +0 -1
  121. package/dist/exports/types.mjs +0 -3
  122. package/dist/io-Cd6GLyjK.mjs +0 -153
  123. package/dist/io-Cd6GLyjK.mjs.map +0 -1
  124. package/dist/types-DyGXcWWp.d.mts +0 -71
  125. package/dist/types-DyGXcWWp.d.mts.map +0 -1
  126. package/src/attestation.ts +0 -81
  127. package/src/dag.ts +0 -426
  128. package/src/exports/attestation.ts +0 -2
  129. package/src/exports/types.ts +0 -10
  130. package/src/types.ts +0 -66
@@ -0,0 +1,599 @@
1
+ import { s as readMigrationsDir } from "../io-D5YYptRO.mjs";
2
+ import "../constants-DWV9_o2Z.mjs";
3
+ import { l as reconstructGraph, o as findPathWithDecision } from "../migration-graph-DGNnKDY5.mjs";
4
+ import { a as APP_SPACE_ID, c as spaceMigrationDirectory, i as readContractSpaceHeadRef, n as listContractSpaceDirectories, t as readContractSpaceContract } from "../read-contract-space-contract-Cme8KZk_.mjs";
5
+ import { notOk, ok } from "@prisma-next/utils/result";
6
+ //#region src/aggregate/loader.ts
7
+ /**
8
+ * Hydrate a {@link ContractSpaceAggregate} from on-disk state and
9
+ * the app contract value the caller supplies.
10
+ *
11
+ * The loader is the **only** descriptor-import boundary at apply /
12
+ * verify time, but it intentionally does **not** read the extension
13
+ * descriptor's `contractJson` value. Each extension space's contract
14
+ * is read from its on-disk `migrations/<id>/contract.json` mirror; the
15
+ * descriptor's role is exhausted by the seed phase that wrote that
16
+ * mirror in the first place. The loader composes existing
17
+ * migration-tools primitives — layout precheck (via
18
+ * {@link listContractSpaceDirectories}), integrity checks (via
19
+ * {@link readMigrationsDir} / {@link readContractSpaceHeadRef} /
20
+ * {@link readContractSpaceContract} / `validateContract`), and
21
+ * disjointness — into a single typed value.
22
+ *
23
+ * Failure semantics: every failure variant in {@link LoadAggregateError}
24
+ * short-circuits the load.
25
+ */
26
+ async function loadContractSpaceAggregate(input) {
27
+ const appContractTarget = input.appContract.target;
28
+ if (appContractTarget !== input.targetId) return notOk({
29
+ kind: "targetMismatch",
30
+ spaceId: APP_SPACE_ID,
31
+ expected: input.targetId,
32
+ actual: appContractTarget
33
+ });
34
+ for (const entry of input.declaredExtensions) if (entry.targetId !== input.targetId) return notOk({
35
+ kind: "targetMismatch",
36
+ spaceId: entry.id,
37
+ expected: input.targetId,
38
+ actual: entry.targetId
39
+ });
40
+ const declaredSpaceIds = new Set(input.declaredExtensions.map((e) => e.id));
41
+ const extensionDirsOnDisk = (await listContractSpaceDirectories(input.migrationsDir)).filter((d) => d !== APP_SPACE_ID);
42
+ const spaceDirSet = new Set(extensionDirsOnDisk);
43
+ const layoutViolations = [];
44
+ for (const dir of extensionDirsOnDisk) if (!declaredSpaceIds.has(dir)) layoutViolations.push({
45
+ kind: "orphanSpaceDir",
46
+ spaceId: dir
47
+ });
48
+ for (const id of [...declaredSpaceIds].sort()) if (!spaceDirSet.has(id)) layoutViolations.push({
49
+ kind: "declaredButUnmigrated",
50
+ spaceId: id
51
+ });
52
+ if (layoutViolations.length > 0) return notOk({
53
+ kind: "layoutViolation",
54
+ violations: layoutViolations
55
+ });
56
+ const loadedExtensions = [];
57
+ for (const entry of [...input.declaredExtensions].sort((a, b) => a.id.localeCompare(b.id))) {
58
+ const headRef = await readContractSpaceHeadRef(input.migrationsDir, entry.id);
59
+ if (headRef === null) return notOk({
60
+ kind: "integrityFailure",
61
+ spaceId: entry.id,
62
+ detail: `Head ref \`refs/head.json\` is missing for extension space "${entry.id}".`
63
+ });
64
+ let spaceContractRaw;
65
+ try {
66
+ spaceContractRaw = await readContractSpaceContract(input.migrationsDir, entry.id);
67
+ } catch (error) {
68
+ return notOk({
69
+ kind: "integrityFailure",
70
+ spaceId: entry.id,
71
+ detail: error instanceof Error ? error.message : String(error)
72
+ });
73
+ }
74
+ let spaceContract;
75
+ try {
76
+ spaceContract = input.validateContract(spaceContractRaw);
77
+ } catch (error) {
78
+ return notOk({
79
+ kind: "validationFailure",
80
+ spaceId: entry.id,
81
+ detail: error instanceof Error ? error.message : String(error)
82
+ });
83
+ }
84
+ if (spaceContract.target !== input.targetId) return notOk({
85
+ kind: "targetMismatch",
86
+ spaceId: entry.id,
87
+ expected: input.targetId,
88
+ actual: spaceContract.target
89
+ });
90
+ let packages;
91
+ try {
92
+ packages = await readMigrationsDir(spaceMigrationDirectory(input.migrationsDir, entry.id));
93
+ } catch (error) {
94
+ return notOk({
95
+ kind: "integrityFailure",
96
+ spaceId: entry.id,
97
+ detail: error instanceof Error ? error.message : String(error)
98
+ });
99
+ }
100
+ let graph;
101
+ try {
102
+ graph = reconstructGraph(packages);
103
+ } catch (error) {
104
+ return notOk({
105
+ kind: "integrityFailure",
106
+ spaceId: entry.id,
107
+ detail: error instanceof Error ? error.message : String(error)
108
+ });
109
+ }
110
+ if (graph.nodes.size === 0) {
111
+ if (headRef.hash !== "sha256:empty") return notOk({
112
+ kind: "integrityFailure",
113
+ spaceId: entry.id,
114
+ detail: `Head ref "${headRef.hash}" is not present in the (empty) on-disk migration graph.`
115
+ });
116
+ } else if (!graph.nodes.has(headRef.hash)) return notOk({
117
+ kind: "integrityFailure",
118
+ spaceId: entry.id,
119
+ detail: `Head ref "${headRef.hash}" is not present in the on-disk migration graph.`
120
+ });
121
+ const packagesByMigrationHash = new Map(packages.map((p) => [p.metadata.migrationHash, p]));
122
+ loadedExtensions.push({
123
+ entry,
124
+ contract: spaceContract,
125
+ headRefHash: headRef.hash,
126
+ headRefInvariants: [...headRef.invariants].sort(),
127
+ migrations: {
128
+ graph,
129
+ packagesByMigrationHash
130
+ }
131
+ });
132
+ }
133
+ let appGraph;
134
+ try {
135
+ appGraph = reconstructGraph(input.appMigrationPackages);
136
+ } catch (error) {
137
+ return notOk({
138
+ kind: "integrityFailure",
139
+ spaceId: APP_SPACE_ID,
140
+ detail: error instanceof Error ? error.message : String(error)
141
+ });
142
+ }
143
+ const appPackagesByMigrationHash = new Map(input.appMigrationPackages.map((p) => [p.metadata.migrationHash, p]));
144
+ const appMember = {
145
+ spaceId: APP_SPACE_ID,
146
+ contract: input.appContract,
147
+ headRef: {
148
+ hash: input.appContract.storage.storageHash,
149
+ invariants: []
150
+ },
151
+ migrations: {
152
+ graph: appGraph,
153
+ packagesByMigrationHash: appPackagesByMigrationHash
154
+ }
155
+ };
156
+ const extensionMembers = loadedExtensions.map((s) => ({
157
+ spaceId: s.entry.id,
158
+ contract: s.contract,
159
+ headRef: {
160
+ hash: s.headRefHash,
161
+ invariants: s.headRefInvariants
162
+ },
163
+ migrations: s.migrations
164
+ }));
165
+ const elementClaimedBy = /* @__PURE__ */ new Map();
166
+ for (const member of [appMember, ...extensionMembers]) {
167
+ const tables = extractTableNames(member.contract);
168
+ for (const tableName of tables) {
169
+ const claimers = elementClaimedBy.get(tableName);
170
+ if (claimers) claimers.push(member.spaceId);
171
+ else elementClaimedBy.set(tableName, [member.spaceId]);
172
+ }
173
+ }
174
+ for (const [element, claimedBy] of elementClaimedBy) if (claimedBy.length > 1) return notOk({
175
+ kind: "disjointnessViolation",
176
+ element,
177
+ claimedBy: [...claimedBy].sort()
178
+ });
179
+ return ok({ aggregate: {
180
+ targetId: input.targetId,
181
+ app: appMember,
182
+ extensions: extensionMembers
183
+ } });
184
+ }
185
+ /**
186
+ * Extract the set of top-level storage table names from a contract.
187
+ * Duck-typed: returns `[]` if the contract's storage shape doesn't
188
+ * match the canonical `storage.tables: Record<string, ...>` form. A
189
+ * future family with a different storage shape gets disjointness
190
+ * effectively disabled (not enforced) rather than a hard failure.
191
+ */
192
+ function extractTableNames(contract) {
193
+ const storage = contract.storage;
194
+ if (typeof storage !== "object" || storage === null) return [];
195
+ const tables = storage.tables;
196
+ if (typeof tables !== "object" || tables === null) return [];
197
+ return Object.keys(tables);
198
+ }
199
+ //#endregion
200
+ //#region src/aggregate/strategies/graph-walk.ts
201
+ /**
202
+ * Walk a member's hydrated migration graph from the live marker to
203
+ * `member.headRef.hash`, covering every required invariant.
204
+ *
205
+ * Pure synchronous function — no I/O. The aggregate's loader has
206
+ * already integrity-checked every package and reconstructed the graph;
207
+ * this strategy just looks up ops by `migrationHash` and assembles a
208
+ * `MigrationPlan` with `targetId` set from the aggregate (no
209
+ * placeholder cast).
210
+ *
211
+ * Required invariants are computed as `headRef.invariants \ marker.invariants`
212
+ * — the marker already declares some invariants satisfied; the path
213
+ * only needs to provide the remainder. Mirrors today's
214
+ * `computeExtensionSpaceApplyPath` semantics.
215
+ */
216
+ function graphWalkStrategy(input) {
217
+ const { aggregateTargetId, member, currentMarker, refName } = input;
218
+ const { graph, packagesByMigrationHash } = member.migrations;
219
+ const fromHash = currentMarker?.storageHash ?? "sha256:empty";
220
+ const markerInvariants = new Set(currentMarker?.invariants ?? []);
221
+ const required = new Set(member.headRef.invariants.filter((id) => !markerInvariants.has(id)));
222
+ const outcome = findPathWithDecision(graph, fromHash, member.headRef.hash, {
223
+ required,
224
+ ...refName !== void 0 ? { refName } : {}
225
+ });
226
+ if (outcome.kind === "unreachable") return { kind: "unreachable" };
227
+ if (outcome.kind === "unsatisfiable") return {
228
+ kind: "unsatisfiable",
229
+ missing: outcome.missing
230
+ };
231
+ const pathOps = [];
232
+ const providedInvariantsSet = /* @__PURE__ */ new Set();
233
+ const edgeRefs = [];
234
+ for (const edge of outcome.decision.selectedPath) {
235
+ const pkg = packagesByMigrationHash.get(edge.migrationHash);
236
+ if (!pkg) throw new Error(`Migration package missing for edge ${edge.migrationHash} in space "${member.spaceId}". The hydrated migration graph and packagesByMigrationHash map are out of sync — this should be unreachable; report.`);
237
+ for (const op of pkg.ops) pathOps.push(op);
238
+ for (const invariant of pkg.metadata.providedInvariants) providedInvariantsSet.add(invariant);
239
+ edgeRefs.push({
240
+ migrationHash: edge.migrationHash,
241
+ dirName: edge.dirName,
242
+ from: edge.from,
243
+ to: edge.to,
244
+ operationCount: pkg.ops.length
245
+ });
246
+ }
247
+ return {
248
+ kind: "ok",
249
+ result: {
250
+ plan: {
251
+ targetId: aggregateTargetId,
252
+ spaceId: member.spaceId,
253
+ origin: currentMarker === null ? null : { storageHash: currentMarker.storageHash },
254
+ destination: { storageHash: member.headRef.hash },
255
+ operations: pathOps,
256
+ providedInvariants: [...providedInvariantsSet].sort()
257
+ },
258
+ displayOps: pathOps,
259
+ destinationContract: member.contract,
260
+ strategy: "graph-walk",
261
+ migrationEdges: edgeRefs,
262
+ pathDecision: outcome.decision
263
+ }
264
+ };
265
+ }
266
+ //#endregion
267
+ //#region src/aggregate/project-schema-to-space.ts
268
+ /**
269
+ * Project the introspected live schema to the slice claimed by a single
270
+ * contract-space member.
271
+ *
272
+ * Returns the same `schema` value with every top-level table claimed by
273
+ * **other** members of the aggregate removed. Tables not claimed by any
274
+ * member flow through unchanged — the planner / verifier sees them as
275
+ * orphans (extras in strict mode).
276
+ *
277
+ * Used by:
278
+ *
279
+ * - The aggregate planner's **synth strategy**: when synthesising a
280
+ * plan against a member's contract, the live schema must be projected
281
+ * to that member's slice so the planner doesn't treat tables claimed
282
+ * by other members as "extras" and emit destructive ops to drop
283
+ * them.
284
+ * - The aggregate verifier's **schemaCheck**: projects per member so the
285
+ * single-contract `verifySqlSchema` only sees the slice claimed by
286
+ * the member it is checking. Closes the F23 architectural concern
287
+ * (multi-member deployments where each member's tables look like
288
+ * extras to every other member's verify pass).
289
+ *
290
+ * **Duck-typing semantics**: the helper operates on `unknown` for the
291
+ * schema and falls through structurally if the shape doesn't match.
292
+ * Every family today exposes `storage.tables: Record<string, ...>` and
293
+ * the introspected schema mirrors the same shape; a future family with
294
+ * a different storage shape gets the schema returned unchanged rather
295
+ * than blowing up the aggregate planner.
296
+ */
297
+ function projectSchemaToSpace(schema, member, otherMembers) {
298
+ if (typeof schema !== "object" || schema === null) return schema;
299
+ const schemaObj = schema;
300
+ if (typeof schemaObj.tables !== "object" || schemaObj.tables === null) return schema;
301
+ const schemaTables = schemaObj.tables;
302
+ const ownedByOthers = /* @__PURE__ */ new Set();
303
+ for (const other of otherMembers) {
304
+ if (other.spaceId === member.spaceId) continue;
305
+ const storage = other.contract.storage;
306
+ if (typeof storage !== "object" || storage === null) continue;
307
+ const tables = storage.tables;
308
+ if (typeof tables !== "object" || tables === null) continue;
309
+ for (const tableName of Object.keys(tables)) ownedByOthers.add(tableName);
310
+ }
311
+ if (ownedByOthers.size === 0) return schema;
312
+ const prunedTables = {};
313
+ for (const [name, table] of Object.entries(schemaTables)) if (!ownedByOthers.has(name)) prunedTables[name] = table;
314
+ return {
315
+ ...schemaObj,
316
+ tables: prunedTables
317
+ };
318
+ }
319
+ //#endregion
320
+ //#region src/aggregate/strategies/synth.ts
321
+ /**
322
+ * Synthesise a migration plan for a single member by projecting the
323
+ * live schema down to that member's claimed slice and delegating to
324
+ * the family's `createPlanner(...).plan(...)`.
325
+ *
326
+ * Pre-projection (via {@link projectSchemaToSpace}) closes the F23
327
+ * concern: without it, the family's planner sees other members'
328
+ * tables as "extras" and emits destructive ops to drop them. With it,
329
+ * the planner only sees the slice this member claims.
330
+ *
331
+ * The synthesised plan's `targetId` is set from `aggregateTargetId`
332
+ * (the aggregate's ambient target). The family's planner does not
333
+ * stamp `targetId` on the produced plan; the aggregate planner is
334
+ * the single point that knows the target.
335
+ *
336
+ * Used by:
337
+ *
338
+ * - The app member by default (CLI policy
339
+ * `ignoreGraphFor: { app.spaceId }`).
340
+ * - Any extension member whose `headRef.invariants` is empty (the
341
+ * strategy selector falls back to synth when graph-walk isn't
342
+ * required).
343
+ */
344
+ async function synthStrategy(input) {
345
+ const projectedSchema = projectSchemaToSpace(input.schemaIntrospection, input.member, input.otherMembers);
346
+ const plannerResult = await input.migrations.createPlanner(input.familyInstance).plan({
347
+ contract: input.member.contract,
348
+ schema: projectedSchema,
349
+ policy: input.operationPolicy,
350
+ fromContract: null,
351
+ frameworkComponents: input.frameworkComponents,
352
+ spaceId: input.member.spaceId
353
+ });
354
+ if (plannerResult.kind === "failure") return {
355
+ kind: "failure",
356
+ conflicts: plannerResult.conflicts
357
+ };
358
+ const synthedPlan = plannerResult.plan;
359
+ return {
360
+ kind: "ok",
361
+ result: {
362
+ plan: new Proxy(synthedPlan, {
363
+ get(target, prop) {
364
+ if (prop === "targetId") return input.aggregateTargetId;
365
+ return Reflect.get(target, prop, target);
366
+ },
367
+ has(target, prop) {
368
+ if (prop === "targetId") return true;
369
+ return Reflect.has(target, prop);
370
+ }
371
+ }),
372
+ displayOps: synthedPlan.operations,
373
+ destinationContract: input.member.contract,
374
+ strategy: "synth"
375
+ }
376
+ };
377
+ }
378
+ //#endregion
379
+ //#region src/aggregate/planner.ts
380
+ /**
381
+ * Plan a migration across every member of a {@link ContractSpaceAggregate}.
382
+ *
383
+ * Strategy selection per member, in order; first match wins:
384
+ *
385
+ * 1. If `callerPolicy.ignoreGraphFor.has(member.spaceId)`:
386
+ * - If `member.headRef.invariants` is empty → synth.
387
+ * - Else → `policyConflict` (synth cannot satisfy authored invariants).
388
+ * 2. Else if `member.migrations.graph` is non-empty AND graph-walk
389
+ * succeeds → graph-walk.
390
+ * 3. Else if `member.headRef.invariants` is empty → synth.
391
+ * 4. Else → graph-walk failure → `extensionPathUnreachable` /
392
+ * `extensionPathUnsatisfiable`.
393
+ *
394
+ * Output `applyOrder` is `[...aggregate.extensions.map(spaceId), aggregate.app.spaceId]`
395
+ * — extensions alphabetical, then app — matching today's
396
+ * `concatenateSpaceApplyInputs` ordering. This preserves
397
+ * `MultiSpaceRunnerFailure.failingSpace` attribution byte-for-byte.
398
+ *
399
+ * Every emitted `MigrationPlan` has `targetId = aggregate.targetId`.
400
+ * No placeholder cast; no patch step.
401
+ */
402
+ async function planAggregate(input) {
403
+ const { aggregate, currentDBState, callerPolicy } = input;
404
+ const allMembers = [aggregate.app, ...aggregate.extensions];
405
+ const perSpace = /* @__PURE__ */ new Map();
406
+ const orderedMembers = [...aggregate.extensions, aggregate.app];
407
+ for (const member of orderedMembers) {
408
+ const otherMembers = allMembers.filter((m) => m.spaceId !== member.spaceId);
409
+ const currentMarker = currentDBState.markersBySpaceId.get(member.spaceId) ?? null;
410
+ const ignoreGraph = callerPolicy.ignoreGraphFor.has(member.spaceId);
411
+ const invariantsRequired = member.headRef.invariants.length > 0;
412
+ if (ignoreGraph && invariantsRequired) return notOk({
413
+ kind: "policyConflict",
414
+ spaceId: member.spaceId,
415
+ 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.`
416
+ });
417
+ if (ignoreGraph) {
418
+ const synthOutcome = await synthStrategy({
419
+ aggregateTargetId: aggregate.targetId,
420
+ member,
421
+ otherMembers,
422
+ schemaIntrospection: currentDBState.schemaIntrospection,
423
+ familyInstance: input.familyInstance,
424
+ migrations: input.migrations,
425
+ frameworkComponents: input.frameworkComponents,
426
+ operationPolicy: input.operationPolicy
427
+ });
428
+ if (synthOutcome.kind === "failure") return notOk({
429
+ kind: "appSynthFailure",
430
+ spaceId: member.spaceId,
431
+ conflicts: synthOutcome.conflicts
432
+ });
433
+ perSpace.set(member.spaceId, synthOutcome.result);
434
+ continue;
435
+ }
436
+ if (member.migrations.graph.nodes.size > 0) {
437
+ const walked = graphWalkStrategy({
438
+ aggregateTargetId: aggregate.targetId,
439
+ member,
440
+ currentMarker
441
+ });
442
+ if (walked.kind === "ok") {
443
+ perSpace.set(member.spaceId, walked.result);
444
+ continue;
445
+ }
446
+ if (walked.kind === "unreachable") return notOk({
447
+ kind: "extensionPathUnreachable",
448
+ spaceId: member.spaceId,
449
+ target: member.headRef.hash
450
+ });
451
+ return notOk({
452
+ kind: "extensionPathUnsatisfiable",
453
+ spaceId: member.spaceId,
454
+ missingInvariants: walked.missing
455
+ });
456
+ }
457
+ if (invariantsRequired) return notOk({
458
+ kind: "extensionPathUnsatisfiable",
459
+ spaceId: member.spaceId,
460
+ missingInvariants: [...member.headRef.invariants].sort()
461
+ });
462
+ const synthOutcome = await synthStrategy({
463
+ aggregateTargetId: aggregate.targetId,
464
+ member,
465
+ otherMembers,
466
+ schemaIntrospection: currentDBState.schemaIntrospection,
467
+ familyInstance: input.familyInstance,
468
+ migrations: input.migrations,
469
+ frameworkComponents: input.frameworkComponents,
470
+ operationPolicy: input.operationPolicy
471
+ });
472
+ if (synthOutcome.kind === "failure") return notOk({
473
+ kind: "appSynthFailure",
474
+ spaceId: member.spaceId,
475
+ conflicts: synthOutcome.conflicts
476
+ });
477
+ perSpace.set(member.spaceId, synthOutcome.result);
478
+ }
479
+ return ok({
480
+ perSpace,
481
+ applyOrder: [...aggregate.extensions.map((m) => m.spaceId), aggregate.app.spaceId]
482
+ });
483
+ }
484
+ //#endregion
485
+ //#region src/aggregate/verifier.ts
486
+ /**
487
+ * Verify a {@link ContractSpaceAggregate} against the live database
488
+ * state. Bundles two checks:
489
+ *
490
+ * - `markerCheck` per member: compare the live marker row against the
491
+ * member's `headRef.hash` + `headRef.invariants`. Absence is a
492
+ * distinct kind, not an error (callers — `db verify` strict vs
493
+ * `db init` precondition — choose how to interpret it).
494
+ * - `schemaCheck` per member: project the live schema to the slice
495
+ * the member claims via {@link projectSchemaToSpace}, then delegate
496
+ * to the caller-supplied `verifySchemaForMember`. The pre-projection
497
+ * means the family's single-contract verifier no longer sees other
498
+ * members' tables as `extras`, so a multi-member deployment never
499
+ * surfaces cross-member tables as orphaned schema elements.
500
+ *
501
+ * `markerCheck.orphanMarkers` lists every marker row whose `space` is
502
+ * not a member of the aggregate. `db verify` callers reject orphans;
503
+ * future tooling may not.
504
+ *
505
+ * Pure synchronous function; no I/O. The caller (CLI) gathers
506
+ * `markersBySpaceId` and `schemaIntrospection` ahead of the call.
507
+ */
508
+ function verifyAggregate(input) {
509
+ try {
510
+ return runVerifyAggregate(input);
511
+ } catch (error) {
512
+ return notOk({
513
+ kind: "introspectionFailure",
514
+ detail: error instanceof Error ? error.message : String(error)
515
+ });
516
+ }
517
+ }
518
+ function runVerifyAggregate(input) {
519
+ const { aggregate, markersBySpaceId, schemaIntrospection, mode, verifySchemaForMember } = input;
520
+ const allMembers = [aggregate.app, ...aggregate.extensions];
521
+ const memberSpaceIds = new Set(allMembers.map((m) => m.spaceId));
522
+ const markerPerSpace = /* @__PURE__ */ new Map();
523
+ for (const member of allMembers) {
524
+ const marker = markersBySpaceId.get(member.spaceId) ?? null;
525
+ if (marker === null) {
526
+ markerPerSpace.set(member.spaceId, { kind: "absent" });
527
+ continue;
528
+ }
529
+ if (marker.storageHash !== member.headRef.hash) {
530
+ markerPerSpace.set(member.spaceId, {
531
+ kind: "hashMismatch",
532
+ markerHash: marker.storageHash,
533
+ expected: member.headRef.hash
534
+ });
535
+ continue;
536
+ }
537
+ const markerInvariants = new Set(marker.invariants);
538
+ const missing = member.headRef.invariants.filter((id) => !markerInvariants.has(id));
539
+ if (missing.length > 0) {
540
+ markerPerSpace.set(member.spaceId, {
541
+ kind: "missingInvariants",
542
+ missing: [...missing].sort()
543
+ });
544
+ continue;
545
+ }
546
+ markerPerSpace.set(member.spaceId, { kind: "ok" });
547
+ }
548
+ const orphanMarkers = [];
549
+ for (const [spaceId, row] of markersBySpaceId) if (row !== null && !memberSpaceIds.has(spaceId)) orphanMarkers.push({
550
+ spaceId,
551
+ row
552
+ });
553
+ orphanMarkers.sort((a, b) => a.spaceId.localeCompare(b.spaceId));
554
+ const schemaPerSpace = /* @__PURE__ */ new Map();
555
+ for (const member of allMembers) {
556
+ const projected = projectSchemaToSpace(schemaIntrospection, member, allMembers.filter((m) => m.spaceId !== member.spaceId));
557
+ schemaPerSpace.set(member.spaceId, verifySchemaForMember(projected, member, mode));
558
+ }
559
+ return ok({
560
+ markerCheck: {
561
+ perSpace: markerPerSpace,
562
+ orphanMarkers
563
+ },
564
+ schemaCheck: {
565
+ perSpace: schemaPerSpace,
566
+ orphanElements: detectOrphanElements(schemaIntrospection, allMembers)
567
+ }
568
+ });
569
+ }
570
+ /**
571
+ * Live tables not claimed by any aggregate member. Duck-typed against
572
+ * the introspected schema's `tables` map; schemas whose shape doesn't
573
+ * match return an empty list (consistent with
574
+ * {@link projectSchemaToSpace}'s fall-through).
575
+ */
576
+ function detectOrphanElements(schemaIntrospection, members) {
577
+ if (typeof schemaIntrospection !== "object" || schemaIntrospection === null) return [];
578
+ const liveTables = schemaIntrospection.tables;
579
+ if (typeof liveTables !== "object" || liveTables === null) return [];
580
+ const claimedTables = /* @__PURE__ */ new Set();
581
+ for (const member of members) {
582
+ const storage = member.contract.storage;
583
+ if (typeof storage !== "object" || storage === null) continue;
584
+ const tables = storage.tables;
585
+ if (typeof tables !== "object" || tables === null) continue;
586
+ for (const tableName of Object.keys(tables)) claimedTables.add(tableName);
587
+ }
588
+ const orphans = [];
589
+ for (const tableName of Object.keys(liveTables)) if (!claimedTables.has(tableName)) orphans.push({
590
+ kind: "table",
591
+ name: tableName
592
+ });
593
+ orphans.sort((a, b) => a.name.localeCompare(b.name));
594
+ return orphans;
595
+ }
596
+ //#endregion
597
+ export { graphWalkStrategy, loadContractSpaceAggregate, planAggregate, projectSchemaToSpace, verifyAggregate };
598
+
599
+ //# sourceMappingURL=aggregate.mjs.map