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