@prisma-next/migration-tools 0.11.0-dev.46 → 0.11.0-dev.47
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/dist/{errors-CoEN114u.mjs → errors-4YabujxZ.mjs} +3 -21
- package/dist/{errors-CoEN114u.mjs.map → errors-4YabujxZ.mjs.map} +1 -1
- package/dist/exports/aggregate.d.mts +273 -177
- package/dist/exports/aggregate.d.mts.map +1 -1
- package/dist/exports/aggregate.mjs +363 -185
- package/dist/exports/aggregate.mjs.map +1 -1
- package/dist/exports/enumerate-migration-spaces.d.mts +1 -1
- package/dist/exports/enumerate-migration-spaces.mjs +4 -4
- package/dist/exports/enumerate-migration-spaces.mjs.map +1 -1
- package/dist/exports/errors.mjs +1 -1
- package/dist/exports/graph.d.mts +1 -1
- package/dist/exports/hash.d.mts +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/metadata.d.mts +1 -1
- package/dist/exports/migration-graph.d.mts +2 -2
- package/dist/exports/migration-graph.mjs +2 -2
- package/dist/exports/migration-list-graph-layout.d.mts +2 -2
- package/dist/exports/migration-list-graph-topology.d.mts +1 -1
- package/dist/exports/migration-list-types.d.mts +1 -1
- package/dist/exports/migration.d.mts +1 -1
- package/dist/exports/migration.mjs +2 -2
- package/dist/exports/ref-resolution.d.mts +2 -2
- package/dist/exports/ref-resolution.mjs +1 -1
- package/dist/exports/refs.d.mts +1 -1
- package/dist/exports/refs.mjs +2 -2
- package/dist/exports/spaces.d.mts +1 -130
- package/dist/exports/spaces.d.mts.map +1 -1
- package/dist/exports/spaces.mjs +6 -6
- package/dist/exports/spaces.mjs.map +1 -1
- package/dist/{graph-B0LIIjIu.d.mts → graph-3dLMZp5l.d.mts} +1 -1
- package/dist/{graph-B0LIIjIu.d.mts.map → graph-3dLMZp5l.d.mts.map} +1 -1
- package/dist/{invariants-lbJddL-S.mjs → invariants-CCOAyg6c.mjs} +2 -2
- package/dist/{invariants-lbJddL-S.mjs.map → invariants-CCOAyg6c.mjs.map} +1 -1
- package/dist/io-BH4G3F-i.d.mts +124 -0
- package/dist/io-BH4G3F-i.d.mts.map +1 -0
- package/dist/{io-Dc64lvaL.mjs → io-BHl0amF0.mjs} +99 -6
- package/dist/io-BHl0amF0.mjs.map +1 -0
- package/dist/{migration-graph-fl5ChjXE.d.mts → migration-graph-CWEM2SLR.d.mts} +2 -2
- package/dist/{migration-graph-fl5ChjXE.d.mts.map → migration-graph-CWEM2SLR.d.mts.map} +1 -1
- package/dist/{migration-graph-D5JeadSE.mjs → migration-graph-kGBkIZDa.mjs} +3 -7
- package/dist/migration-graph-kGBkIZDa.mjs.map +1 -0
- package/dist/{migration-list-graph-topology-CafEnhPT.d.mts → migration-list-graph-topology-Be1d8Y89.d.mts} +2 -2
- package/dist/{migration-list-graph-topology-CafEnhPT.d.mts.map → migration-list-graph-topology-Be1d8Y89.d.mts.map} +1 -1
- package/dist/{migration-list-types-wLyb3E-p.d.mts → migration-list-types-0YjFETIv.d.mts} +1 -1
- package/dist/{migration-list-types-wLyb3E-p.d.mts.map → migration-list-types-0YjFETIv.d.mts.map} +1 -1
- package/dist/{read-contract-space-contract-C4fEdoXO.mjs → read-contract-space-contract-7-OB-ykY.mjs} +3 -3
- package/dist/{read-contract-space-contract-C4fEdoXO.mjs.map → read-contract-space-contract-7-OB-ykY.mjs.map} +1 -1
- package/dist/{refs-D8xBNqs7.d.mts → refs-B33AsTjk.d.mts} +12 -2
- package/dist/refs-B33AsTjk.d.mts.map +1 -0
- package/dist/{refs-HhOkD8BT.mjs → refs-BBKNL45K.mjs} +75 -3
- package/dist/refs-BBKNL45K.mjs.map +1 -0
- package/dist/{verify-contract-spaces-DIdQLGo7.mjs → verify-contract-spaces-BJX5gqtD.mjs} +3 -3
- package/dist/{verify-contract-spaces-DIdQLGo7.mjs.map → verify-contract-spaces-BJX5gqtD.mjs.map} +1 -1
- package/dist/verify-contract-spaces-BdysZdQk.d.mts +132 -0
- package/dist/verify-contract-spaces-BdysZdQk.d.mts.map +1 -0
- package/package.json +6 -6
- package/src/aggregate/aggregate.ts +90 -0
- package/src/aggregate/check-integrity.ts +243 -0
- package/src/aggregate/loader.ts +156 -334
- package/src/aggregate/planner.ts +8 -6
- package/src/aggregate/project-schema-to-space.ts +1 -1
- package/src/aggregate/strategies/graph-walk.ts +12 -7
- package/src/aggregate/strategies/synth.ts +2 -2
- package/src/aggregate/types.ts +56 -64
- package/src/aggregate/verifier.ts +6 -4
- package/src/compute-extension-space-apply-path.ts +1 -1
- package/src/enumerate-migration-spaces.ts +1 -1
- package/src/exports/aggregate.ts +17 -12
- package/src/exports/io.ts +2 -0
- package/src/integrity-violation.ts +114 -0
- package/src/io.ts +139 -6
- package/src/migration-graph.ts +3 -17
- package/src/refs.ts +94 -0
- package/dist/exports/io.d.mts.map +0 -1
- package/dist/io-Dc64lvaL.mjs.map +0 -1
- package/dist/migration-graph-D5JeadSE.mjs.map +0 -1
- package/dist/refs-D8xBNqs7.d.mts.map +0 -1
- package/dist/refs-HhOkD8BT.mjs.map +0 -1
- /package/dist/{metadata-COhIQCiH.d.mts → metadata-CGkJF4L6.d.mts} +0 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
//#region src/verify-contract-spaces.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* List the per-space subdirectories under
|
|
4
|
+
* `<projectRoot>/migrations/`. Returns space-id directory names (sorted
|
|
5
|
+
* alphabetically) — i.e. any non-dot-prefixed subdirectory whose root
|
|
6
|
+
* does **not** contain a `migration.json` manifest. The manifest is the
|
|
7
|
+
* structural marker of a user-authored migration directory (see
|
|
8
|
+
* `readMigrationsDir` in `./io`); directory names themselves belong to
|
|
9
|
+
* the user and are not part of the contract.
|
|
10
|
+
*
|
|
11
|
+
* Returns `[]` if the migrations directory does not exist (greenfield
|
|
12
|
+
* project).
|
|
13
|
+
*
|
|
14
|
+
* Reads only the user's repo. **No descriptor import.** The caller
|
|
15
|
+
* (verifier) feeds the result into {@link verifyContractSpaces} alongside
|
|
16
|
+
* the loaded-space set and the marker rows.
|
|
17
|
+
*/
|
|
18
|
+
declare function listContractSpaceDirectories(projectMigrationsDir: string): Promise<readonly string[]>;
|
|
19
|
+
/**
|
|
20
|
+
* On-disk head value (`(hash, invariants)`) for one contract space.
|
|
21
|
+
* The verifier compares this against the marker row for the same space
|
|
22
|
+
* to detect drift between the user-emitted artefacts and the live DB
|
|
23
|
+
* marker.
|
|
24
|
+
*/
|
|
25
|
+
interface ContractSpaceHeadRecord {
|
|
26
|
+
readonly hash: string;
|
|
27
|
+
readonly invariants: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Marker row read from `prisma_contract.marker` (one per `space`).
|
|
31
|
+
* Caller resolves these via the family runtime's marker reader before
|
|
32
|
+
* invoking {@link verifyContractSpaces}.
|
|
33
|
+
*/
|
|
34
|
+
interface SpaceMarkerRecord {
|
|
35
|
+
readonly hash: string;
|
|
36
|
+
readonly invariants: readonly string[];
|
|
37
|
+
}
|
|
38
|
+
interface VerifyContractSpacesInputs {
|
|
39
|
+
/**
|
|
40
|
+
* Set of contract spaces the project declares: `'app'` plus each
|
|
41
|
+
* extension space in `extensionPacks`. The caller's discovery path
|
|
42
|
+
* never reads the extension descriptor module — it walks the
|
|
43
|
+
* `extensionPacks` configuration in `prisma-next.config.ts` for the
|
|
44
|
+
* space ids.
|
|
45
|
+
*/
|
|
46
|
+
readonly loadedSpaces: ReadonlySet<string>;
|
|
47
|
+
/**
|
|
48
|
+
* Per-space subdirectories observed under
|
|
49
|
+
* `<projectRoot>/migrations/`. Resolved via
|
|
50
|
+
* {@link listContractSpaceDirectories}.
|
|
51
|
+
*/
|
|
52
|
+
readonly spaceDirsOnDisk: readonly string[];
|
|
53
|
+
/**
|
|
54
|
+
* Head ref per space, keyed by space id. Caller reads
|
|
55
|
+
* `<projectRoot>/migrations/<space-id>/contract.json` and
|
|
56
|
+
* `<projectRoot>/migrations/<space-id>/refs/head.json` to construct
|
|
57
|
+
* this map. Spaces with no contract-space dir on disk simply omit a
|
|
58
|
+
* map entry.
|
|
59
|
+
*/
|
|
60
|
+
readonly headRefsBySpace: ReadonlyMap<string, ContractSpaceHeadRecord>;
|
|
61
|
+
/**
|
|
62
|
+
* Marker rows keyed by `space`. Caller reads them from the
|
|
63
|
+
* `prisma_contract.marker` table.
|
|
64
|
+
*/
|
|
65
|
+
readonly markerRowsBySpace: ReadonlyMap<string, SpaceMarkerRecord>;
|
|
66
|
+
}
|
|
67
|
+
type SpaceVerifierViolation = {
|
|
68
|
+
readonly kind: 'declaredButUnmigrated';
|
|
69
|
+
readonly spaceId: string;
|
|
70
|
+
readonly remediation: string;
|
|
71
|
+
} | {
|
|
72
|
+
readonly kind: 'orphanMarker';
|
|
73
|
+
readonly spaceId: string;
|
|
74
|
+
readonly remediation: string;
|
|
75
|
+
} | {
|
|
76
|
+
readonly kind: 'orphanSpaceDir';
|
|
77
|
+
readonly spaceId: string;
|
|
78
|
+
readonly remediation: string;
|
|
79
|
+
} | {
|
|
80
|
+
readonly kind: 'hashMismatch';
|
|
81
|
+
readonly spaceId: string;
|
|
82
|
+
readonly priorHeadHash: string;
|
|
83
|
+
readonly markerHash: string;
|
|
84
|
+
readonly remediation: string;
|
|
85
|
+
} | {
|
|
86
|
+
readonly kind: 'invariantsMismatch';
|
|
87
|
+
readonly spaceId: string;
|
|
88
|
+
readonly onDiskInvariants: readonly string[];
|
|
89
|
+
readonly markerInvariants: readonly string[];
|
|
90
|
+
readonly remediation: string;
|
|
91
|
+
};
|
|
92
|
+
type VerifyContractSpacesResult = {
|
|
93
|
+
readonly ok: true;
|
|
94
|
+
} | {
|
|
95
|
+
readonly ok: false;
|
|
96
|
+
readonly violations: readonly SpaceVerifierViolation[];
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Pure structural verifier for the per-space mechanism. Aggregates the
|
|
100
|
+
* three orphan / missing checks plus per-space hash and invariant
|
|
101
|
+
* comparison.
|
|
102
|
+
*
|
|
103
|
+
* Algorithm:
|
|
104
|
+
*
|
|
105
|
+
* - For every extension space declared in `loadedSpaces` (`'app'`
|
|
106
|
+
* excluded — the per-space verifier is scoped to extension members;
|
|
107
|
+
* the app is verified through the aggregate path):
|
|
108
|
+
* - If no contract-space dir on disk → `declaredButUnmigrated`.
|
|
109
|
+
* - Else if `markerRowsBySpace` lacks an entry → no violation here;
|
|
110
|
+
* the live-DB compare done outside this helper is where the
|
|
111
|
+
* absence shows up.
|
|
112
|
+
* - Else compare marker hash / invariants vs. on-disk head hash /
|
|
113
|
+
* invariants → `hashMismatch` / `invariantsMismatch` on drift.
|
|
114
|
+
* - For every contract-space dir on disk that is not in `loadedSpaces` →
|
|
115
|
+
* `orphanSpaceDir`.
|
|
116
|
+
* - For every marker row whose `space` is not in `loadedSpaces` →
|
|
117
|
+
* `orphanMarker`. The app-space marker is always loaded (`'app'` is
|
|
118
|
+
* in `loadedSpaces` by definition).
|
|
119
|
+
*
|
|
120
|
+
* Output is deterministic: violations are sorted first by `kind`
|
|
121
|
+
* (`declaredButUnmigrated` → `orphanMarker` → `orphanSpaceDir` →
|
|
122
|
+
* `hashMismatch` → `invariantsMismatch`) then by `spaceId`. Two callers
|
|
123
|
+
* passing equivalent inputs see byte-identical violation lists.
|
|
124
|
+
*
|
|
125
|
+
* Synchronous, pure, no I/O. **Does not import the extension descriptor**
|
|
126
|
+
* (the inputs are pre-resolved by the caller); the verifier reads only
|
|
127
|
+
* the user repo, not `node_modules`.
|
|
128
|
+
*/
|
|
129
|
+
declare function verifyContractSpaces(inputs: VerifyContractSpacesInputs): VerifyContractSpacesResult;
|
|
130
|
+
//#endregion
|
|
131
|
+
export { VerifyContractSpacesResult as a, VerifyContractSpacesInputs as i, SpaceMarkerRecord as n, listContractSpaceDirectories as o, SpaceVerifierViolation as r, verifyContractSpaces as s, ContractSpaceHeadRecord as t };
|
|
132
|
+
//# sourceMappingURL=verify-contract-spaces-BdysZdQk.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify-contract-spaces-BdysZdQk.d.mts","names":[],"sources":["../src/verify-contract-spaces.ts"],"mappings":";;AAyBA;;;;AAEU;AAyCV;;;;AAEqB;AAQrB;;;;AAEqB;iBAvDC,4BAAA,CACpB,oBAAA,WACC,OAAO;;;;;;;UAyCO,uBAAA;EAAA,SACN,IAAA;EAAA,SACA,UAAU;AAAA;;;;;;UAQJ,iBAAA;EAAA,SACN,IAAA;EAAA,SACA,UAAU;AAAA;AAAA,UAGJ,0BAAA;EAiCL;;;;;;;EAAA,SAzBD,YAAA,EAAc,WAAA;EAiCV;;;;;EAAA,SA1BJ,eAAA;EAoCI;;;;;;;EAAA,SA3BJ,eAAA,EAAiB,WAAA,SAAoB,uBAAA;EAqCjC;;AAAW;AAG1B;EAHe,SA/BJ,iBAAA,EAAmB,WAAA,SAAoB,iBAAA;AAAA;AAAA,KAGtC,sBAAA;EAAA,SAEG,IAAA;EAAA,SACA,OAAA;EAAA,SACA,WAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,OAAA;EAAA,SACA,WAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,OAAA;EAAA,SACA,WAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,OAAA;EAAA,SACA,aAAA;EAAA,SACA,UAAA;EAAA,SACA,WAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,OAAA;EAAA,SACA,gBAAA;EAAA,SACA,gBAAA;EAAA,SACA,WAAA;AAAA;AAAA,KAGH,0BAAA;EAAA,SACG,EAAA;AAAA;EAAA,SACA,EAAA;EAAA,SAAoB,UAAA,WAAqB,sBAAsB;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiC9D,oBAAA,CACd,MAAA,EAAQ,0BAAA,GACP,0BAA0B"}
|
package/package.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/migration-tools",
|
|
3
|
-
"version": "0.11.0-dev.
|
|
3
|
+
"version": "0.11.0-dev.47",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"description": "On-disk migration persistence, hash verification, and chain reconstruction for Prisma Next",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@prisma-next/contract": "0.11.0-dev.
|
|
10
|
-
"@prisma-next/framework-components": "0.11.0-dev.
|
|
11
|
-
"@prisma-next/utils": "0.11.0-dev.
|
|
9
|
+
"@prisma-next/contract": "0.11.0-dev.47",
|
|
10
|
+
"@prisma-next/framework-components": "0.11.0-dev.47",
|
|
11
|
+
"@prisma-next/utils": "0.11.0-dev.47",
|
|
12
12
|
"arktype": "^2.2.0",
|
|
13
13
|
"pathe": "^2.0.3",
|
|
14
14
|
"prettier": "^3.8.3"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@prisma-next/tsconfig": "0.11.0-dev.
|
|
18
|
-
"@prisma-next/tsdown": "0.11.0-dev.
|
|
17
|
+
"@prisma-next/tsconfig": "0.11.0-dev.47",
|
|
18
|
+
"@prisma-next/tsdown": "0.11.0-dev.47",
|
|
19
19
|
"tsdown": "0.22.0",
|
|
20
20
|
"typescript": "5.9.3",
|
|
21
21
|
"vitest": "4.1.6"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
2
|
+
import type { MigrationGraph } from '../graph';
|
|
3
|
+
import type { IntegrityQueryOptions, IntegrityViolation } from '../integrity-violation';
|
|
4
|
+
import { reconstructGraph } from '../migration-graph';
|
|
5
|
+
import type { OnDiskMigrationPackage } from '../package';
|
|
6
|
+
import type { Refs } from '../refs';
|
|
7
|
+
import type { ContractSpaceHeadRecord } from '../verify-contract-spaces';
|
|
8
|
+
import type { ContractSpaceAggregate, ContractSpaceMember } from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a member's head ref, asserting it is present. The apply/verify
|
|
12
|
+
* engine only runs after `checkIntegrity` has refused on `headRefMissing`,
|
|
13
|
+
* so a member reaching the planner / verifier without a head ref is a
|
|
14
|
+
* programming error (the integrity gate was skipped), not a user-facing
|
|
15
|
+
* state. The app member's head ref is always synthesised, so this only
|
|
16
|
+
* ever guards an ungated extension space.
|
|
17
|
+
*/
|
|
18
|
+
export function requireHeadRef(member: ContractSpaceMember): ContractSpaceHeadRecord {
|
|
19
|
+
if (member.headRef === null) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Contract space "${member.spaceId}" has no head ref; the integrity gate must refuse a missing head ref before planning or verifying.`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return member.headRef;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build a {@link ContractSpaceMember} with lazily-memoised `graph()` and
|
|
29
|
+
* `contract()` facets.
|
|
30
|
+
*
|
|
31
|
+
* `graph()` reconstructs the migration graph from `packages` on first
|
|
32
|
+
* call and caches it. `contract()` calls `resolveContract` on first call
|
|
33
|
+
* and caches the result; a throwing `resolveContract` (e.g. a missing or
|
|
34
|
+
* undeserializable on-disk contract) re-throws on each call rather than
|
|
35
|
+
* caching a value — `checkIntegrity` surfaces that as `contractUnreadable`.
|
|
36
|
+
*/
|
|
37
|
+
export function createContractSpaceMember(args: {
|
|
38
|
+
readonly spaceId: string;
|
|
39
|
+
readonly packages: readonly OnDiskMigrationPackage[];
|
|
40
|
+
readonly refs: Refs;
|
|
41
|
+
readonly headRef: ContractSpaceHeadRecord | null;
|
|
42
|
+
readonly resolveContract: () => Contract;
|
|
43
|
+
}): ContractSpaceMember {
|
|
44
|
+
const { spaceId, packages, refs, headRef, resolveContract } = args;
|
|
45
|
+
let graphMemo: MigrationGraph | undefined;
|
|
46
|
+
let contractMemo: Contract | undefined;
|
|
47
|
+
return {
|
|
48
|
+
spaceId,
|
|
49
|
+
packages,
|
|
50
|
+
refs,
|
|
51
|
+
headRef,
|
|
52
|
+
graph() {
|
|
53
|
+
graphMemo ??= reconstructGraph(packages);
|
|
54
|
+
return graphMemo;
|
|
55
|
+
},
|
|
56
|
+
contract() {
|
|
57
|
+
contractMemo ??= resolveContract();
|
|
58
|
+
return contractMemo;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Assemble a {@link ContractSpaceAggregate} value from its members and a
|
|
65
|
+
* `checkIntegrity` implementation. The query methods (`listSpaces` /
|
|
66
|
+
* `hasSpace` / `space` / `spaces`) are derived here so every aggregate —
|
|
67
|
+
* loader-built or test-built — shares one query surface: `app` first,
|
|
68
|
+
* then `extensions` in the order supplied (the loader sorts them
|
|
69
|
+
* lex-ascending by `spaceId`).
|
|
70
|
+
*/
|
|
71
|
+
export function createContractSpaceAggregate(args: {
|
|
72
|
+
readonly targetId: string;
|
|
73
|
+
readonly app: ContractSpaceMember;
|
|
74
|
+
readonly extensions: readonly ContractSpaceMember[];
|
|
75
|
+
readonly checkIntegrity: (opts?: IntegrityQueryOptions) => readonly IntegrityViolation[];
|
|
76
|
+
}): ContractSpaceAggregate {
|
|
77
|
+
const { targetId, app, extensions, checkIntegrity } = args;
|
|
78
|
+
const ordered: readonly ContractSpaceMember[] = [app, ...extensions];
|
|
79
|
+
const byId = new Map(ordered.map((m) => [m.spaceId, m]));
|
|
80
|
+
return {
|
|
81
|
+
targetId,
|
|
82
|
+
app,
|
|
83
|
+
extensions,
|
|
84
|
+
listSpaces: () => ordered.map((m) => m.spaceId),
|
|
85
|
+
hasSpace: (id) => byId.has(id),
|
|
86
|
+
space: (id) => byId.get(id),
|
|
87
|
+
spaces: () => ordered,
|
|
88
|
+
checkIntegrity,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { EMPTY_CONTRACT_HASH } from '../constants';
|
|
2
|
+
import { MigrationToolsError } from '../errors';
|
|
3
|
+
import type {
|
|
4
|
+
DeclaredExtensionEntry,
|
|
5
|
+
IntegrityQueryOptions,
|
|
6
|
+
IntegrityViolation,
|
|
7
|
+
} from '../integrity-violation';
|
|
8
|
+
import type { PackageLoadProblem } from '../io';
|
|
9
|
+
import type { OnDiskMigrationPackage } from '../package';
|
|
10
|
+
import type { RefLoadProblem } from '../refs';
|
|
11
|
+
import { extractStorageElementNames } from './extract-storage-element-names';
|
|
12
|
+
import type { ContractSpaceMember } from './types';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One space's load-time facts that `checkIntegrity` judges: the loaded
|
|
16
|
+
* member, the load-time problems `readMigrationsDir` surfaced for it, and
|
|
17
|
+
* whether it is the app space (the app head ref is synthesised, so the
|
|
18
|
+
* head-ref checks are skipped for it).
|
|
19
|
+
*/
|
|
20
|
+
export interface IntegritySpaceState {
|
|
21
|
+
readonly member: ContractSpaceMember;
|
|
22
|
+
readonly problems: readonly PackageLoadProblem[];
|
|
23
|
+
/** Per-ref problems: a user ref `*.json` that exists but is unparseable. */
|
|
24
|
+
readonly refProblems: readonly RefLoadProblem[];
|
|
25
|
+
/**
|
|
26
|
+
* The space's `refs/head.json` problem when it exists but is unparseable.
|
|
27
|
+
* `null` means the head ref was read cleanly or is genuinely absent —
|
|
28
|
+
* the absent case is judged `headRefMissing`, the corrupt case here is
|
|
29
|
+
* judged `refUnreadable` (and suppresses `headRefMissing`).
|
|
30
|
+
*/
|
|
31
|
+
readonly headRefProblem: RefLoadProblem | null;
|
|
32
|
+
readonly isApp: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface IntegrityComputationInput {
|
|
36
|
+
readonly targetId: string;
|
|
37
|
+
readonly spaces: readonly IntegritySpaceState[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Walk the loaded model and return **every** integrity violation — never
|
|
42
|
+
* bailing at the first. Structurally-derivable violations (load-time
|
|
43
|
+
* problems, self-edges, missing / unreachable head refs) are always
|
|
44
|
+
* produced; layout-drift checks require `declaredExtensions`, and
|
|
45
|
+
* contract / target / disjointness checks require `checkContracts`.
|
|
46
|
+
*/
|
|
47
|
+
export function computeIntegrityViolations(
|
|
48
|
+
input: IntegrityComputationInput,
|
|
49
|
+
opts?: IntegrityQueryOptions,
|
|
50
|
+
): readonly IntegrityViolation[] {
|
|
51
|
+
const violations: IntegrityViolation[] = [];
|
|
52
|
+
|
|
53
|
+
for (const { member, problems, refProblems, headRefProblem, isApp } of input.spaces) {
|
|
54
|
+
const { spaceId } = member;
|
|
55
|
+
|
|
56
|
+
for (const problem of problems) {
|
|
57
|
+
violations.push(loadProblemToViolation(spaceId, problem));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const refProblem of refProblems) {
|
|
61
|
+
violations.push({
|
|
62
|
+
kind: 'refUnreadable',
|
|
63
|
+
spaceId,
|
|
64
|
+
refName: refProblem.refName,
|
|
65
|
+
detail: refProblem.detail,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (headRefProblem !== null) {
|
|
69
|
+
violations.push({
|
|
70
|
+
kind: 'refUnreadable',
|
|
71
|
+
spaceId,
|
|
72
|
+
refName: headRefProblem.refName,
|
|
73
|
+
detail: headRefProblem.detail,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const pkg of member.packages) {
|
|
78
|
+
const from = pkg.metadata.from ?? EMPTY_CONTRACT_HASH;
|
|
79
|
+
const isSelfEdge = from === pkg.metadata.to;
|
|
80
|
+
const hasDataOp = pkg.ops.some((op) => op.operationClass === 'data');
|
|
81
|
+
if (isSelfEdge && !hasDataOp) {
|
|
82
|
+
violations.push({ kind: 'sameSourceAndTarget', spaceId, dirName: pkg.dirName, hash: from });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
violations.push(...duplicateMigrationHashViolations(spaceId, member.packages));
|
|
87
|
+
|
|
88
|
+
// The app head ref is synthesised from the live contract, so it is
|
|
89
|
+
// always present and reachable; only extension spaces read their head
|
|
90
|
+
// ref from disk and can be missing or point outside the graph. A head
|
|
91
|
+
// ref that exists but is unparseable is already surfaced above as
|
|
92
|
+
// `refUnreadable`, so it is not also reported as `headRefMissing`.
|
|
93
|
+
if (!isApp && headRefProblem === null) {
|
|
94
|
+
if (member.headRef === null) {
|
|
95
|
+
violations.push({ kind: 'headRefMissing', spaceId });
|
|
96
|
+
} else if (!headRefPresentInGraph(member, member.headRef.hash)) {
|
|
97
|
+
violations.push({ kind: 'headRefNotInGraph', spaceId, hash: member.headRef.hash });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (opts?.declaredExtensions !== undefined) {
|
|
103
|
+
violations.push(...layoutViolations(input.spaces, opts.declaredExtensions));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (opts?.checkContracts === true) {
|
|
107
|
+
violations.push(...contractViolations(input));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return violations;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function loadProblemToViolation(
|
|
114
|
+
spaceId: string,
|
|
115
|
+
problem: PackageLoadProblem,
|
|
116
|
+
): IntegrityViolation {
|
|
117
|
+
switch (problem.kind) {
|
|
118
|
+
case 'hashMismatch':
|
|
119
|
+
return {
|
|
120
|
+
kind: 'hashMismatch',
|
|
121
|
+
spaceId,
|
|
122
|
+
dirName: problem.dirName,
|
|
123
|
+
stored: problem.stored,
|
|
124
|
+
computed: problem.computed,
|
|
125
|
+
};
|
|
126
|
+
case 'providedInvariantsMismatch':
|
|
127
|
+
return { kind: 'providedInvariantsMismatch', spaceId, dirName: problem.dirName };
|
|
128
|
+
case 'packageUnloadable':
|
|
129
|
+
return {
|
|
130
|
+
kind: 'packageUnloadable',
|
|
131
|
+
spaceId,
|
|
132
|
+
dirName: problem.dirName,
|
|
133
|
+
detail: problem.detail,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function duplicateMigrationHashViolations(
|
|
139
|
+
spaceId: string,
|
|
140
|
+
packages: readonly OnDiskMigrationPackage[],
|
|
141
|
+
): readonly IntegrityViolation[] {
|
|
142
|
+
const dirNamesByHash = new Map<string, string[]>();
|
|
143
|
+
for (const pkg of packages) {
|
|
144
|
+
const hash = pkg.metadata.migrationHash;
|
|
145
|
+
const dirNames = dirNamesByHash.get(hash);
|
|
146
|
+
if (dirNames) dirNames.push(pkg.dirName);
|
|
147
|
+
else dirNamesByHash.set(hash, [pkg.dirName]);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const out: IntegrityViolation[] = [];
|
|
151
|
+
for (const [migrationHash, dirNames] of dirNamesByHash) {
|
|
152
|
+
if (dirNames.length > 1) {
|
|
153
|
+
out.push({
|
|
154
|
+
kind: 'duplicateMigrationHash',
|
|
155
|
+
spaceId,
|
|
156
|
+
migrationHash,
|
|
157
|
+
dirNames: [...dirNames].sort(),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Whether a space's head-ref hash is present in its reconstructed graph.
|
|
166
|
+
* An empty graph is reachable only by the empty-contract sentinel.
|
|
167
|
+
*/
|
|
168
|
+
function headRefPresentInGraph(member: ContractSpaceMember, headHash: string): boolean {
|
|
169
|
+
const graph = member.graph();
|
|
170
|
+
if (graph.nodes.size === 0) {
|
|
171
|
+
return headHash === EMPTY_CONTRACT_HASH;
|
|
172
|
+
}
|
|
173
|
+
return graph.nodes.has(headHash);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function layoutViolations(
|
|
177
|
+
spaces: readonly IntegritySpaceState[],
|
|
178
|
+
declaredExtensions: readonly DeclaredExtensionEntry[],
|
|
179
|
+
): readonly IntegrityViolation[] {
|
|
180
|
+
const out: IntegrityViolation[] = [];
|
|
181
|
+
const extensionSpaceIds = new Set(spaces.filter((s) => !s.isApp).map((s) => s.member.spaceId));
|
|
182
|
+
const declaredIds = new Set(declaredExtensions.map((d) => d.id));
|
|
183
|
+
|
|
184
|
+
for (const id of [...extensionSpaceIds].sort()) {
|
|
185
|
+
if (!declaredIds.has(id)) {
|
|
186
|
+
out.push({ kind: 'orphanSpaceDir', spaceId: id });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const id of [...declaredIds].sort()) {
|
|
190
|
+
if (!extensionSpaceIds.has(id)) {
|
|
191
|
+
out.push({ kind: 'declaredButUnmigrated', spaceId: id });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function contractViolations(input: IntegrityComputationInput): readonly IntegrityViolation[] {
|
|
198
|
+
const out: IntegrityViolation[] = [];
|
|
199
|
+
const elementClaimedBy = new Map<string, string[]>();
|
|
200
|
+
|
|
201
|
+
for (const { member } of input.spaces) {
|
|
202
|
+
let contract: ReturnType<ContractSpaceMember['contract']>;
|
|
203
|
+
try {
|
|
204
|
+
contract = member.contract();
|
|
205
|
+
} catch (error) {
|
|
206
|
+
out.push({ kind: 'contractUnreadable', spaceId: member.spaceId, detail: detailOf(error) });
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (contract.target !== input.targetId) {
|
|
211
|
+
out.push({
|
|
212
|
+
kind: 'targetMismatch',
|
|
213
|
+
spaceId: member.spaceId,
|
|
214
|
+
expected: input.targetId,
|
|
215
|
+
actual: contract.target,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const elementName of extractStorageElementNames(contract)) {
|
|
220
|
+
const claimers = elementClaimedBy.get(elementName);
|
|
221
|
+
if (claimers) claimers.push(member.spaceId);
|
|
222
|
+
else elementClaimedBy.set(elementName, [member.spaceId]);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const disjointness: IntegrityViolation[] = [];
|
|
227
|
+
for (const [element, claimedBy] of elementClaimedBy) {
|
|
228
|
+
if (claimedBy.length > 1) {
|
|
229
|
+
disjointness.push({ kind: 'disjointness', element, claimedBy: [...claimedBy].sort() });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
disjointness.sort((a, b) =>
|
|
233
|
+
a.kind === 'disjointness' && b.kind === 'disjointness' ? a.element.localeCompare(b.element) : 0,
|
|
234
|
+
);
|
|
235
|
+
out.push(...disjointness);
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function detailOf(error: unknown): string {
|
|
240
|
+
if (MigrationToolsError.is(error)) return error.why;
|
|
241
|
+
if (error instanceof Error) return error.message;
|
|
242
|
+
return String(error);
|
|
243
|
+
}
|