@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,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inputs for {@link detectSpaceContractDrift}.
|
|
3
|
+
*
|
|
4
|
+
* Both hashes are produced by the caller (the SQL-family wiring at the
|
|
5
|
+
* consumption site) using the canonical contract hashing pipeline.
|
|
6
|
+
* Keeping the helper pure lets `migration-tools` stay framework-neutral
|
|
7
|
+
* — the SQL family already speaks `Contract<SqlStorage>`, the Mongo
|
|
8
|
+
* family speaks its own contract type, and both reduce to a hash string
|
|
9
|
+
* before drift detection runs.
|
|
10
|
+
*
|
|
11
|
+
* `priorHeadHash` is `null` when no `contract.json` exists yet on disk for
|
|
12
|
+
* the space (the descriptor declares an extension that has never been
|
|
13
|
+
* emitted into the user's repo). That's the "first emit" case — no
|
|
14
|
+
* drift to surface; the migrate emit will create the on-disk artefacts.
|
|
15
|
+
*/
|
|
16
|
+
export interface DetectSpaceContractDriftInputs {
|
|
17
|
+
readonly descriptorHash: string;
|
|
18
|
+
readonly priorHeadHash: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Result discriminant for {@link detectSpaceContractDrift}.
|
|
23
|
+
*
|
|
24
|
+
* - `noDrift`: descriptor hash and on-disk head hash agree byte-for-byte.
|
|
25
|
+
* The migrate emit can proceed with no warning.
|
|
26
|
+
* - `firstEmit`: no on-disk `contract.json` on disk yet. The extension
|
|
27
|
+
* was just added to `extensionPacks`; this run will create the
|
|
28
|
+
* on-disk artefacts. No warning either — the user's intent is to install
|
|
29
|
+
* the extension, not to "drift" from a state they haven't recorded.
|
|
30
|
+
* - `drift`: descriptor hash differs from on-disk head hash. The caller
|
|
31
|
+
* surfaces a non-fatal warning naming the extension and the
|
|
32
|
+
* diff direction (descriptor → on-disk head). The migrate emit proceeds
|
|
33
|
+
* normally so the bump is materialised this run; the warning just
|
|
34
|
+
* confirms the bump is being captured.
|
|
35
|
+
*
|
|
36
|
+
* `spaceId`, `descriptorHash`, and `priorHeadHash` are threaded through
|
|
37
|
+
* verbatim so the caller (logger / TerminalUI / strict-mode envelope)
|
|
38
|
+
* has everything it needs to format the warning message without
|
|
39
|
+
* re-reading the descriptor or the on-disk artefact.
|
|
40
|
+
*/
|
|
41
|
+
export type SpaceContractDriftResult = {
|
|
42
|
+
readonly kind: 'noDrift' | 'firstEmit' | 'drift';
|
|
43
|
+
readonly spaceId: string;
|
|
44
|
+
readonly descriptorHash: string;
|
|
45
|
+
readonly priorHeadHash: string | null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Pure drift-detection primitive for a single contract space.
|
|
50
|
+
*
|
|
51
|
+
* Runs once per loaded extension space, just before computing the
|
|
52
|
+
* `priorContract` that feeds {@link import('./plan-all-spaces').planAllSpaces}.
|
|
53
|
+
* Hash equality is byte-for-byte (no normalisation) — both sides are
|
|
54
|
+
* already canonical hashes produced by the same pipeline, so any
|
|
55
|
+
* difference is meaningful drift.
|
|
56
|
+
*
|
|
57
|
+
* Synchronous, pure, no I/O. The caller (SQL family) reads the on-disk
|
|
58
|
+
* `contract.json` and computes its hash, then invokes this helper
|
|
59
|
+
* alongside the descriptor's `headRef.hash`. Composes naturally with
|
|
60
|
+
* {@link import('./read-contract-space-head-ref').readContractSpaceHeadRef}
|
|
61
|
+
* which provides the read-side primitive.
|
|
62
|
+
*
|
|
63
|
+
* The drift warning surfaces the extension name and the diff direction.
|
|
64
|
+
*/
|
|
65
|
+
export function detectSpaceContractDrift(
|
|
66
|
+
spaceId: string,
|
|
67
|
+
inputs: DetectSpaceContractDriftInputs,
|
|
68
|
+
): SpaceContractDriftResult {
|
|
69
|
+
if (inputs.priorHeadHash === null) {
|
|
70
|
+
return {
|
|
71
|
+
kind: 'firstEmit',
|
|
72
|
+
spaceId,
|
|
73
|
+
descriptorHash: inputs.descriptorHash,
|
|
74
|
+
priorHeadHash: null,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (inputs.descriptorHash === inputs.priorHeadHash) {
|
|
78
|
+
return {
|
|
79
|
+
kind: 'noDrift',
|
|
80
|
+
spaceId,
|
|
81
|
+
descriptorHash: inputs.descriptorHash,
|
|
82
|
+
priorHeadHash: inputs.priorHeadHash,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
kind: 'drift',
|
|
87
|
+
spaceId,
|
|
88
|
+
descriptorHash: inputs.descriptorHash,
|
|
89
|
+
priorHeadHash: inputs.priorHeadHash,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'pathe';
|
|
3
|
+
import { canonicalizeJson } from './canonicalize-json';
|
|
4
|
+
import type { ContractSpaceHeadRef } from './read-contract-space-head-ref';
|
|
5
|
+
import { assertValidSpaceId } from './space-layout';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Inputs for {@link emitContractSpaceArtefacts}.
|
|
9
|
+
*
|
|
10
|
+
* - `contract` is the canonical contract value the framework just emitted
|
|
11
|
+
* for the space; it is serialised through {@link canonicalizeJson}, so
|
|
12
|
+
* it must be a JSON-compatible value (objects / arrays / primitives).
|
|
13
|
+
* Typed as `unknown` rather than the SQL-family `Contract<SqlStorage>`
|
|
14
|
+
* to keep `migration-tools` framework-neutral; SQL-family callers pass
|
|
15
|
+
* their typed value through unchanged.
|
|
16
|
+
*
|
|
17
|
+
* - `contractDts` is the pre-rendered `.d.ts` text. Rendering happens in
|
|
18
|
+
* the SQL family (which owns the codec / typemap input the renderer
|
|
19
|
+
* needs), so this helper accepts the text verbatim and writes it out
|
|
20
|
+
* without further transformation.
|
|
21
|
+
*
|
|
22
|
+
* - `headRef` is the head reference for the space.
|
|
23
|
+
* `invariants` are sorted alphabetically before serialisation so two
|
|
24
|
+
* callers passing the same set in different orders produce
|
|
25
|
+
* byte-identical `refs/head.json`.
|
|
26
|
+
*/
|
|
27
|
+
export interface ContractSpaceArtefactInputs {
|
|
28
|
+
readonly contract: unknown;
|
|
29
|
+
readonly contractDts: string;
|
|
30
|
+
readonly headRef: ContractSpaceHeadRef;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Emit the per-space artefacts (`contract.json`, `contract.d.ts`,
|
|
35
|
+
* `refs/head.json`) under `<projectMigrationsDir>/<spaceId>/`.
|
|
36
|
+
*
|
|
37
|
+
* Always-overwrite: the framework owns these files; running `migrate`
|
|
38
|
+
* twice with the same inputs is a no-op observably (idempotent), but the
|
|
39
|
+
* helper does not check pre-existing contents — re-emit always wins.
|
|
40
|
+
*
|
|
41
|
+
* Path layout matches the convention in
|
|
42
|
+
* [`spaceMigrationDirectory`](./space-layout.ts). The space id is
|
|
43
|
+
* validated against `[a-z][a-z0-9_-]{0,63}` via
|
|
44
|
+
* {@link assertValidSpaceId} for filesystem-safety reasons; the helper
|
|
45
|
+
* accepts every space uniformly (including the app space, default
|
|
46
|
+
* `'app'`).
|
|
47
|
+
*
|
|
48
|
+
* The migrations directory and space subdirectory are created if they
|
|
49
|
+
* do not yet exist (`mkdir { recursive: true }`).
|
|
50
|
+
*/
|
|
51
|
+
export async function emitContractSpaceArtefacts(
|
|
52
|
+
projectMigrationsDir: string,
|
|
53
|
+
spaceId: string,
|
|
54
|
+
inputs: ContractSpaceArtefactInputs,
|
|
55
|
+
): Promise<void> {
|
|
56
|
+
assertValidSpaceId(spaceId);
|
|
57
|
+
|
|
58
|
+
const dir = join(projectMigrationsDir, spaceId);
|
|
59
|
+
await mkdir(join(dir, 'refs'), { recursive: true });
|
|
60
|
+
|
|
61
|
+
await writeFile(join(dir, 'contract.json'), `${canonicalizeJson(inputs.contract)}\n`);
|
|
62
|
+
await writeFile(join(dir, 'contract.d.ts'), inputs.contractDts);
|
|
63
|
+
|
|
64
|
+
const sortedInvariants = [...inputs.headRef.invariants].sort();
|
|
65
|
+
const headJson = canonicalizeJson({
|
|
66
|
+
hash: inputs.headRef.hash,
|
|
67
|
+
invariants: sortedInvariants,
|
|
68
|
+
});
|
|
69
|
+
await writeFile(join(dir, 'refs', 'head.json'), `${headJson}\n`);
|
|
70
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
2
|
+
import { basename, dirname, relative } from 'pathe';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build the canonical "re-emit this package" remediation hint.
|
|
6
|
+
*
|
|
7
|
+
* Every on-disk migration package ships its own `migration.ts` author-time
|
|
8
|
+
* file. Running it regenerates `migration.json` and `ops.json` with the
|
|
9
|
+
* correct hash + metadata, so it is the right primitive whenever a single
|
|
10
|
+
* package's on-disk artifacts are missing, malformed, or otherwise corrupt.
|
|
11
|
+
* Pointing users at `migration plan` would emit a *new* package rather than
|
|
12
|
+
* heal the broken one.
|
|
13
|
+
*/
|
|
14
|
+
function reemitHint(dir: string, fallback?: string): string {
|
|
15
|
+
const relativeDir = relative(process.cwd(), dir);
|
|
16
|
+
const reemit = `Re-emit the package by running \`node "${relativeDir}/migration.ts"\``;
|
|
17
|
+
return fallback ? `${reemit}, ${fallback}` : `${reemit}.`;
|
|
18
|
+
}
|
|
19
|
+
|
|
1
20
|
/**
|
|
2
21
|
* Structured error for migration tooling operations.
|
|
3
22
|
*
|
|
4
23
|
* Follows the NAMESPACE.SUBCODE convention from ADR 027. All codes live under
|
|
5
|
-
* the MIGRATION namespace. These are tooling-time errors (file I/O,
|
|
6
|
-
* migration history reconstruction), distinct from the runtime
|
|
7
|
-
* failures (PRECHECK_FAILED, POSTCHECK_FAILED,
|
|
24
|
+
* the MIGRATION namespace. These are tooling-time errors (file I/O, hash
|
|
25
|
+
* verification, migration history reconstruction), distinct from the runtime
|
|
26
|
+
* MIGRATION.* codes for apply-time failures (PRECHECK_FAILED, POSTCHECK_FAILED,
|
|
27
|
+
* etc.).
|
|
8
28
|
*
|
|
9
29
|
* Fields:
|
|
10
30
|
* - code: Stable machine-readable code (MIGRATION.SUBCODE)
|
|
@@ -55,7 +75,10 @@ export function errorDirectoryExists(dir: string): MigrationToolsError {
|
|
|
55
75
|
export function errorMissingFile(file: string, dir: string): MigrationToolsError {
|
|
56
76
|
return new MigrationToolsError('MIGRATION.FILE_MISSING', `Missing ${file}`, {
|
|
57
77
|
why: `Expected "${file}" in "${dir}" but the file does not exist.`,
|
|
58
|
-
fix:
|
|
78
|
+
fix: reemitHint(
|
|
79
|
+
dir,
|
|
80
|
+
'or delete the directory if the migration is unwanted and the source TypeScript is gone.',
|
|
81
|
+
),
|
|
59
82
|
details: { file, dir },
|
|
60
83
|
});
|
|
61
84
|
}
|
|
@@ -63,19 +86,52 @@ export function errorMissingFile(file: string, dir: string): MigrationToolsError
|
|
|
63
86
|
export function errorInvalidJson(filePath: string, parseError: string): MigrationToolsError {
|
|
64
87
|
return new MigrationToolsError('MIGRATION.INVALID_JSON', 'Invalid JSON in migration file', {
|
|
65
88
|
why: `Failed to parse "${filePath}": ${parseError}`,
|
|
66
|
-
fix:
|
|
89
|
+
fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),
|
|
67
90
|
details: { filePath, parseError },
|
|
68
91
|
});
|
|
69
92
|
}
|
|
70
93
|
|
|
71
94
|
export function errorInvalidManifest(filePath: string, reason: string): MigrationToolsError {
|
|
72
95
|
return new MigrationToolsError('MIGRATION.INVALID_MANIFEST', 'Invalid migration manifest', {
|
|
73
|
-
why: `
|
|
74
|
-
fix:
|
|
96
|
+
why: `Migration manifest at "${filePath}" is invalid: ${reason}`,
|
|
97
|
+
fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),
|
|
75
98
|
details: { filePath, reason },
|
|
76
99
|
});
|
|
77
100
|
}
|
|
78
101
|
|
|
102
|
+
export function errorInvalidOperationEntry(index: number, reason: string): MigrationToolsError {
|
|
103
|
+
return new MigrationToolsError(
|
|
104
|
+
'MIGRATION.INVALID_OPERATION_ENTRY',
|
|
105
|
+
'Migration operation entry is malformed',
|
|
106
|
+
{
|
|
107
|
+
why: `Operation at index ${index} returned by the migration class failed schema validation: ${reason}.`,
|
|
108
|
+
fix: "Update the migration class so each entry of `operations` carries `id` (string), `label` (string), and `operationClass` (one of 'additive' | 'widening' | 'destructive' | 'data').",
|
|
109
|
+
details: { index, reason },
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function errorStaleContractBookends(args: {
|
|
115
|
+
readonly side: 'from' | 'to';
|
|
116
|
+
readonly metaHash: string | null;
|
|
117
|
+
readonly contractHash: string;
|
|
118
|
+
}): MigrationToolsError {
|
|
119
|
+
const { side, metaHash, contractHash } = args;
|
|
120
|
+
// `meta.from` is `string | null` (null = baseline). Render `null` as a
|
|
121
|
+
// human-readable token in the diagnostic so the message stays clear when
|
|
122
|
+
// the mismatch is a baseline-vs-non-baseline disagreement.
|
|
123
|
+
const renderedMetaHash = metaHash === null ? 'null (baseline)' : `"${metaHash}"`;
|
|
124
|
+
return new MigrationToolsError(
|
|
125
|
+
'MIGRATION.STALE_CONTRACT_BOOKENDS',
|
|
126
|
+
'Migration manifest contract bookends disagree with describe()',
|
|
127
|
+
{
|
|
128
|
+
why: `migration.json stores ${side}Contract.storage.storageHash "${contractHash}", but describe() returned meta.${side} = ${renderedMetaHash}. The bookend is stale — most likely the migration's describe() was edited after the package was scaffolded by \`migration plan\`.`,
|
|
129
|
+
fix: 'Re-run `migration plan` to regenerate the package with fresh contract bookends, or restore the directory from version control.',
|
|
130
|
+
details: { side, metaHash, contractHash },
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
79
135
|
export function errorInvalidSlug(slug: string): MigrationToolsError {
|
|
80
136
|
return new MigrationToolsError('MIGRATION.INVALID_NAME', 'Invalid migration name', {
|
|
81
137
|
why: `The slug "${slug}" contains no valid characters after sanitization (only a-z, 0-9 are kept).`,
|
|
@@ -92,13 +148,58 @@ export function errorInvalidDestName(destName: string): MigrationToolsError {
|
|
|
92
148
|
});
|
|
93
149
|
}
|
|
94
150
|
|
|
95
|
-
export function
|
|
151
|
+
export function errorInvalidSpaceId(spaceId: string): MigrationToolsError {
|
|
152
|
+
return new MigrationToolsError(
|
|
153
|
+
'MIGRATION.INVALID_SPACE_ID',
|
|
154
|
+
'Invalid contract space identifier',
|
|
155
|
+
{
|
|
156
|
+
why: `The space id "${spaceId}" does not match the required pattern /^[a-z][a-z0-9_-]{0,63}$/. Space ids are used as filesystem directory names under \`migrations/\`, so the pattern is conservative on purpose.`,
|
|
157
|
+
fix: 'Pick a lowercase identifier that begins with a letter and contains only lowercase letters, digits, hyphens, or underscores; max 64 characters total.',
|
|
158
|
+
details: { spaceId },
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function errorDescriptorHeadHashMismatch(args: {
|
|
164
|
+
readonly extensionId: string;
|
|
165
|
+
readonly recomputedHash: string;
|
|
166
|
+
readonly headRefHash: string;
|
|
167
|
+
}): MigrationToolsError {
|
|
168
|
+
const { extensionId, recomputedHash, headRefHash } = args;
|
|
169
|
+
return new MigrationToolsError(
|
|
170
|
+
'MIGRATION.DESCRIPTOR_HEAD_HASH_MISMATCH',
|
|
171
|
+
"Extension descriptor's headRef.hash does not match its contractJson",
|
|
172
|
+
{
|
|
173
|
+
why: `Extension "${extensionId}" publishes a \`contractSpace\` whose \`headRef.hash\` (${headRefHash}) does not match the canonical hash recomputed from \`contractSpace.contractJson\` (${recomputedHash}). This means the extension descriptor was published with stale \`headRef.hash\` — typically because the contract was bumped without rerunning the extension's emit pipeline.`,
|
|
174
|
+
fix: 'Re-run the extension authoring pipeline so `contractJson.storage.storageHash` and `headRef.hash` agree, then republish the extension. If you are the extension author and you intentionally bumped `contractJson`, recompute and update `headRef.hash` (and refresh any on-disk migration metadata that derives from it).',
|
|
175
|
+
details: { extensionId, recomputedHash, headRefHash },
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function errorDuplicateSpaceId(spaceId: string): MigrationToolsError {
|
|
181
|
+
return new MigrationToolsError(
|
|
182
|
+
'MIGRATION.DUPLICATE_SPACE_ID',
|
|
183
|
+
'Duplicate contract space identifier',
|
|
184
|
+
{
|
|
185
|
+
why: `The space id "${spaceId}" appears more than once in the per-space planner input. Each space id must be unique across the inputs (the per-space planner emits one output entry per id).`,
|
|
186
|
+
fix: 'Deduplicate the inputs before passing them to `planAllSpaces` — typically by checking your `extensionPacks` declaration for repeated entries.',
|
|
187
|
+
details: { spaceId },
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function errorSameSourceAndTarget(dir: string, hash: string): MigrationToolsError {
|
|
193
|
+
const dirName = basename(dir);
|
|
96
194
|
return new MigrationToolsError(
|
|
97
195
|
'MIGRATION.SAME_SOURCE_AND_TARGET',
|
|
98
|
-
'Migration has same source and target',
|
|
196
|
+
'Migration without data-transform operations has same source and target',
|
|
99
197
|
{
|
|
100
|
-
why: `Migration "${dirName}" has from === to === "${hash}".
|
|
101
|
-
fix:
|
|
198
|
+
why: `Migration "${dirName}" has from === to === "${hash}" and declares no data-transform operations. Self-edges are only allowed when the migration runs at least one dataTransform — otherwise the migration is a no-op.`,
|
|
199
|
+
fix: reemitHint(
|
|
200
|
+
dir,
|
|
201
|
+
'and either change the contract so from ≠ to, add a dataTransform op, or delete the directory if the migration is unwanted.',
|
|
202
|
+
),
|
|
102
203
|
details: { dirName, hash },
|
|
103
204
|
},
|
|
104
205
|
);
|
|
@@ -175,14 +276,147 @@ export function errorInvalidRefValue(value: string): MigrationToolsError {
|
|
|
175
276
|
});
|
|
176
277
|
}
|
|
177
278
|
|
|
178
|
-
export function
|
|
279
|
+
export function errorDuplicateMigrationHash(migrationHash: string): MigrationToolsError {
|
|
179
280
|
return new MigrationToolsError(
|
|
180
|
-
'MIGRATION.
|
|
181
|
-
'Duplicate
|
|
281
|
+
'MIGRATION.DUPLICATE_MIGRATION_HASH',
|
|
282
|
+
'Duplicate migrationHash in migration graph',
|
|
182
283
|
{
|
|
183
|
-
why: `Multiple migrations share
|
|
184
|
-
fix: 'Regenerate one of the conflicting migrations so each
|
|
185
|
-
details: {
|
|
284
|
+
why: `Multiple migrations share migrationHash "${migrationHash}". Each migration must have a unique content-addressed identity.`,
|
|
285
|
+
fix: 'Regenerate one of the conflicting migrations so each migrationHash is unique, then re-run migration commands.',
|
|
286
|
+
details: { migrationHash },
|
|
186
287
|
},
|
|
187
288
|
);
|
|
188
289
|
}
|
|
290
|
+
|
|
291
|
+
export function errorInvalidInvariantId(invariantId: string): MigrationToolsError {
|
|
292
|
+
return new MigrationToolsError('MIGRATION.INVALID_INVARIANT_ID', 'Invalid invariantId', {
|
|
293
|
+
why: `invariantId ${JSON.stringify(invariantId)} is invalid. Ids must be non-empty and contain no whitespace or control characters (including Unicode whitespace like NBSP); other content (kebab-case, camelCase, namespaced, Unicode letters) is allowed.`,
|
|
294
|
+
fix: 'Pick an invariantId without spaces, tabs, newlines, or control characters — e.g. "backfill-user-phone", "users/backfill-phone", or "BackfillUserPhone".',
|
|
295
|
+
details: { invariantId },
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function errorDuplicateInvariantInEdge(invariantId: string): MigrationToolsError {
|
|
300
|
+
return new MigrationToolsError(
|
|
301
|
+
'MIGRATION.DUPLICATE_INVARIANT_IN_EDGE',
|
|
302
|
+
'Duplicate invariantId on a single migration',
|
|
303
|
+
{
|
|
304
|
+
why: `invariantId "${invariantId}" is declared by more than one dataTransform on the same migration. The marker stores invariants as a set and the routing layer treats them as edge-level, so two ops cannot share a routing identity.`,
|
|
305
|
+
fix: 'Rename one of the conflicting dataTransform invariantIds, or drop invariantId on the op that does not need to be routing-visible.',
|
|
306
|
+
details: { invariantId },
|
|
307
|
+
},
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function errorProvidedInvariantsMismatch(
|
|
312
|
+
filePath: string,
|
|
313
|
+
stored: readonly string[],
|
|
314
|
+
derived: readonly string[],
|
|
315
|
+
): MigrationToolsError {
|
|
316
|
+
const storedSet = new Set(stored);
|
|
317
|
+
const derivedSet = new Set(derived);
|
|
318
|
+
const missing = [...derivedSet].filter((id) => !storedSet.has(id));
|
|
319
|
+
const extra = [...storedSet].filter((id) => !derivedSet.has(id));
|
|
320
|
+
// When sets agree but arrays don't, the only difference is ordering — call
|
|
321
|
+
// it out so the reader doesn't stare at two visually-identical arrays.
|
|
322
|
+
// Canonical providedInvariants is sorted ascending; a manifest with the
|
|
323
|
+
// same ids in a different order is still a mismatch (the hash check would
|
|
324
|
+
// also fail), but the human-readable diagnostic is otherwise unhelpful.
|
|
325
|
+
const orderingOnly = missing.length === 0 && extra.length === 0;
|
|
326
|
+
const why = orderingOnly
|
|
327
|
+
? `migration.json at "${filePath}" stores providedInvariants ${JSON.stringify(stored)}, but the canonical value derived from ops.json is ${JSON.stringify(derived)} — same ids, different order. Canonical providedInvariants is sorted ascending.`
|
|
328
|
+
: `migration.json at "${filePath}" stores providedInvariants ${JSON.stringify(stored)}, but the value derived from ops.json is ${JSON.stringify(derived)}. The manifest copy was likely hand-edited without re-emitting.`;
|
|
329
|
+
return new MigrationToolsError(
|
|
330
|
+
'MIGRATION.PROVIDED_INVARIANTS_MISMATCH',
|
|
331
|
+
'providedInvariants on migration.json disagrees with ops.json',
|
|
332
|
+
{
|
|
333
|
+
why,
|
|
334
|
+
fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),
|
|
335
|
+
details: { filePath, stored, derived, difference: { missing, extra } },
|
|
336
|
+
},
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Wire-shape edge surfaced through the JSON envelope's
|
|
342
|
+
* `meta.structuralPath` of `MIGRATION.NO_INVARIANT_PATH`. Slim by design —
|
|
343
|
+
* authoring metadata (`createdAt`, `labels`) lives on `MigrationEdge` but
|
|
344
|
+
* is intentionally dropped here so the envelope stays stable across
|
|
345
|
+
* graph-internal refactors.
|
|
346
|
+
*
|
|
347
|
+
* Stability: any field added here is part of the public CLI JSON contract.
|
|
348
|
+
* Callers (CLI consumers, agents) must be able to treat
|
|
349
|
+
* `(dirName, migrationHash, from, to, invariants)` as the canonical shape.
|
|
350
|
+
*/
|
|
351
|
+
export interface NoInvariantPathStructuralEdge {
|
|
352
|
+
readonly dirName: string;
|
|
353
|
+
readonly migrationHash: string;
|
|
354
|
+
readonly from: string;
|
|
355
|
+
readonly to: string;
|
|
356
|
+
readonly invariants: readonly string[];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function errorNoInvariantPath(args: {
|
|
360
|
+
readonly refName?: string;
|
|
361
|
+
readonly required: readonly string[];
|
|
362
|
+
readonly missing: readonly string[];
|
|
363
|
+
readonly structuralPath: readonly NoInvariantPathStructuralEdge[];
|
|
364
|
+
}): MigrationToolsError {
|
|
365
|
+
const { refName, required, missing, structuralPath } = args;
|
|
366
|
+
const refClause = refName ? `Ref "${refName}"` : 'Target';
|
|
367
|
+
const missingList = missing.map((id) => JSON.stringify(id)).join(', ');
|
|
368
|
+
const requiredList = required.map((id) => JSON.stringify(id)).join(', ');
|
|
369
|
+
return new MigrationToolsError(
|
|
370
|
+
'MIGRATION.NO_INVARIANT_PATH',
|
|
371
|
+
'No path covers the required invariants',
|
|
372
|
+
{
|
|
373
|
+
why: `${refClause} requires invariants the reachable path doesn't cover. required=[${requiredList}], missing=[${missingList}].`,
|
|
374
|
+
fix: 'Add a migration on the path that runs `dataTransform({ invariantId: "<id>", … })` for each missing invariant, or retarget the ref to a hash whose path already provides them.',
|
|
375
|
+
details: {
|
|
376
|
+
required,
|
|
377
|
+
missing,
|
|
378
|
+
structuralPath,
|
|
379
|
+
...ifDefined('refName', refName),
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function errorUnknownInvariant(args: {
|
|
386
|
+
readonly refName?: string;
|
|
387
|
+
readonly unknown: readonly string[];
|
|
388
|
+
readonly declared: readonly string[];
|
|
389
|
+
}): MigrationToolsError {
|
|
390
|
+
const { refName, unknown, declared } = args;
|
|
391
|
+
const refClause = refName ? `Ref "${refName}" declares` : 'Declares';
|
|
392
|
+
const unknownList = unknown.map((id) => JSON.stringify(id)).join(', ');
|
|
393
|
+
return new MigrationToolsError(
|
|
394
|
+
'MIGRATION.UNKNOWN_INVARIANT',
|
|
395
|
+
'Ref declares invariants no migration in the graph provides',
|
|
396
|
+
{
|
|
397
|
+
why: `${refClause} invariants no migration in the graph provides. unknown=[${unknownList}].`,
|
|
398
|
+
fix: 'Either the ref has a typo, or the declaring migration has not been authored/attested yet. Re-check the ref file and the migrations directory.',
|
|
399
|
+
details: {
|
|
400
|
+
unknown,
|
|
401
|
+
declared,
|
|
402
|
+
...ifDefined('refName', refName),
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function errorMigrationHashMismatch(
|
|
409
|
+
dir: string,
|
|
410
|
+
storedHash: string,
|
|
411
|
+
computedHash: string,
|
|
412
|
+
): MigrationToolsError {
|
|
413
|
+
// Render a cwd-relative path in the human-readable diagnostic so users
|
|
414
|
+
// running CLI commands from the project root see a familiar short path.
|
|
415
|
+
// Keep the absolute path in `details.dir` for machine consumers.
|
|
416
|
+
const relativeDir = relative(process.cwd(), dir);
|
|
417
|
+
return new MigrationToolsError('MIGRATION.HASH_MISMATCH', 'Migration package is corrupt', {
|
|
418
|
+
why: `Stored migrationHash "${storedHash}" does not match the recomputed hash "${computedHash}" for "${relativeDir}". The migration.json or ops.json has been edited or partially written since emit.`,
|
|
419
|
+
fix: reemitHint(dir, 'or restore the directory from version control.'),
|
|
420
|
+
details: { dir, storedHash, computedHash },
|
|
421
|
+
});
|
|
422
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type AggregateContractHasher,
|
|
3
|
+
type DeclaredExtensionEntry,
|
|
4
|
+
type LayoutViolation,
|
|
5
|
+
type LoadAggregateError,
|
|
6
|
+
type LoadAggregateInput,
|
|
7
|
+
type LoadAggregateOutput,
|
|
8
|
+
loadContractSpaceAggregate,
|
|
9
|
+
} from '../aggregate/loader';
|
|
10
|
+
export type { ContractMarkerRecordLike } from '../aggregate/marker-types';
|
|
11
|
+
export {
|
|
12
|
+
type AggregateCurrentDBState,
|
|
13
|
+
type AggregatePerSpacePlan,
|
|
14
|
+
type AggregatePlannerError,
|
|
15
|
+
type AggregatePlannerInput,
|
|
16
|
+
type AggregatePlannerOutput,
|
|
17
|
+
type AggregatePlannerSuccess,
|
|
18
|
+
type CallerPolicy,
|
|
19
|
+
planAggregate,
|
|
20
|
+
} from '../aggregate/planner';
|
|
21
|
+
export { projectSchemaToSpace } from '../aggregate/project-schema-to-space';
|
|
22
|
+
export {
|
|
23
|
+
type GraphWalkOutcome,
|
|
24
|
+
type GraphWalkStrategyInputs,
|
|
25
|
+
graphWalkStrategy,
|
|
26
|
+
} from '../aggregate/strategies/graph-walk';
|
|
27
|
+
export type {
|
|
28
|
+
ContractSpaceAggregate,
|
|
29
|
+
ContractSpaceMember,
|
|
30
|
+
HydratedMigrationGraph,
|
|
31
|
+
} from '../aggregate/types';
|
|
32
|
+
export {
|
|
33
|
+
type AggregateVerifierError,
|
|
34
|
+
type AggregateVerifierInput,
|
|
35
|
+
type AggregateVerifierOutput,
|
|
36
|
+
type AggregateVerifierSuccess,
|
|
37
|
+
type MarkerCheckResult,
|
|
38
|
+
type MarkerCheckSection,
|
|
39
|
+
type OrphanElement,
|
|
40
|
+
type SchemaCheckSection,
|
|
41
|
+
verifyAggregate,
|
|
42
|
+
} from '../aggregate/verifier';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { MigrationEdge, MigrationGraph } from '../graph';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { deriveProvidedInvariants, validateInvariantId } from '../invariants';
|
package/src/exports/io.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export {
|
|
2
2
|
copyFilesWithRename,
|
|
3
3
|
formatMigrationDirName,
|
|
4
|
+
materialiseExtensionMigrationPackageIfMissing,
|
|
5
|
+
materialiseMigrationPackage,
|
|
4
6
|
readMigrationPackage,
|
|
5
7
|
readMigrationsDir,
|
|
6
|
-
|
|
8
|
+
writeMigrationMetadata,
|
|
7
9
|
writeMigrationOps,
|
|
8
10
|
writeMigrationPackage,
|
|
9
11
|
} from '../io';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { MigrationHints, MigrationMetadata } from '../metadata';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { PathDecision } from '../
|
|
1
|
+
export type { PathDecision } from '../migration-graph';
|
|
2
2
|
export {
|
|
3
3
|
detectCycles,
|
|
4
4
|
detectOrphans,
|
|
@@ -6,6 +6,7 @@ export {
|
|
|
6
6
|
findLeaf,
|
|
7
7
|
findPath,
|
|
8
8
|
findPathWithDecision,
|
|
9
|
+
findPathWithInvariants,
|
|
9
10
|
findReachableLeaves,
|
|
10
11
|
reconstructGraph,
|
|
11
|
-
} from '../
|
|
12
|
+
} from '../migration-graph';
|
package/src/exports/migration.ts
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export {
|
|
2
|
+
assertDescriptorSelfConsistency,
|
|
3
|
+
type DescriptorSelfConsistencyInputs,
|
|
4
|
+
} from '../assert-descriptor-self-consistency';
|
|
5
|
+
export {
|
|
6
|
+
type ComputeExtensionSpaceApplyPathInputs,
|
|
7
|
+
computeExtensionSpaceApplyPath,
|
|
8
|
+
type ExtensionSpaceApplyPathOutcome,
|
|
9
|
+
} from '../compute-extension-space-apply-path';
|
|
10
|
+
export type { SpaceApplyInput } from '../concatenate-space-apply-inputs';
|
|
11
|
+
export { contractSpaceFromJson } from '../contract-space-from-json';
|
|
12
|
+
export {
|
|
13
|
+
type DetectSpaceContractDriftInputs,
|
|
14
|
+
detectSpaceContractDrift,
|
|
15
|
+
type SpaceContractDriftResult,
|
|
16
|
+
} from '../detect-space-contract-drift';
|
|
17
|
+
export {
|
|
18
|
+
type ContractSpaceArtefactInputs,
|
|
19
|
+
emitContractSpaceArtefacts,
|
|
20
|
+
} from '../emit-contract-space-artefacts';
|
|
21
|
+
export {
|
|
22
|
+
type DiskContractSpaceState,
|
|
23
|
+
gatherDiskContractSpaceState,
|
|
24
|
+
} from '../gather-disk-contract-space-state';
|
|
25
|
+
export {
|
|
26
|
+
planAllSpaces,
|
|
27
|
+
type SpacePlanInput,
|
|
28
|
+
type SpacePlanOutput,
|
|
29
|
+
} from '../plan-all-spaces';
|
|
30
|
+
export { readContractSpaceContract } from '../read-contract-space-contract';
|
|
31
|
+
export {
|
|
32
|
+
type ContractSpaceHeadRef,
|
|
33
|
+
readContractSpaceHeadRef,
|
|
34
|
+
} from '../read-contract-space-head-ref';
|
|
35
|
+
export {
|
|
36
|
+
APP_SPACE_ID,
|
|
37
|
+
assertValidSpaceId,
|
|
38
|
+
isValidSpaceId,
|
|
39
|
+
spaceMigrationDirectory,
|
|
40
|
+
type ValidSpaceId,
|
|
41
|
+
} from '../space-layout';
|
|
42
|
+
export {
|
|
43
|
+
type ContractSpaceHeadRecord,
|
|
44
|
+
listContractSpaceDirectories,
|
|
45
|
+
type SpaceMarkerRecord,
|
|
46
|
+
type SpaceVerifierViolation,
|
|
47
|
+
type VerifyContractSpacesInputs,
|
|
48
|
+
type VerifyContractSpacesResult,
|
|
49
|
+
verifyContractSpaces,
|
|
50
|
+
} from '../verify-contract-spaces';
|