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

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