@prisma-next/migration-tools 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +328 -204
- package/dist/exports/aggregate.d.mts.map +1 -1
- package/dist/exports/aggregate.mjs +480 -243
- 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/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 +18 -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 +14 -14
- package/src/aggregate/planner.ts +20 -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 +4 -4
- 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 +29 -19
- package/src/exports/io.ts +2 -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/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
package/src/migration-graph.ts
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
2
2
|
import { EMPTY_CONTRACT_HASH } from './constants';
|
|
3
|
-
import {
|
|
4
|
-
errorAmbiguousTarget,
|
|
5
|
-
errorDuplicateMigrationHash,
|
|
6
|
-
errorNoInitialMigration,
|
|
7
|
-
errorNoTarget,
|
|
8
|
-
errorSameSourceAndTarget,
|
|
9
|
-
} from './errors';
|
|
3
|
+
import { errorAmbiguousTarget, errorNoInitialMigration, errorNoTarget } from './errors';
|
|
10
4
|
import type { MigrationEdge, MigrationGraph } from './graph';
|
|
11
5
|
import { bfs } from './graph-ops';
|
|
12
6
|
import type { OnDiskMigrationPackage } from './package';
|
|
@@ -50,13 +44,6 @@ export function reconstructGraph(packages: readonly OnDiskMigrationPackage[]): M
|
|
|
50
44
|
const from = pkg.metadata.from ?? EMPTY_CONTRACT_HASH;
|
|
51
45
|
const { to } = pkg.metadata;
|
|
52
46
|
|
|
53
|
-
if (from === to) {
|
|
54
|
-
const hasDataOp = pkg.ops.some((op) => op.operationClass === 'data');
|
|
55
|
-
if (!hasDataOp) {
|
|
56
|
-
throw errorSameSourceAndTarget(pkg.dirPath, from);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
47
|
nodes.add(from);
|
|
61
48
|
nodes.add(to);
|
|
62
49
|
|
|
@@ -66,14 +53,12 @@ export function reconstructGraph(packages: readonly OnDiskMigrationPackage[]): M
|
|
|
66
53
|
migrationHash: pkg.metadata.migrationHash,
|
|
67
54
|
dirName: pkg.dirName,
|
|
68
55
|
createdAt: pkg.metadata.createdAt,
|
|
69
|
-
labels: pkg.metadata.labels,
|
|
70
56
|
invariants: pkg.metadata.providedInvariants,
|
|
71
57
|
};
|
|
72
58
|
|
|
73
|
-
if (migrationByHash.has(migration.migrationHash)) {
|
|
74
|
-
|
|
59
|
+
if (!migrationByHash.has(migration.migrationHash)) {
|
|
60
|
+
migrationByHash.set(migration.migrationHash, migration);
|
|
75
61
|
}
|
|
76
|
-
migrationByHash.set(migration.migrationHash, migration);
|
|
77
62
|
|
|
78
63
|
appendEdge(forwardChain, from, migration);
|
|
79
64
|
appendEdge(reverseChain, to, migration);
|
|
@@ -85,23 +70,10 @@ export function reconstructGraph(packages: readonly OnDiskMigrationPackage[]): M
|
|
|
85
70
|
// ---------------------------------------------------------------------------
|
|
86
71
|
// Deterministic tie-breaking for BFS neighbour order.
|
|
87
72
|
// Used by path-finders only; not a general-purpose utility.
|
|
88
|
-
// Ordering:
|
|
73
|
+
// Ordering: createdAt → to → migrationHash.
|
|
89
74
|
// ---------------------------------------------------------------------------
|
|
90
75
|
|
|
91
|
-
const LABEL_PRIORITY: Record<string, number> = { main: 0, default: 1, feature: 2 };
|
|
92
|
-
|
|
93
|
-
function labelPriority(labels: readonly string[]): number {
|
|
94
|
-
let best = 3;
|
|
95
|
-
for (const l of labels) {
|
|
96
|
-
const p = LABEL_PRIORITY[l];
|
|
97
|
-
if (p !== undefined && p < best) best = p;
|
|
98
|
-
}
|
|
99
|
-
return best;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
76
|
function compareTieBreak(a: MigrationEdge, b: MigrationEdge): number {
|
|
103
|
-
const lp = labelPriority(a.labels) - labelPriority(b.labels);
|
|
104
|
-
if (lp !== 0) return lp;
|
|
105
77
|
const ca = a.createdAt.localeCompare(b.createdAt);
|
|
106
78
|
if (ca !== 0) return ca;
|
|
107
79
|
const tc = a.to.localeCompare(b.to);
|
|
@@ -119,7 +91,7 @@ function sortedNeighbors(edges: readonly MigrationEdge[]): readonly MigrationEdg
|
|
|
119
91
|
* exists. Returns an empty array when `fromHash === toHash` (no-op).
|
|
120
92
|
*
|
|
121
93
|
* Neighbor ordering is deterministic via the tie-break sort key:
|
|
122
|
-
*
|
|
94
|
+
* createdAt → to → migrationHash.
|
|
123
95
|
*/
|
|
124
96
|
export function findPath(
|
|
125
97
|
graph: MigrationGraph,
|
|
@@ -165,8 +137,8 @@ export function findPath(
|
|
|
165
137
|
* control chars at authoring time).
|
|
166
138
|
*
|
|
167
139
|
* Neighbour ordering when `required ≠ ∅`: edges covering ≥1 still-needed
|
|
168
|
-
* invariant come first, with `
|
|
169
|
-
*
|
|
140
|
+
* invariant come first, with `createdAt → to → migrationHash` as the
|
|
141
|
+
* secondary key. The heuristic steers BFS toward the satisfying path;
|
|
170
142
|
* correctness (shortest, deterministic) does not depend on it.
|
|
171
143
|
*/
|
|
172
144
|
export function findPathWithInvariants(
|
|
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import type { ContractSpaceHeadRef } from '@prisma-next/framework-components/control';
|
|
3
3
|
import { join } from 'pathe';
|
|
4
4
|
import { errorInvalidJson, errorInvalidRefFile } from './errors';
|
|
5
|
-
import { assertValidSpaceId } from './space-layout';
|
|
5
|
+
import { assertValidSpaceId, spaceMigrationDirectory, spaceRefsDirectory } from './space-layout';
|
|
6
6
|
|
|
7
7
|
export type { ContractSpaceHeadRef };
|
|
8
8
|
|
|
@@ -29,7 +29,10 @@ export async function readContractSpaceHeadRef(
|
|
|
29
29
|
): Promise<ContractSpaceHeadRef | null> {
|
|
30
30
|
assertValidSpaceId(spaceId);
|
|
31
31
|
|
|
32
|
-
const filePath = join(
|
|
32
|
+
const filePath = join(
|
|
33
|
+
spaceRefsDirectory(spaceMigrationDirectory(projectMigrationsDir, spaceId)),
|
|
34
|
+
'head.json',
|
|
35
|
+
);
|
|
33
36
|
|
|
34
37
|
let raw: string;
|
|
35
38
|
try {
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { access, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { canonicalizeJson } from '@prisma-next/framework-components/utils';
|
|
4
|
+
import { type } from 'arktype';
|
|
5
|
+
import { basename, dirname, join } from 'pathe';
|
|
6
|
+
import { errorInvalidRefFile, errorInvalidRefName, MigrationToolsError } from '../errors';
|
|
7
|
+
import { deleteRef, type RefEntry, validateRefName, writeRef } from '../refs';
|
|
8
|
+
|
|
9
|
+
export interface ContractIR {
|
|
10
|
+
readonly contract: unknown;
|
|
11
|
+
readonly contractDts: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ContractIrSchema = type({
|
|
15
|
+
targetFamily: 'string',
|
|
16
|
+
target: 'string',
|
|
17
|
+
profileHash: 'string',
|
|
18
|
+
storage: type({
|
|
19
|
+
storageHash: 'string',
|
|
20
|
+
}),
|
|
21
|
+
domain: type({
|
|
22
|
+
namespaces: 'object',
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function hasErrnoCode(error: unknown, code: string): boolean {
|
|
27
|
+
return error instanceof Error && (error as { code?: string }).code === code;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function snapshotJsonPath(refsDir: string, name: string): string {
|
|
31
|
+
return join(refsDir, `${name}.contract.json`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function snapshotDtsPath(refsDir: string, name: string): string {
|
|
35
|
+
return join(refsDir, `${name}.contract.d.ts`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function tmpPathFor(finalPath: string): string {
|
|
39
|
+
const dir = dirname(finalPath);
|
|
40
|
+
const fileName = basename(finalPath);
|
|
41
|
+
return join(dir, `.${fileName}.${Date.now()}-${randomBytes(4).toString('hex')}.tmp`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function atomicWriteFile(finalPath: string, content: string): Promise<void> {
|
|
45
|
+
const dir = dirname(finalPath);
|
|
46
|
+
await mkdir(dir, { recursive: true });
|
|
47
|
+
const tmpPath = tmpPathFor(finalPath);
|
|
48
|
+
await writeFile(tmpPath, content);
|
|
49
|
+
await rename(tmpPath, finalPath);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function unlinkIfExists(filePath: string): Promise<void> {
|
|
53
|
+
try {
|
|
54
|
+
await unlink(filePath);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (hasErrnoCode(error, 'ENOENT')) return;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseContractSnapshotJson(filePath: string, raw: string): unknown {
|
|
62
|
+
let parsed: unknown;
|
|
63
|
+
try {
|
|
64
|
+
parsed = JSON.parse(raw);
|
|
65
|
+
} catch {
|
|
66
|
+
throw errorInvalidRefFile(filePath, 'Failed to parse as JSON');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const result = ContractIrSchema(parsed);
|
|
70
|
+
if (result instanceof type.errors) {
|
|
71
|
+
throw errorInvalidRefFile(filePath, result.summary);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function writeRefSnapshot(
|
|
78
|
+
refsDir: string,
|
|
79
|
+
name: string,
|
|
80
|
+
snapshot: ContractIR,
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
if (!validateRefName(name)) {
|
|
83
|
+
throw errorInvalidRefName(name);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const jsonPath = snapshotJsonPath(refsDir, name);
|
|
87
|
+
const dtsPath = snapshotDtsPath(refsDir, name);
|
|
88
|
+
const jsonContent = `${canonicalizeJson(snapshot.contract)}\n`;
|
|
89
|
+
const dtsContent = snapshot.contractDts.endsWith('\n')
|
|
90
|
+
? snapshot.contractDts
|
|
91
|
+
: `${snapshot.contractDts}\n`;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
await atomicWriteFile(jsonPath, jsonContent);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
await unlinkIfExists(jsonPath);
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await atomicWriteFile(dtsPath, dtsContent);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
await unlinkIfExists(jsonPath);
|
|
104
|
+
await unlinkIfExists(dtsPath);
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function readRefSnapshot(refsDir: string, name: string): Promise<ContractIR | null> {
|
|
110
|
+
if (!validateRefName(name)) {
|
|
111
|
+
throw errorInvalidRefName(name);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const jsonPath = snapshotJsonPath(refsDir, name);
|
|
115
|
+
const dtsPath = snapshotDtsPath(refsDir, name);
|
|
116
|
+
|
|
117
|
+
let raw: string;
|
|
118
|
+
try {
|
|
119
|
+
raw = await readFile(jsonPath, 'utf-8');
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (hasErrnoCode(error, 'ENOENT')) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const contract = parseContractSnapshotJson(jsonPath, raw);
|
|
128
|
+
|
|
129
|
+
let contractDts: string;
|
|
130
|
+
try {
|
|
131
|
+
contractDts = await readFile(dtsPath, 'utf-8');
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (hasErrnoCode(error, 'ENOENT')) {
|
|
134
|
+
throw errorInvalidRefFile(dtsPath, 'Missing paired contract.d.ts snapshot file');
|
|
135
|
+
}
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { contract, contractDts };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function deleteRefSnapshot(refsDir: string, name: string): Promise<void> {
|
|
143
|
+
if (!validateRefName(name)) {
|
|
144
|
+
throw errorInvalidRefName(name);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await unlinkIfExists(snapshotJsonPath(refsDir, name));
|
|
148
|
+
await unlinkIfExists(snapshotDtsPath(refsDir, name));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function writeRefPaired(
|
|
152
|
+
refsDir: string,
|
|
153
|
+
name: string,
|
|
154
|
+
entry: RefEntry,
|
|
155
|
+
snapshot: ContractIR,
|
|
156
|
+
): Promise<void> {
|
|
157
|
+
await writeRefSnapshot(refsDir, name, snapshot);
|
|
158
|
+
try {
|
|
159
|
+
await writeRef(refsDir, name, entry);
|
|
160
|
+
} catch (writeError) {
|
|
161
|
+
try {
|
|
162
|
+
await deleteRefSnapshot(refsDir, name);
|
|
163
|
+
} catch {
|
|
164
|
+
// Rollback failure is secondary; preserve the original writeRef error.
|
|
165
|
+
}
|
|
166
|
+
throw writeError;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isUnknownRefError(error: unknown): boolean {
|
|
171
|
+
return MigrationToolsError.is(error) && error.code === 'MIGRATION.UNKNOWN_REF';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function snapshotFilesExist(refsDir: string, name: string): Promise<boolean> {
|
|
175
|
+
if (!validateRefName(name)) {
|
|
176
|
+
throw errorInvalidRefName(name);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const paths = [snapshotJsonPath(refsDir, name), snapshotDtsPath(refsDir, name)];
|
|
180
|
+
const checks = await Promise.allSettled(paths.map((filePath) => access(filePath)));
|
|
181
|
+
return checks.some((result) => result.status === 'fulfilled');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function deleteRefPaired(refsDir: string, name: string): Promise<void> {
|
|
185
|
+
if (await snapshotFilesExist(refsDir, name)) {
|
|
186
|
+
try {
|
|
187
|
+
await deleteRef(refsDir, name);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (!isUnknownRefError(error)) {
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
await deleteRefSnapshot(refsDir, name);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
await deleteRef(refsDir, name);
|
|
198
|
+
await deleteRefSnapshot(refsDir, name);
|
|
199
|
+
}
|
package/src/refs.ts
CHANGED
|
@@ -15,6 +15,29 @@ export interface RefEntry {
|
|
|
15
15
|
|
|
16
16
|
export type Refs = Readonly<Record<string, RefEntry>>;
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* The system head ref lives at `refs/head.json`. It is read (and its
|
|
20
|
+
* corruption judged) through `readContractSpaceHeadRef`, not as a
|
|
21
|
+
* user-authored ref, so {@link readRefsTolerant} excludes it.
|
|
22
|
+
*/
|
|
23
|
+
export const HEAD_REF_NAME = 'head';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A single ref file that exists on disk but cannot be turned into a
|
|
27
|
+
* {@link RefEntry} (unparseable JSON or schema-invalid content). The ref
|
|
28
|
+
* is omitted from the result; the problem is surfaced for the integrity
|
|
29
|
+
* layer to report as `refUnreadable` rather than aborting the load.
|
|
30
|
+
*/
|
|
31
|
+
export interface RefLoadProblem {
|
|
32
|
+
readonly refName: string;
|
|
33
|
+
readonly detail: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface TolerantRefsResult {
|
|
37
|
+
readonly refs: Refs;
|
|
38
|
+
readonly problems: readonly RefLoadProblem[];
|
|
39
|
+
}
|
|
40
|
+
|
|
18
41
|
const REF_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\/[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/;
|
|
19
42
|
const REF_VALUE_PATTERN = /^sha256:(empty|[0-9a-f]{64})$/;
|
|
20
43
|
|
|
@@ -94,7 +117,9 @@ export async function readRefs(refsDir: string): Promise<Refs> {
|
|
|
94
117
|
throw error;
|
|
95
118
|
}
|
|
96
119
|
|
|
97
|
-
const jsonFiles = entries.filter(
|
|
120
|
+
const jsonFiles = entries.filter(
|
|
121
|
+
(entry) => entry.endsWith('.json') && !entry.endsWith('.contract.json'),
|
|
122
|
+
);
|
|
98
123
|
const refs: Record<string, RefEntry> = {};
|
|
99
124
|
|
|
100
125
|
for (const jsonFile of jsonFiles) {
|
|
@@ -134,6 +159,77 @@ export async function readRefs(refsDir: string): Promise<Refs> {
|
|
|
134
159
|
return refs;
|
|
135
160
|
}
|
|
136
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Read a space's user-authored refs without ever throwing on disk
|
|
164
|
+
* content. A ref whose JSON is unparseable or whose shape fails
|
|
165
|
+
* {@link RefEntrySchema} is omitted from `refs` and reported as a
|
|
166
|
+
* {@link RefLoadProblem}; the remaining well-formed refs are still
|
|
167
|
+
* returned. A missing `refs/` directory yields no refs and no problems.
|
|
168
|
+
*
|
|
169
|
+
* `refs/head.json` is deliberately skipped here: the system head ref is
|
|
170
|
+
* read through `readContractSpaceHeadRef` (which validates head-ref
|
|
171
|
+
* shape, distinct from the strict user-ref hash grammar), so it is judged
|
|
172
|
+
* there and never doubles as a user ref. Genuine I/O faults (EACCES, EIO,
|
|
173
|
+
* …) still propagate — only parse / schema problems are made tolerant.
|
|
174
|
+
*/
|
|
175
|
+
export async function readRefsTolerant(refsDir: string): Promise<TolerantRefsResult> {
|
|
176
|
+
let entries: string[];
|
|
177
|
+
try {
|
|
178
|
+
entries = await readdir(refsDir, { recursive: true, encoding: 'utf-8' });
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
|
|
181
|
+
return { refs: {}, problems: [] };
|
|
182
|
+
}
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const jsonFiles = entries.filter(
|
|
187
|
+
(entry) =>
|
|
188
|
+
entry.endsWith('.json') &&
|
|
189
|
+
!entry.endsWith('.contract.json') &&
|
|
190
|
+
entry !== `${HEAD_REF_NAME}.json`,
|
|
191
|
+
);
|
|
192
|
+
const refs: Record<string, RefEntry> = {};
|
|
193
|
+
const problems: RefLoadProblem[] = [];
|
|
194
|
+
|
|
195
|
+
for (const jsonFile of jsonFiles) {
|
|
196
|
+
const filePath = join(refsDir, jsonFile);
|
|
197
|
+
const name = refNameFromPath(refsDir, filePath);
|
|
198
|
+
|
|
199
|
+
let raw: string;
|
|
200
|
+
try {
|
|
201
|
+
raw = await readFile(filePath, 'utf-8');
|
|
202
|
+
} catch (error) {
|
|
203
|
+
// Tolerate the TOCTOU race between `readdir` and `readFile` (ENOENT)
|
|
204
|
+
// and benign EISDIR if a directory happens to end in `.json`.
|
|
205
|
+
// Anything else (EACCES, EIO, …) is a real failure and propagates.
|
|
206
|
+
const code = error instanceof Error ? (error as { code?: string }).code : undefined;
|
|
207
|
+
if (code === 'ENOENT' || code === 'EISDIR') {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let parsed: unknown;
|
|
214
|
+
try {
|
|
215
|
+
parsed = JSON.parse(raw);
|
|
216
|
+
} catch (e) {
|
|
217
|
+
problems.push({ refName: name, detail: e instanceof Error ? e.message : String(e) });
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const result = RefEntrySchema(parsed);
|
|
222
|
+
if (result instanceof type.errors) {
|
|
223
|
+
problems.push({ refName: name, detail: result.summary });
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
refs[name] = result;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return { refs, problems };
|
|
231
|
+
}
|
|
232
|
+
|
|
137
233
|
export async function writeRef(refsDir: string, name: string, entry: RefEntry): Promise<void> {
|
|
138
234
|
if (!validateRefName(name)) {
|
|
139
235
|
throw errorInvalidRefName(name);
|
|
@@ -192,6 +288,33 @@ export async function deleteRef(refsDir: string, name: string): Promise<void> {
|
|
|
192
288
|
}
|
|
193
289
|
}
|
|
194
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Index user-authored refs by the contract hash each ref points at.
|
|
293
|
+
* Each bucket is sorted lex-asc for deterministic output.
|
|
294
|
+
*/
|
|
295
|
+
export function refsByContractHash(refs: Refs): ReadonlyMap<string, readonly string[]> {
|
|
296
|
+
const byHash = new Map<string, string[]>();
|
|
297
|
+
for (const [name, entry] of Object.entries(refs)) {
|
|
298
|
+
const bucket = byHash.get(entry.hash);
|
|
299
|
+
if (bucket) bucket.push(name);
|
|
300
|
+
else byHash.set(entry.hash, [name]);
|
|
301
|
+
}
|
|
302
|
+
for (const bucket of byHash.values()) {
|
|
303
|
+
bucket.sort();
|
|
304
|
+
}
|
|
305
|
+
return byHash;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Read `migrations/<space>/refs/*.json` and index by destination hash.
|
|
310
|
+
* Returns an empty map when the refs directory does not exist.
|
|
311
|
+
*/
|
|
312
|
+
export async function resolveRefsByContractHash(
|
|
313
|
+
refsDir: string,
|
|
314
|
+
): Promise<ReadonlyMap<string, readonly string[]>> {
|
|
315
|
+
return refsByContractHash(await readRefs(refsDir));
|
|
316
|
+
}
|
|
317
|
+
|
|
195
318
|
export function resolveRef(refs: Refs, name: string): RefEntry {
|
|
196
319
|
if (!validateRefName(name)) {
|
|
197
320
|
throw errorInvalidRefName(name);
|
package/src/space-layout.ts
CHANGED
|
@@ -46,3 +46,33 @@ export function spaceMigrationDirectory(projectMigrationsDir: string, spaceId: s
|
|
|
46
46
|
assertValidSpaceId(spaceId);
|
|
47
47
|
return join(projectMigrationsDir, spaceId);
|
|
48
48
|
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Per-space subdirectory name reserved for the ref store
|
|
52
|
+
* (`migrations/<space>/<SPACE_REFS_DIRNAME>/*.json`). Single source of
|
|
53
|
+
* truth: every helper that composes a per-space refs path imports this
|
|
54
|
+
* constant, and the enumerator uses it (via
|
|
55
|
+
* {@link RESERVED_SPACE_SUBDIR_NAMES}) to exclude reserved names from
|
|
56
|
+
* the contract-space candidate list.
|
|
57
|
+
*/
|
|
58
|
+
export const SPACE_REFS_DIRNAME = 'refs';
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Names reserved as per-space subdirectories of `migrations/<space>/`.
|
|
62
|
+
* Used by the enumerator to filter contract-space candidates so a
|
|
63
|
+
* reserved name (e.g. a top-level `migrations/refs/` left in the wrong
|
|
64
|
+
* place) is never enumerated as a phantom contract space. Currently
|
|
65
|
+
* holds `SPACE_REFS_DIRNAME`; extend if future per-space layouts add
|
|
66
|
+
* more reserved subdirectories.
|
|
67
|
+
*/
|
|
68
|
+
export const RESERVED_SPACE_SUBDIR_NAMES: ReadonlySet<string> = new Set([SPACE_REFS_DIRNAME]);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the per-space refs directory for `spaceMigrationsDir`
|
|
72
|
+
* (typically the value returned by {@link spaceMigrationDirectory}).
|
|
73
|
+
* Composes the canonical {@link SPACE_REFS_DIRNAME} so callers do not
|
|
74
|
+
* hard-code the literal.
|
|
75
|
+
*/
|
|
76
|
+
export function spaceRefsDirectory(spaceMigrationsDir: string): string {
|
|
77
|
+
return join(spaceMigrationsDir, SPACE_REFS_DIRNAME);
|
|
78
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"errors-DGYwcwXs.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["import { ifDefined } from '@prisma-next/utils/defined';\nimport { basename, dirname, relative } from 'pathe';\n\n/**\n * Build the canonical \"re-emit this package\" remediation hint.\n *\n * Every on-disk migration package ships its own `migration.ts` author-time\n * file. Running it regenerates `migration.json` and `ops.json` with the\n * correct hash + metadata, so it is the right primitive whenever a single\n * package's on-disk artifacts are missing, malformed, or otherwise corrupt.\n * Pointing users at `migration plan` would emit a *new* package rather than\n * heal the broken one.\n */\nfunction reemitHint(dir: string, fallback?: string): string {\n const relativeDir = relative(process.cwd(), dir);\n const reemit = `Re-emit the package by running \\`node \"${relativeDir}/migration.ts\"\\``;\n return fallback ? `${reemit}, ${fallback}` : `${reemit}.`;\n}\n\n/**\n * Structured error for migration tooling operations.\n *\n * Follows the NAMESPACE.SUBCODE convention from ADR 027. All codes live under\n * the MIGRATION namespace. These are tooling-time errors (file I/O, hash\n * verification, migration history reconstruction), distinct from the runtime\n * MIGRATION.* codes for apply-time failures (PRECHECK_FAILED, POSTCHECK_FAILED,\n * etc.).\n *\n * Fields:\n * - code: Stable machine-readable code (MIGRATION.SUBCODE)\n * - category: Always 'MIGRATION'\n * - why: Explains the cause in plain language\n * - fix: Actionable remediation step\n * - details: Machine-readable structured data for agents\n */\nexport class MigrationToolsError extends Error {\n readonly code: string;\n readonly category = 'MIGRATION' as const;\n readonly why: string;\n readonly fix: string;\n readonly details: Record<string, unknown> | undefined;\n\n constructor(\n code: string,\n summary: string,\n options: {\n readonly why: string;\n readonly fix: string;\n readonly details?: Record<string, unknown>;\n },\n ) {\n super(summary);\n this.name = 'MigrationToolsError';\n this.code = code;\n this.why = options.why;\n this.fix = options.fix;\n this.details = options.details;\n }\n\n static is(error: unknown): error is MigrationToolsError {\n if (!(error instanceof Error)) return false;\n const candidate = error as MigrationToolsError;\n return candidate.name === 'MigrationToolsError' && typeof candidate.code === 'string';\n }\n}\n\nexport function errorDirectoryExists(dir: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.DIR_EXISTS', 'Migration directory already exists', {\n why: `The directory \"${dir}\" already exists. Each migration must have a unique directory.`,\n fix: 'Use --name to pick a different name, or delete the existing directory and re-run.',\n details: { dir },\n });\n}\n\nexport function errorMissingFile(file: string, dir: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.FILE_MISSING', `Missing ${file}`, {\n why: `Expected \"${file}\" in \"${dir}\" but the file does not exist.`,\n fix: reemitHint(\n dir,\n 'or delete the directory if the migration is unwanted and the source TypeScript is gone.',\n ),\n details: { file, dir },\n });\n}\n\nexport function errorInvalidJson(filePath: string, parseError: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_JSON', 'Invalid JSON in migration file', {\n why: `Failed to parse \"${filePath}\": ${parseError}`,\n fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),\n details: { filePath, parseError },\n });\n}\n\nexport function errorInvalidManifest(filePath: string, reason: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_MANIFEST', 'Invalid migration manifest', {\n why: `Migration manifest at \"${filePath}\" is invalid: ${reason}`,\n fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),\n details: { filePath, reason },\n });\n}\n\nexport function errorInvalidOperationEntry(index: number, reason: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.INVALID_OPERATION_ENTRY',\n 'Migration operation entry is malformed',\n {\n why: `Operation at index ${index} returned by the migration class failed schema validation: ${reason}.`,\n fix: \"Update the migration class so each entry of `operations` carries `id` (string), `label` (string), and `operationClass` (one of 'additive' | 'widening' | 'destructive' | 'data').\",\n details: { index, reason },\n },\n );\n}\n\nexport function errorInvalidSlug(slug: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_NAME', 'Invalid migration name', {\n why: `The slug \"${slug}\" contains no valid characters after sanitization (only a-z, 0-9 are kept).`,\n fix: 'Provide a name with at least one alphanumeric character, e.g. --name add_users.',\n details: { slug },\n });\n}\n\nexport function errorInvalidDestName(destName: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_DEST_NAME', 'Invalid copy destination name', {\n why: `The destination name \"${destName}\" must be a single path segment (no \"..\" or directory separators).`,\n fix: 'Use a simple file name such as \"contract.json\" for each destination in the copy list.',\n details: { destName },\n });\n}\n\nexport function errorInvalidSpaceId(spaceId: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.INVALID_SPACE_ID',\n 'Invalid contract space identifier',\n {\n 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.`,\n fix: 'Pick a lowercase identifier that begins with a letter and contains only lowercase letters, digits, hyphens, or underscores; max 64 characters total.',\n details: { spaceId },\n },\n );\n}\n\nexport function errorDescriptorHeadHashMismatch(args: {\n readonly extensionId: string;\n readonly recomputedHash: string;\n readonly headRefHash: string;\n}): MigrationToolsError {\n const { extensionId, recomputedHash, headRefHash } = args;\n return new MigrationToolsError(\n 'MIGRATION.DESCRIPTOR_HEAD_HASH_MISMATCH',\n \"Extension descriptor's headRef.hash does not match its contractJson\",\n {\n 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.`,\n 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).',\n details: { extensionId, recomputedHash, headRefHash },\n },\n );\n}\n\nexport function errorDuplicateSpaceId(spaceId: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.DUPLICATE_SPACE_ID',\n 'Duplicate contract space identifier',\n {\n 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).`,\n fix: 'Deduplicate the inputs before passing them to `planAllSpaces` — typically by checking your `extensionPacks` declaration for repeated entries.',\n details: { spaceId },\n },\n );\n}\n\nexport function errorSameSourceAndTarget(dir: string, hash: string): MigrationToolsError {\n const dirName = basename(dir);\n return new MigrationToolsError(\n 'MIGRATION.SAME_SOURCE_AND_TARGET',\n 'Migration without data-transform operations has same source and target',\n {\n 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.`,\n fix: reemitHint(\n dir,\n 'and either change the contract so from ≠ to, add a dataTransform op, or delete the directory if the migration is unwanted.',\n ),\n details: { dirName, hash },\n },\n );\n}\n\nexport function errorAmbiguousTarget(\n branchTips: readonly string[],\n context?: {\n divergencePoint: string;\n branches: readonly {\n tip: string;\n edges: readonly { dirName: string; from: string; to: string }[];\n }[];\n },\n): MigrationToolsError {\n const divergenceInfo = context\n ? `\\nDivergence point: ${context.divergencePoint}\\nBranches:\\n${context.branches.map((b) => ` → ${b.tip} (${b.edges.length} edge(s): ${b.edges.map((e) => e.dirName).join(' → ') || 'direct'})`).join('\\n')}`\n : '';\n return new MigrationToolsError('MIGRATION.AMBIGUOUS_TARGET', 'Ambiguous migration target', {\n why: `The migration history has diverged into multiple branches: ${branchTips.join(', ')}. This typically happens when two developers plan migrations from the same starting point.${divergenceInfo}`,\n fix: 'Use `ref set <name> <hash>` to target a specific branch, delete one of the conflicting migration directories and re-run `migration plan`, or use --from <hash> to explicitly select a starting point.',\n details: {\n branchTips,\n ...(context ? { divergencePoint: context.divergencePoint, branches: context.branches } : {}),\n },\n });\n}\n\nexport function errorNoInitialMigration(nodes: readonly string[]): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.NO_INITIAL_MIGRATION', 'No initial migration found', {\n why: `No migration starts from the empty contract state (known hashes: ${nodes.join(', ')}). At least one migration must originate from the empty state.`,\n fix: 'Inspect the migrations directory for corrupted migration.json files. At least one migration must start from the empty contract hash.',\n details: { nodes },\n });\n}\n\nexport function errorInvalidRefs(refsPath: string, reason: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_REFS', 'Invalid refs.json', {\n why: `refs.json at \"${refsPath}\" is invalid: ${reason}`,\n fix: 'Ensure refs.json is a flat object mapping valid ref names to contract hash strings.',\n details: { path: refsPath, reason },\n });\n}\n\nexport function errorInvalidRefFile(filePath: string, reason: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_REF_FILE', 'Invalid ref file', {\n why: `Ref file at \"${filePath}\" is invalid: ${reason}`,\n fix: 'Ensure the ref file contains valid JSON with { \"hash\": \"sha256:<64 hex chars>\", \"invariants\": [\"...\"] }.',\n details: { path: filePath, reason },\n });\n}\n\nexport function errorInvalidRefName(refName: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_REF_NAME', 'Invalid ref name', {\n why: `Ref name \"${refName}\" is invalid. Names must be lowercase alphanumeric with hyphens or forward slashes (no \".\" or \"..\" segments).`,\n fix: `Use a valid ref name (e.g., \"staging\", \"envs/production\").`,\n details: { refName },\n });\n}\n\nexport function errorNoTarget(reachableHashes: readonly string[]): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.NO_TARGET', 'No migration target could be resolved', {\n why: `The migration history contains cycles and no target can be resolved automatically (reachable hashes: ${reachableHashes.join(', ')}). This typically happens after rollback migrations (e.g., C1→C2→C1).`,\n fix: 'Use --from <hash> to specify the planning origin explicitly.',\n details: { reachableHashes },\n });\n}\n\nexport function errorInvalidRefValue(value: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_REF_VALUE', 'Invalid ref value', {\n why: `Ref value \"${value}\" is not a valid contract hash. Values must be in the format \"sha256:<64 hex chars>\" or \"sha256:empty\".`,\n fix: 'Use a valid storage hash from `prisma-next contract emit` output or an existing migration.',\n details: { value },\n });\n}\n\nexport function errorDuplicateMigrationHash(migrationHash: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.DUPLICATE_MIGRATION_HASH',\n 'Duplicate migrationHash in migration graph',\n {\n why: `Multiple migrations share migrationHash \"${migrationHash}\". Each migration must have a unique content-addressed identity.`,\n fix: 'Regenerate one of the conflicting migrations so each migrationHash is unique, then re-run migration commands.',\n details: { migrationHash },\n },\n );\n}\n\nexport function errorInvalidInvariantId(invariantId: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_INVARIANT_ID', 'Invalid invariantId', {\n 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.`,\n fix: 'Pick an invariantId without spaces, tabs, newlines, or control characters — e.g. \"backfill-user-phone\", \"users/backfill-phone\", or \"BackfillUserPhone\".',\n details: { invariantId },\n });\n}\n\nexport function errorDuplicateInvariantInEdge(invariantId: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.DUPLICATE_INVARIANT_IN_EDGE',\n 'Duplicate invariantId on a single migration',\n {\n 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.`,\n fix: 'Rename one of the conflicting dataTransform invariantIds, or drop invariantId on the op that does not need to be routing-visible.',\n details: { invariantId },\n },\n );\n}\n\nexport function errorProvidedInvariantsMismatch(\n filePath: string,\n stored: readonly string[],\n derived: readonly string[],\n): MigrationToolsError {\n const storedSet = new Set(stored);\n const derivedSet = new Set(derived);\n const missing = [...derivedSet].filter((id) => !storedSet.has(id));\n const extra = [...storedSet].filter((id) => !derivedSet.has(id));\n // When sets agree but arrays don't, the only difference is ordering — call\n // it out so the reader doesn't stare at two visually-identical arrays.\n // Canonical providedInvariants is sorted ascending; a manifest with the\n // same ids in a different order is still a mismatch (the hash check would\n // also fail), but the human-readable diagnostic is otherwise unhelpful.\n const orderingOnly = missing.length === 0 && extra.length === 0;\n const why = orderingOnly\n ? `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.`\n : `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.`;\n return new MigrationToolsError(\n 'MIGRATION.PROVIDED_INVARIANTS_MISMATCH',\n 'providedInvariants on migration.json disagrees with ops.json',\n {\n why,\n fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),\n details: { filePath, stored, derived, difference: { missing, extra } },\n },\n );\n}\n\n/**\n * Wire-shape edge surfaced through the JSON envelope's\n * `meta.structuralPath` of `MIGRATION.NO_INVARIANT_PATH`. Slim by design —\n * authoring metadata (`createdAt`, `labels`) lives on `MigrationEdge` but\n * is intentionally dropped here so the envelope stays stable across\n * graph-internal refactors.\n *\n * Stability: any field added here is part of the public CLI JSON contract.\n * Callers (CLI consumers, agents) must be able to treat\n * `(dirName, migrationHash, from, to, invariants)` as the canonical shape.\n */\nexport interface NoInvariantPathStructuralEdge {\n readonly dirName: string;\n readonly migrationHash: string;\n readonly from: string;\n readonly to: string;\n readonly invariants: readonly string[];\n}\n\nexport function errorNoInvariantPath(args: {\n readonly refName?: string;\n readonly required: readonly string[];\n readonly missing: readonly string[];\n readonly structuralPath: readonly NoInvariantPathStructuralEdge[];\n}): MigrationToolsError {\n const { refName, required, missing, structuralPath } = args;\n const refClause = refName ? `Ref \"${refName}\"` : 'Target';\n const missingList = missing.map((id) => JSON.stringify(id)).join(', ');\n const requiredList = required.map((id) => JSON.stringify(id)).join(', ');\n return new MigrationToolsError(\n 'MIGRATION.NO_INVARIANT_PATH',\n 'No path covers the required invariants',\n {\n why: `${refClause} requires invariants the reachable path doesn't cover. required=[${requiredList}], missing=[${missingList}].`,\n 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.',\n details: {\n required,\n missing,\n structuralPath,\n ...ifDefined('refName', refName),\n },\n },\n );\n}\n\nexport function errorUnknownInvariant(args: {\n readonly refName?: string;\n readonly unknown: readonly string[];\n readonly declared: readonly string[];\n}): MigrationToolsError {\n const { refName, unknown, declared } = args;\n const refClause = refName ? `Ref \"${refName}\" declares` : 'Declares';\n const unknownList = unknown.map((id) => JSON.stringify(id)).join(', ');\n return new MigrationToolsError(\n 'MIGRATION.UNKNOWN_INVARIANT',\n 'Ref declares invariants no migration in the graph provides',\n {\n why: `${refClause} invariants no migration in the graph provides. unknown=[${unknownList}].`,\n 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.',\n details: {\n unknown,\n declared,\n ...ifDefined('refName', refName),\n },\n },\n );\n}\n\nexport function errorMigrationHashMismatch(\n dir: string,\n storedHash: string,\n computedHash: string,\n): MigrationToolsError {\n // Render a cwd-relative path in the human-readable diagnostic so users\n // running CLI commands from the project root see a familiar short path.\n // Keep the absolute path in `details.dir` for machine consumers.\n const relativeDir = relative(process.cwd(), dir);\n return new MigrationToolsError('MIGRATION.HASH_MISMATCH', 'Migration package is corrupt', {\n 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.`,\n fix: reemitHint(dir, 'or restore the directory from version control.'),\n details: { dir, storedHash, computedHash },\n });\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"io.d.mts","names":[],"sources":["../../src/io.ts"],"mappings":";;;;iBA+CsB,qBAAA,CACpB,GAAA,UACA,QAAA,EAAU,iBAAA,EACV,GAAA,EAAK,YAAA,GACJ,OAAA;AAJH;;;;;;;;;;;;;;;;AAoDA;;;;;;;;;;AA8BA;;;;AAlFA,iBAoDsB,2BAAA,CACpB,SAAA,UACA,GAAA,EAAK,gBAAA,GACJ,OAAA;;;;;;;AA2DH;;;;;;;;;;;AAaA;;;;iBA7CsB,6CAAA,CACpB,SAAA,UACA,GAAA,EAAK,gBAAA,GACJ,OAAA;EAAA,SAAmB,OAAA;AAAA;;;;AAiDtB;;;;;;;;iBApBsB,mBAAA,CACpB,OAAA,UACA,KAAA;EAAA,SAA2B,UAAA;EAAA,SAA6B,QAAA;AAAA,MACvD,OAAA;AAAA,iBAUmB,sBAAA,CACpB,GAAA,UACA,QAAA,EAAU,iBAAA,GACT,OAAA;AAAA,iBAImB,iBAAA,CAAkB,GAAA,UAAa,GAAA,EAAK,YAAA,GAAe,OAAA;AAAA,iBAInD,oBAAA,CAAqB,GAAA,WAAc,OAAA,CAAQ,sBAAA;AAAA,iBAiG3C,iBAAA,CACpB,cAAA,WACC,OAAA,UAAiB,sBAAA;AAAA,iBA+BJ,sBAAA,CAAuB,SAAA,EAAW,IAAA,EAAM,IAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"graph-BrLXqoUc.d.mts","names":[],"sources":["../src/graph.ts"],"mappings":";;AAIA;;;UAAiB,aAAA;EAAA,SACN,IAAA;EAAA,SACA,EAAA;EAAA,SACA,aAAA;EAAA,SACA,OAAA;EAAA,SACA,SAAA;EAAA,SACA,MAAA;EAMA;;;AAGX;;EAHW,SAAA,UAAA;AAAA;AAAA,UAGM,cAAA;EAAA,SACN,KAAA,EAAO,WAAA;EAAA,SACP,YAAA,EAAc,WAAA,kBAA6B,aAAA;EAAA,SAC3C,YAAA,EAAc,WAAA,kBAA6B,aAAA;EAAA,SAC3C,eAAA,EAAiB,WAAA,SAAoB,aAAA;AAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hash-Cr4WIr4Z.mjs","names":[],"sources":["../src/hash.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { canonicalizeJson } from '@prisma-next/framework-components/utils';\nimport type { MigrationMetadata } from './metadata';\nimport type { MigrationOps, OnDiskMigrationPackage } from './package';\n\nexport interface VerifyResult {\n readonly ok: boolean;\n readonly reason?: 'mismatch';\n readonly storedHash: string;\n readonly computedHash: string;\n}\n\nfunction sha256Hex(input: string): string {\n return createHash('sha256').update(input).digest('hex');\n}\n\n/**\n * Content-addressed migration hash over (metadata envelope sans hints,\n * ops). See ADR 199 — Storage-only migration identity for the\n * rationale: the storage-hash bookends (`from`, `to`) inside the\n * envelope anchor the contract identity by hash, and planner hints are\n * advisory and must not affect identity. The full contract IRs are not\n * part of the manifest — they live in sibling `*-contract.json` files\n * authored alongside the migration, never inlined here.\n *\n * The integrity check is purely structural, not semantic. The function\n * canonicalizes its inputs via `sortKeys` (recursive) + `JSON.stringify`\n * and hashes the result. Target-specific operation payloads (`step.sql`,\n * Mongo's pipeline AST, …) are hashed verbatim — no per-target\n * normalization is required, because what's being verified is \"do the\n * on-disk bytes still produce their recorded hash\", not \"do two\n * semantically-equivalent migrations hash the same\". The latter is an\n * emit-drift concern (ADR 192 step 2).\n *\n * The symmetry across write and read holds because `JSON.parse(\n * JSON.stringify(x))` round-trips JSON-safe values losslessly and\n * `sortKeys` is idempotent and deterministic — write-time and read-time\n * canonicalization produce the same canonical bytes regardless of\n * source-side key ordering or whitespace.\n *\n * The `migrationHash` field on the metadata is stripped before hashing\n * so the function can be used both at write time (when no hash exists\n * yet) and at verify time (rehashing an already-attested record).\n */\nexport function computeMigrationHash(\n metadata: Omit<MigrationMetadata, 'migrationHash'> & { readonly migrationHash?: string },\n ops: MigrationOps,\n): string {\n const { migrationHash: _migrationHash, hints: _hints, ...strippedMeta } = metadata;\n\n const canonicalMetadata = canonicalizeJson(strippedMeta);\n const canonicalOps = canonicalizeJson(ops);\n\n const partHashes = [canonicalMetadata, canonicalOps].map(sha256Hex);\n const hash = sha256Hex(canonicalizeJson(partHashes));\n\n return `sha256:${hash}`;\n}\n\n/**\n * Re-hash an in-memory migration package and compare against the stored\n * `migrationHash`. See `computeMigrationHash` for the canonicalization rules.\n *\n * Returns `{ ok: true }` when the package is internally consistent, or\n * `{ ok: false, reason: 'mismatch', storedHash, computedHash }` when it is\n * not — typically a sign of FS corruption, partial writes, or a post-emit\n * hand edit.\n */\nexport function verifyMigrationHash(pkg: OnDiskMigrationPackage): VerifyResult {\n const computed = computeMigrationHash(pkg.metadata, pkg.ops);\n\n if (pkg.metadata.migrationHash === computed) {\n return {\n ok: true,\n storedHash: pkg.metadata.migrationHash,\n computedHash: computed,\n };\n }\n\n return {\n ok: false,\n reason: 'mismatch',\n storedHash: pkg.metadata.migrationHash,\n computedHash: computed,\n };\n}\n"],"mappings":";;;AAYA,SAAS,UAAU,OAAuB;CACxC,OAAO,WAAW,SAAS,CAAC,OAAO,MAAM,CAAC,OAAO,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BzD,SAAgB,qBACd,UACA,KACQ;CACR,MAAM,EAAE,eAAe,gBAAgB,OAAO,QAAQ,GAAG,iBAAiB;CAQ1E,OAAO,UAFM,UAAU,iBADJ,CAHO,iBAAiB,aAGN,EAFhB,iBAAiB,IAEa,CAAC,CAAC,IAAI,UACP,CAAC,CAE9B;;;;;;;;;;;AAYvB,SAAgB,oBAAoB,KAA2C;CAC7E,MAAM,WAAW,qBAAqB,IAAI,UAAU,IAAI,IAAI;CAE5D,IAAI,IAAI,SAAS,kBAAkB,UACjC,OAAO;EACL,IAAI;EACJ,YAAY,IAAI,SAAS;EACzB,cAAc;EACf;CAGH,OAAO;EACL,IAAI;EACJ,QAAQ;EACR,YAAY,IAAI,SAAS;EACzB,cAAc;EACf"}
|
package/dist/io-BPLfzvZe.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"io-BPLfzvZe.mjs","names":[],"sources":["../src/io.ts"],"sourcesContent":["import { copyFile, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';\nimport type {\n MigrationMetadata,\n MigrationPackage,\n} from '@prisma-next/framework-components/control';\nimport { type } from 'arktype';\nimport { basename, dirname, join, resolve } from 'pathe';\nimport {\n errorDirectoryExists,\n errorInvalidDestName,\n errorInvalidJson,\n errorInvalidManifest,\n errorInvalidSlug,\n errorMigrationHashMismatch,\n errorMissingFile,\n errorProvidedInvariantsMismatch,\n} from './errors';\nimport { verifyMigrationHash } from './hash';\nimport { deriveProvidedInvariants } from './invariants';\nimport { MigrationOpsSchema } from './op-schema';\nimport type { MigrationOps, OnDiskMigrationPackage } from './package';\n\nexport const MANIFEST_FILE = 'migration.json';\nconst OPS_FILE = 'ops.json';\nconst MAX_SLUG_LENGTH = 64;\n\nfunction hasErrnoCode(error: unknown, code: string): boolean {\n return error instanceof Error && (error as { code?: string }).code === code;\n}\n\nconst MigrationHintsSchema = type({\n used: 'string[]',\n applied: 'string[]',\n plannerVersion: 'string',\n});\n\nconst MigrationMetadataSchema = type({\n '+': 'reject',\n from: 'string > 0 | null',\n to: 'string',\n migrationHash: 'string',\n hints: MigrationHintsSchema,\n labels: 'string[]',\n providedInvariants: 'string[]',\n createdAt: 'string',\n});\n\nexport async function writeMigrationPackage(\n dir: string,\n metadata: MigrationMetadata,\n ops: MigrationOps,\n): Promise<void> {\n await mkdir(dirname(dir), { recursive: true });\n\n try {\n await mkdir(dir);\n } catch (error) {\n if (hasErrnoCode(error, 'EEXIST')) {\n throw errorDirectoryExists(dir);\n }\n throw error;\n }\n\n await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(metadata, null, 2), {\n flag: 'wx',\n });\n await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: 'wx' });\n}\n\n/**\n * Materialise an in-memory {@link MigrationPackage} to a per-space\n * directory on disk.\n *\n * Writes two files under `<targetDir>/<pkg.dirName>/`:\n *\n * - `migration.json` — the manifest (pretty-printed, matches\n * {@link writeMigrationPackage}'s output for byte-for-byte parity with\n * app-space migrations).\n * - `ops.json` — the operation list (pretty-printed).\n *\n * Distinct verb from the lower-level {@link writeMigrationPackage}\n * (which takes constituent `(metadata, ops)`): callers reading\n * `materialise…` know they are persisting a struct-typed package.\n *\n * Overwrite-idempotent: the per-package directory is cleared before\n * each emit, so re-running against the same `targetDir` produces\n * byte-identical contents and never leaves stale files behind. The\n * lower-level {@link writeMigrationPackage} stays strict because the\n * CLI authoring path (`migration plan` / `migration new`) deliberately\n * refuses to clobber an existing authored migration; this helper is\n * the re-emit path that is supposed to converge on a single canonical\n * on-disk shape.\n *\n * The per-space head contract lives at\n * `<projectMigrationsDir>/<spaceId>/contract.json` (written by\n * {@link import('./emit-contract-space-artefacts').emitContractSpaceArtefacts}),\n * not inside the per-package directory. The runner reads only\n * `migration.json` + `ops.json` from each package.\n */\nexport async function materialiseMigrationPackage(\n targetDir: string,\n pkg: MigrationPackage,\n): Promise<void> {\n const dir = join(targetDir, pkg.dirName);\n await rm(dir, { recursive: true, force: true });\n await writeMigrationPackage(dir, pkg.metadata, pkg.ops);\n}\n\n/**\n * Idempotent variant of {@link materialiseMigrationPackage}: writes the\n * package only if `<targetDir>/<pkg.dirName>/` does not already exist on\n * disk as a directory; returns `{ written: false }` when the package\n * directory is present (no rewrite, no comparison — by-existence skip).\n *\n * Concretely:\n * - existing directory → skip silently, return `{ written: false }`.\n * - missing path → write three files via {@link materialiseMigrationPackage},\n * return `{ written: true }`.\n * - path exists but is not a directory (file/symlink) → treated as\n * missing; {@link materialiseMigrationPackage} will attempt creation\n * and fail with an appropriate OS error.\n * - any other I/O error from `stat` → propagated unchanged.\n *\n * Used by the CLI's `runContractSpaceExtensionMigrationsPass` to\n * materialise extension migration packages into a project's\n * `migrations/<spaceId>/` directory, and by extension-package tests\n * that mirror the same idempotent-rematerialise property locally\n * without taking a CLI dependency.\n */\nexport async function materialiseExtensionMigrationPackageIfMissing(\n targetDir: string,\n pkg: MigrationPackage,\n): Promise<{ readonly written: boolean }> {\n const pkgDir = join(targetDir, pkg.dirName);\n if (await directoryExists(pkgDir)) {\n return { written: false };\n }\n await materialiseMigrationPackage(targetDir, pkg);\n return { written: true };\n}\n\nasync function directoryExists(p: string): Promise<boolean> {\n try {\n return (await stat(p)).isDirectory();\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) return false;\n throw error;\n }\n}\n\n/**\n * Copy a list of files into `destDir`, optionally renaming each one.\n *\n * The destination directory is created (with `recursive: true`) if it\n * does not already exist. Each source path is copied byte-for-byte into\n * `destDir/<destName>`; missing sources throw `ENOENT`. The helper is\n * intentionally generic: callers own the list of files (e.g. a contract\n * emitter's emitted output) and the naming convention (e.g. renaming\n * the destination contract to `end-contract.*` and the source contract\n * to `start-contract.*`).\n */\nexport async function copyFilesWithRename(\n destDir: string,\n files: readonly { readonly sourcePath: string; readonly destName: string }[],\n): Promise<void> {\n await mkdir(destDir, { recursive: true });\n for (const file of files) {\n if (basename(file.destName) !== file.destName) {\n throw errorInvalidDestName(file.destName);\n }\n await copyFile(file.sourcePath, join(destDir, file.destName));\n }\n}\n\nexport async function writeMigrationMetadata(\n dir: string,\n metadata: MigrationMetadata,\n): Promise<void> {\n await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(metadata, null, 2)}\\n`);\n}\n\nexport async function writeMigrationOps(dir: string, ops: MigrationOps): Promise<void> {\n await writeFile(join(dir, OPS_FILE), `${JSON.stringify(ops, null, 2)}\\n`);\n}\n\nexport async function readMigrationPackage(dir: string): Promise<OnDiskMigrationPackage> {\n const absoluteDir = resolve(dir);\n const manifestPath = join(absoluteDir, MANIFEST_FILE);\n const opsPath = join(absoluteDir, OPS_FILE);\n\n let manifestRaw: string;\n try {\n manifestRaw = await readFile(manifestPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorMissingFile(MANIFEST_FILE, absoluteDir);\n }\n throw error;\n }\n\n let opsRaw: string;\n try {\n opsRaw = await readFile(opsPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorMissingFile(OPS_FILE, absoluteDir);\n }\n throw error;\n }\n\n let metadata: MigrationMetadata;\n try {\n metadata = JSON.parse(manifestRaw);\n } catch (e) {\n throw errorInvalidJson(manifestPath, e instanceof Error ? e.message : String(e));\n }\n\n let ops: MigrationOps;\n try {\n ops = JSON.parse(opsRaw);\n } catch (e) {\n throw errorInvalidJson(opsPath, e instanceof Error ? e.message : String(e));\n }\n\n validateMetadata(metadata, manifestPath);\n validateOps(ops, opsPath);\n\n // Re-derive before the hash check so format/duplicate diagnostics\n // fire with their dedicated codes rather than as a generic hash mismatch.\n const derivedInvariants = deriveProvidedInvariants(ops);\n if (!arraysEqual(metadata.providedInvariants, derivedInvariants)) {\n throw errorProvidedInvariantsMismatch(\n manifestPath,\n metadata.providedInvariants,\n derivedInvariants,\n );\n }\n\n const pkg: OnDiskMigrationPackage = {\n dirName: basename(absoluteDir),\n dirPath: absoluteDir,\n metadata,\n ops,\n };\n\n const verification = verifyMigrationHash(pkg);\n if (!verification.ok) {\n throw errorMigrationHashMismatch(\n absoluteDir,\n verification.storedHash,\n verification.computedHash,\n );\n }\n\n return pkg;\n}\n\nfunction arraysEqual(a: readonly string[], b: readonly string[]): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\nfunction validateMetadata(\n metadata: unknown,\n filePath: string,\n): asserts metadata is MigrationMetadata {\n const result = MigrationMetadataSchema(metadata);\n if (result instanceof type.errors) {\n throw errorInvalidManifest(filePath, result.summary);\n }\n}\n\nfunction validateOps(ops: unknown, filePath: string): asserts ops is MigrationOps {\n const result = MigrationOpsSchema(ops);\n if (result instanceof type.errors) {\n throw errorInvalidManifest(filePath, result.summary);\n }\n}\n\nexport async function readMigrationsDir(\n migrationsRoot: string,\n): Promise<readonly OnDiskMigrationPackage[]> {\n let entries: string[];\n try {\n entries = await readdir(migrationsRoot);\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n return [];\n }\n throw error;\n }\n\n const packages: OnDiskMigrationPackage[] = [];\n\n for (const entry of entries.sort()) {\n const entryPath = join(migrationsRoot, entry);\n const entryStat = await stat(entryPath);\n if (!entryStat.isDirectory()) continue;\n\n const manifestPath = join(entryPath, MANIFEST_FILE);\n try {\n await stat(manifestPath);\n } catch {\n continue; // skip non-migration directories\n }\n\n packages.push(await readMigrationPackage(entryPath));\n }\n\n return packages;\n}\n\nexport function formatMigrationDirName(timestamp: Date, slug: string): string {\n const sanitized = slug\n .toLowerCase()\n .replace(/[^a-z0-9]/g, '_')\n .replace(/_+/g, '_')\n .replace(/^_|_$/g, '');\n\n if (sanitized.length === 0) {\n throw errorInvalidSlug(slug);\n }\n\n const truncated = sanitized.slice(0, MAX_SLUG_LENGTH);\n\n const y = timestamp.getUTCFullYear();\n const mo = String(timestamp.getUTCMonth() + 1).padStart(2, '0');\n const d = String(timestamp.getUTCDate()).padStart(2, '0');\n const h = String(timestamp.getUTCHours()).padStart(2, '0');\n const mi = String(timestamp.getUTCMinutes()).padStart(2, '0');\n\n return `${y}${mo}${d}T${h}${mi}_${truncated}`;\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"migration-graph-De0dUZoC.d.mts","names":[],"sources":["../src/migration-graph.ts"],"mappings":";;;;iBAsCgB,gBAAA,CAAiB,QAAA,WAAmB,sBAAA,KAA2B,cAAA;;AAA/E;;;;;;;iBAqFgB,QAAA,CACd,KAAA,EAAO,cAAA,EACP,QAAA,UACA,MAAA,oBACU,aAAA;;AAJZ;;;;;;;;;;;AAgDA;;;;;;;iBAAgB,sBAAA,CACd,KAAA,EAAO,cAAA,EACP,QAAA,UACA,MAAA,UACA,QAAA,EAAU,WAAA,oBACA,aAAA;AAAA,UAsFK,YAAA;EAAA,SACN,YAAA,WAAuB,aAAA;EAAA,SACvB,QAAA;EAAA,SACA,MAAA;EAAA,SACA,gBAAA;EAAA,SACA,eAAA;EAAA,SACA,OAAA;EA5Fc;EAAA,SA8Fd,kBAAA;EARM;;;;;EAAA,SAcN,mBAAA;AAAA;;;;;;;;;AAoBX;;;;;;;;;KAAY,eAAA;EAAA,SACG,IAAA;EAAA,SAAqB,QAAA,EAAU,YAAA;AAAA;EAAA,SAC/B,IAAA;AAAA;EAAA,SAEA,IAAA;EAAA,SACA,cAAA,WAAyB,aAAA;EAAA,SACzB,OAAA;AAAA;;;;;AAyBf;;;UAfiB,2BAAA;EAAA,SACN,OAAA;EAAA,SACA,QAAA,GAAW,WAAA;AAAA;;;;;;;;;;;iBAaN,oBAAA,CACd,KAAA,EAAO,cAAA,EACP,QAAA,UACA,MAAA,UACA,OAAA,GAAS,2BAAA,GACR,eAAA;;;;;iBAqLa,mBAAA,CAAoB,KAAA,EAAO,cAAA,EAAgB,QAAA;;;;AAkB3D;;;;;iBAAgB,QAAA,CAAS,KAAA,EAAO,cAAA;;;;;;iBAwChB,mBAAA,CAAoB,KAAA,EAAO,cAAA,GAAiB,aAAA;AAAA,iBAQ5C,YAAA,CAAa,KAAA,EAAO,cAAA;AAAA,iBA8DpB,aAAA,CAAc,KAAA,EAAO,cAAA,YAA0B,aAAA"}
|