@prisma-next/migration-tools 0.5.0-dev.9 → 0.6.0-dev.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.
- package/README.md +34 -22
- package/dist/{constants-BRi0X7B_.mjs → constants-DWV9_o2Z.mjs} +2 -2
- package/dist/{constants-BRi0X7B_.mjs.map → constants-DWV9_o2Z.mjs.map} +1 -1
- package/dist/errors-EPL_9p9f.mjs +297 -0
- package/dist/errors-EPL_9p9f.mjs.map +1 -0
- package/dist/exports/aggregate.d.mts +614 -0
- package/dist/exports/aggregate.d.mts.map +1 -0
- package/dist/exports/aggregate.mjs +611 -0
- package/dist/exports/aggregate.mjs.map +1 -0
- package/dist/exports/constants.d.mts.map +1 -1
- package/dist/exports/constants.mjs +2 -3
- package/dist/exports/errors.d.mts +68 -0
- package/dist/exports/errors.d.mts.map +1 -0
- package/dist/exports/errors.mjs +2 -0
- package/dist/exports/graph.d.mts +2 -0
- package/dist/exports/graph.mjs +1 -0
- package/dist/exports/hash.d.mts +52 -0
- package/dist/exports/hash.d.mts.map +1 -0
- package/dist/exports/hash.mjs +2 -0
- package/dist/exports/invariants.d.mts +39 -0
- package/dist/exports/invariants.d.mts.map +1 -0
- package/dist/exports/invariants.mjs +2 -0
- package/dist/exports/io.d.mts +66 -6
- package/dist/exports/io.d.mts.map +1 -1
- package/dist/exports/io.mjs +2 -3
- package/dist/exports/metadata.d.mts +2 -0
- package/dist/exports/metadata.mjs +1 -0
- package/dist/exports/migration-graph.d.mts +2 -0
- package/dist/exports/migration-graph.mjs +2 -0
- package/dist/exports/migration-ts.d.mts.map +1 -1
- package/dist/exports/migration-ts.mjs +2 -4
- package/dist/exports/migration-ts.mjs.map +1 -1
- package/dist/exports/migration.d.mts +15 -14
- package/dist/exports/migration.d.mts.map +1 -1
- package/dist/exports/migration.mjs +70 -43
- package/dist/exports/migration.mjs.map +1 -1
- package/dist/exports/package.d.mts +3 -0
- package/dist/exports/package.mjs +1 -0
- package/dist/exports/refs.d.mts.map +1 -1
- package/dist/exports/refs.mjs +3 -4
- package/dist/exports/refs.mjs.map +1 -1
- package/dist/exports/spaces.d.mts +591 -0
- package/dist/exports/spaces.d.mts.map +1 -0
- package/dist/exports/spaces.mjs +266 -0
- package/dist/exports/spaces.mjs.map +1 -0
- package/dist/graph-HMWAldoR.d.mts +28 -0
- package/dist/graph-HMWAldoR.d.mts.map +1 -0
- package/dist/hash-By50zM_E.mjs +74 -0
- package/dist/hash-By50zM_E.mjs.map +1 -0
- package/dist/invariants-qgQGlsrV.mjs +57 -0
- package/dist/invariants-qgQGlsrV.mjs.map +1 -0
- package/dist/io-D5YYptRO.mjs +239 -0
- package/dist/io-D5YYptRO.mjs.map +1 -0
- package/dist/metadata-CFvm3ayn.d.mts +2 -0
- package/dist/migration-graph-DGNnKDY5.mjs +523 -0
- package/dist/migration-graph-DGNnKDY5.mjs.map +1 -0
- package/dist/migration-graph-DulOITvG.d.mts +124 -0
- package/dist/migration-graph-DulOITvG.d.mts.map +1 -0
- package/dist/op-schema-D5qkXfEf.mjs +13 -0
- package/dist/op-schema-D5qkXfEf.mjs.map +1 -0
- package/dist/package-BjiZ7KDy.d.mts +21 -0
- package/dist/package-BjiZ7KDy.d.mts.map +1 -0
- package/dist/read-contract-space-contract-Bj_EMYSC.mjs +298 -0
- package/dist/read-contract-space-contract-Bj_EMYSC.mjs.map +1 -0
- package/package.json +42 -17
- package/src/aggregate/loader.ts +409 -0
- package/src/aggregate/marker-types.ts +16 -0
- package/src/aggregate/planner-types.ts +171 -0
- package/src/aggregate/planner.ts +158 -0
- package/src/aggregate/project-schema-to-space.ts +64 -0
- package/src/aggregate/strategies/graph-walk.ts +118 -0
- package/src/aggregate/strategies/synth.ts +122 -0
- package/src/aggregate/types.ts +89 -0
- package/src/aggregate/verifier.ts +230 -0
- package/src/assert-descriptor-self-consistency.ts +70 -0
- package/src/compute-extension-space-apply-path.ts +152 -0
- package/src/concatenate-space-apply-inputs.ts +90 -0
- package/src/contract-space-from-json.ts +63 -0
- package/src/detect-space-contract-drift.ts +91 -0
- package/src/emit-contract-space-artefacts.ts +70 -0
- package/src/errors.ts +251 -17
- package/src/exports/aggregate.ts +42 -0
- package/src/exports/errors.ts +8 -0
- package/src/exports/graph.ts +1 -0
- package/src/exports/hash.ts +2 -0
- package/src/exports/invariants.ts +1 -0
- package/src/exports/io.ts +3 -1
- package/src/exports/metadata.ts +1 -0
- package/src/exports/{dag.ts → migration-graph.ts} +3 -2
- package/src/exports/migration.ts +0 -1
- package/src/exports/package.ts +2 -0
- package/src/exports/spaces.ts +50 -0
- package/src/gather-disk-contract-space-state.ts +62 -0
- package/src/graph-ops.ts +57 -30
- package/src/graph.ts +25 -0
- package/src/hash.ts +91 -0
- package/src/invariants.ts +61 -0
- package/src/io.ts +163 -40
- package/src/metadata.ts +1 -0
- package/src/migration-base.ts +97 -56
- package/src/migration-graph.ts +676 -0
- package/src/op-schema.ts +11 -0
- package/src/package.ts +21 -0
- package/src/plan-all-spaces.ts +76 -0
- package/src/read-contract-space-contract.ts +44 -0
- package/src/read-contract-space-head-ref.ts +63 -0
- package/src/space-layout.ts +48 -0
- package/src/verify-contract-spaces.ts +272 -0
- package/dist/attestation-BnzTb0Qp.mjs +0 -65
- package/dist/attestation-BnzTb0Qp.mjs.map +0 -1
- package/dist/errors-BmiSgz1j.mjs +0 -160
- package/dist/errors-BmiSgz1j.mjs.map +0 -1
- package/dist/exports/attestation.d.mts +0 -37
- package/dist/exports/attestation.d.mts.map +0 -1
- package/dist/exports/attestation.mjs +0 -4
- package/dist/exports/dag.d.mts +0 -51
- package/dist/exports/dag.d.mts.map +0 -1
- package/dist/exports/dag.mjs +0 -386
- package/dist/exports/dag.mjs.map +0 -1
- package/dist/exports/types.d.mts +0 -35
- package/dist/exports/types.d.mts.map +0 -1
- package/dist/exports/types.mjs +0 -3
- package/dist/io-Cd6GLyjK.mjs +0 -153
- package/dist/io-Cd6GLyjK.mjs.map +0 -1
- package/dist/types-DyGXcWWp.d.mts +0 -71
- package/dist/types-DyGXcWWp.d.mts.map +0 -1
- package/src/attestation.ts +0 -81
- package/src/dag.ts +0 -426
- package/src/exports/attestation.ts +0 -2
- package/src/exports/types.ts +0 -10
- package/src/types.ts +0 -66
|
@@ -0,0 +1,611 @@
|
|
|
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 readContractSpaceHeadRef, i as detectSpaceContractDrift, l as spaceMigrationDirectory, n as listContractSpaceDirectories, o as APP_SPACE_ID, t as readContractSpaceContract } from "../read-contract-space-contract-Bj_EMYSC.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
|