@prisma-next/migration-tools 0.4.0-dev.9 → 0.4.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.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/dist/{attestation-DnebS4XZ.mjs → attestation-DtF8tEOM.mjs} +24 -23
  3. package/dist/attestation-DtF8tEOM.mjs.map +1 -0
  4. package/dist/{errors-C_XuSbX7.mjs → errors-BKbRGCJM.mjs} +9 -2
  5. package/dist/errors-BKbRGCJM.mjs.map +1 -0
  6. package/dist/exports/attestation.d.mts +20 -6
  7. package/dist/exports/attestation.d.mts.map +1 -1
  8. package/dist/exports/attestation.mjs +3 -3
  9. package/dist/exports/dag.d.mts +8 -6
  10. package/dist/exports/dag.d.mts.map +1 -1
  11. package/dist/exports/dag.mjs +181 -107
  12. package/dist/exports/dag.mjs.map +1 -1
  13. package/dist/exports/io.d.mts +16 -13
  14. package/dist/exports/io.d.mts.map +1 -1
  15. package/dist/exports/io.mjs +2 -2
  16. package/dist/exports/migration-ts.d.mts +10 -20
  17. package/dist/exports/migration-ts.d.mts.map +1 -1
  18. package/dist/exports/migration-ts.mjs +23 -35
  19. package/dist/exports/migration-ts.mjs.map +1 -1
  20. package/dist/exports/migration.d.mts +1 -1
  21. package/dist/exports/migration.mjs +20 -13
  22. package/dist/exports/migration.mjs.map +1 -1
  23. package/dist/exports/refs.mjs +1 -1
  24. package/dist/exports/types.d.mts +2 -2
  25. package/dist/exports/types.mjs +2 -16
  26. package/dist/{io-Cun81AIZ.mjs → io-CCnYsUHU.mjs} +18 -22
  27. package/dist/io-CCnYsUHU.mjs.map +1 -0
  28. package/dist/types-DyGXcWWp.d.mts +71 -0
  29. package/dist/types-DyGXcWWp.d.mts.map +1 -0
  30. package/package.json +5 -4
  31. package/src/attestation.ts +34 -26
  32. package/src/dag.ts +140 -154
  33. package/src/errors.ts +8 -0
  34. package/src/exports/attestation.ts +2 -1
  35. package/src/exports/io.ts +1 -1
  36. package/src/exports/migration-ts.ts +1 -1
  37. package/src/exports/types.ts +2 -8
  38. package/src/graph-ops.ts +65 -0
  39. package/src/io.ts +23 -24
  40. package/src/migration-base.ts +21 -13
  41. package/src/migration-ts.ts +23 -49
  42. package/src/queue.ts +37 -0
  43. package/src/types.ts +15 -55
  44. package/dist/attestation-DnebS4XZ.mjs.map +0 -1
  45. package/dist/errors-C_XuSbX7.mjs.map +0 -1
  46. package/dist/exports/types.mjs.map +0 -1
  47. package/dist/io-Cun81AIZ.mjs.map +0 -1
  48. package/dist/types-D2uX4ql7.d.mts +0 -100
  49. package/dist/types-D2uX4ql7.d.mts.map +0 -1
@@ -0,0 +1,65 @@
1
+ import { Queue } from './queue';
2
+
3
+ /**
4
+ * One step of a BFS traversal.
5
+ *
6
+ * `parent` and `incomingEdge` are `null` for start nodes — they were not
7
+ * reached via any edge. For every other node they record the node and edge
8
+ * by which this node was first reached.
9
+ */
10
+ export interface BfsStep<E> {
11
+ readonly node: string;
12
+ readonly parent: string | null;
13
+ readonly incomingEdge: E | null;
14
+ }
15
+
16
+ /**
17
+ * Generic breadth-first traversal.
18
+ *
19
+ * Direction (forward/reverse) is expressed by the caller's `neighbours`
20
+ * closure: return `{ next, edge }` pairs where `next` is the node to visit
21
+ * next and `edge` is the edge that connects them. Callers that don't need
22
+ * path reconstruction can ignore the `parent`/`incomingEdge` fields of each
23
+ * yielded step.
24
+ *
25
+ * Stops are intrinsic — callers `break` out of the `for..of` loop when
26
+ * they've found what they're looking for.
27
+ *
28
+ * `ordering`, if provided, controls the order in which neighbours of each
29
+ * node are enqueued. Only matters for path-finding: a deterministic ordering
30
+ * makes BFS return a deterministic shortest path when multiple exist.
31
+ */
32
+ export function* bfs<E>(
33
+ starts: Iterable<string>,
34
+ neighbours: (node: string) => Iterable<{ next: string; edge: E }>,
35
+ ordering?: (items: readonly { next: string; edge: E }[]) => readonly { next: string; edge: E }[],
36
+ ): Generator<BfsStep<E>> {
37
+ const visited = new Set<string>();
38
+ const parentMap = new Map<string, { parent: string; edge: E }>();
39
+ const queue = new Queue<string>();
40
+ for (const start of starts) {
41
+ if (!visited.has(start)) {
42
+ visited.add(start);
43
+ queue.push(start);
44
+ }
45
+ }
46
+ while (!queue.isEmpty) {
47
+ const current = queue.shift();
48
+ const parentInfo = parentMap.get(current);
49
+ yield {
50
+ node: current,
51
+ parent: parentInfo?.parent ?? null,
52
+ incomingEdge: parentInfo?.edge ?? null,
53
+ };
54
+
55
+ const items = neighbours(current);
56
+ const toVisit = ordering ? ordering([...items]) : items;
57
+ for (const { next, edge } of toVisit) {
58
+ if (!visited.has(next)) {
59
+ visited.add(next);
60
+ parentMap.set(next, { parent: current, edge });
61
+ queue.push(next);
62
+ }
63
+ }
64
+ }
65
+ }
package/src/io.ts CHANGED
@@ -3,12 +3,13 @@ import { type } from 'arktype';
3
3
  import { basename, dirname, join } from 'pathe';
4
4
  import {
5
5
  errorDirectoryExists,
6
+ errorInvalidDestName,
6
7
  errorInvalidJson,
7
8
  errorInvalidManifest,
8
9
  errorInvalidSlug,
9
10
  errorMissingFile,
10
11
  } from './errors';
11
- import type { BaseMigrationBundle, MigrationManifest, MigrationOps } from './types';
12
+ import type { MigrationBundle, MigrationManifest, MigrationOps } from './types';
12
13
 
13
14
  const MANIFEST_FILE = 'migration.json';
14
15
  const OPS_FILE = 'ops.json';
@@ -22,13 +23,12 @@ const MigrationHintsSchema = type({
22
23
  used: 'string[]',
23
24
  applied: 'string[]',
24
25
  plannerVersion: 'string',
25
- planningStrategy: 'string',
26
26
  });
27
27
 
28
28
  const MigrationManifestSchema = type({
29
29
  from: 'string',
30
30
  to: 'string',
31
- migrationId: 'string | null',
31
+ migrationId: 'string',
32
32
  kind: "'regular' | 'baseline'",
33
33
  fromContract: 'object | null',
34
34
  toContract: 'object',
@@ -75,27 +75,26 @@ export async function writeMigrationPackage(
75
75
  }
76
76
 
77
77
  /**
78
- * Copy the destination contract artifacts (`contract.json` and the
79
- * colocated `contract.d.ts`) into the migration package directory so
80
- * authors of the scaffolded `migration.ts` can import the typed
81
- * contract relative to the migration directory
82
- * (`import type { Contract } from './contract'`).
78
+ * Copy a list of files into `destDir`, optionally renaming each one.
83
79
  *
84
- * A missing `.d.ts` is tolerated (only the `.json` is required) so the
85
- * helper stays usable in tests that hand-roll a bare `contract.json`.
86
- * A missing `contract.json` or any other I/O failure — throws.
80
+ * The destination directory is created (with `recursive: true`) if it
81
+ * does not already exist. Each source path is copied byte-for-byte into
82
+ * `destDir/<destName>`; missing sources throw `ENOENT`. The helper is
83
+ * intentionally generic: callers own the list of files (e.g. a contract
84
+ * emitter's emitted output) and the naming convention (e.g. renaming
85
+ * the destination contract to `end-contract.*` and the source contract
86
+ * to `start-contract.*`).
87
87
  */
88
- export async function copyContractToMigrationDir(
89
- packageDir: string,
90
- contractJsonPath: string,
88
+ export async function copyFilesWithRename(
89
+ destDir: string,
90
+ files: readonly { readonly sourcePath: string; readonly destName: string }[],
91
91
  ): Promise<void> {
92
- await copyFile(contractJsonPath, join(packageDir, 'contract.json'));
93
- const dtsPath = `${contractJsonPath.slice(0, -'.json'.length)}.d.ts`;
94
- try {
95
- await copyFile(dtsPath, join(packageDir, 'contract.d.ts'));
96
- } catch (error) {
97
- if (hasErrnoCode(error, 'ENOENT')) return;
98
- throw error;
92
+ await mkdir(destDir, { recursive: true });
93
+ for (const file of files) {
94
+ if (basename(file.destName) !== file.destName) {
95
+ throw errorInvalidDestName(file.destName);
96
+ }
97
+ await copyFile(file.sourcePath, join(destDir, file.destName));
99
98
  }
100
99
  }
101
100
 
@@ -110,7 +109,7 @@ export async function writeMigrationOps(dir: string, ops: MigrationOps): Promise
110
109
  await writeFile(join(dir, OPS_FILE), `${JSON.stringify(ops, null, 2)}\n`);
111
110
  }
112
111
 
113
- export async function readMigrationPackage(dir: string): Promise<BaseMigrationBundle> {
112
+ export async function readMigrationPackage(dir: string): Promise<MigrationBundle> {
114
113
  const manifestPath = join(dir, MANIFEST_FILE);
115
114
  const opsPath = join(dir, OPS_FILE);
116
115
 
@@ -178,7 +177,7 @@ function validateOps(ops: unknown, filePath: string): asserts ops is MigrationOp
178
177
 
179
178
  export async function readMigrationsDir(
180
179
  migrationsRoot: string,
181
- ): Promise<readonly BaseMigrationBundle[]> {
180
+ ): Promise<readonly MigrationBundle[]> {
182
181
  let entries: string[];
183
182
  try {
184
183
  entries = await readdir(migrationsRoot);
@@ -189,7 +188,7 @@ export async function readMigrationsDir(
189
188
  throw error;
190
189
  }
191
190
 
192
- const packages: BaseMigrationBundle[] = [];
191
+ const packages: MigrationBundle[] = [];
193
192
 
194
193
  for (const entry of entries.sort()) {
195
194
  const entryPath = join(migrationsRoot, entry);
@@ -9,7 +9,7 @@ import { ifDefined } from '@prisma-next/utils/defined';
9
9
  import { type } from 'arktype';
10
10
  import { dirname, join } from 'pathe';
11
11
  import { computeMigrationId } from './attestation';
12
- import type { MigrationManifest, MigrationOps } from './types';
12
+ import type { MigrationHints, MigrationManifest, MigrationOps } from './types';
13
13
 
14
14
  export interface MigrationMeta {
15
15
  readonly from: string;
@@ -26,7 +26,7 @@ const MigrationMetaSchema = type({
26
26
  });
27
27
 
28
28
  /**
29
- * Base class for class-flow migrations.
29
+ * Base class for migrations.
30
30
  *
31
31
  * A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the
32
32
  * runner can consume it directly via `targetId`, `operations`, `origin`, and
@@ -140,9 +140,8 @@ function printHelp(): void {
140
140
  * schema-conformant manifest so the resulting package can still be read,
141
141
  * verified, and applied.
142
142
  *
143
- * In both cases the `migrationId` is recomputed against the current
144
- * manifest + ops so the on-disk artifacts are always fully attested — no
145
- * draft (`migrationId: null`) ever leaves this function.
143
+ * The `migrationId` is recomputed against the current manifest + ops so
144
+ * the on-disk artifacts are always fully attested.
146
145
  */
147
146
  function buildAttestedManifest(
148
147
  migrationDir: string,
@@ -151,8 +150,7 @@ function buildAttestedManifest(
151
150
  ): MigrationManifest {
152
151
  const existing = readExistingManifest(join(migrationDir, 'migration.json'));
153
152
 
154
- const baseManifest: MigrationManifest = {
155
- migrationId: null,
153
+ const baseManifest: Omit<MigrationManifest, 'migrationId'> = {
156
154
  from: meta.from,
157
155
  to: meta.to,
158
156
  kind: meta.kind ?? 'regular',
@@ -165,12 +163,7 @@ function buildAttestedManifest(
165
163
  // (everything else is stripped by `computeMigrationId`), and a real
166
164
  // contract bookend would only be available after `migration plan`.
167
165
  toContract: existing?.toContract ?? ({ storage: { storageHash: meta.to } } as Contract),
168
- hints: existing?.hints ?? {
169
- used: [],
170
- applied: [],
171
- plannerVersion: '2.0.0',
172
- planningStrategy: 'class-based',
173
- },
166
+ hints: normalizeHints(existing?.hints),
174
167
  ...ifDefined('authorship', existing?.authorship),
175
168
  };
176
169
 
@@ -178,6 +171,21 @@ function buildAttestedManifest(
178
171
  return { ...baseManifest, migrationId };
179
172
  }
180
173
 
174
+ /**
175
+ * Project `existing.hints` down to the known `MigrationHints` shape, dropping
176
+ * any legacy keys that may linger in manifests scaffolded by older CLI
177
+ * versions (e.g. `planningStrategy`). Picking fields explicitly instead of
178
+ * spreading keeps refreshed `migration.json` files schema-clean regardless
179
+ * of what was on disk before.
180
+ */
181
+ function normalizeHints(existing: MigrationHints | undefined): MigrationHints {
182
+ return {
183
+ used: existing?.used ?? [],
184
+ applied: existing?.applied ?? [],
185
+ plannerVersion: existing?.plannerVersion ?? '2.0.0',
186
+ };
187
+ }
188
+
181
189
  function readExistingManifest(manifestPath: string): Partial<MigrationManifest> | null {
182
190
  let raw: string;
183
191
  try {
@@ -1,17 +1,15 @@
1
1
  /**
2
2
  * Utilities for reading/writing `migration.ts` files.
3
3
  *
4
- * Rendering migration.ts source is now the target's responsibility — the CLI
5
- * obtains source strings either from a class-flow planner's
6
- * `plan.renderTypeScript()` or from a descriptor-flow target's
7
- * `migrations.renderDescriptorTypeScript(descriptors, context)`. The helper
8
- * here is limited to file I/O: writing the returned source with the right
9
- * executable bit, probing for existence, and evaluating legacy descriptor-
10
- * flow files.
4
+ * Rendering migration.ts source is the target's responsibility — the CLI
5
+ * obtains source strings from a planner's `plan.renderTypeScript()`. The
6
+ * helper here is limited to file I/O: writing the returned source with the
7
+ * right executable bit and probing for existence.
11
8
  */
12
9
 
13
10
  import { stat, writeFile } from 'node:fs/promises';
14
- import { join, resolve } from 'pathe';
11
+ import { join } from 'pathe';
12
+ import { format } from 'prettier';
15
13
 
16
14
  const MIGRATION_TS_FILE = 'migration.ts';
17
15
 
@@ -20,16 +18,31 @@ const MIGRATION_TS_FILE = 'migration.ts';
20
18
  * directory. If the source begins with a shebang, the file is written with
21
19
  * executable permissions (0o755) so it can be run directly via
22
20
  * `./migration.ts` by the authoring class's `Migration.run(...)` guard.
21
+ *
22
+ * The source is run through prettier before writing so migration renderers
23
+ * can produce structurally-correct but loosely-indented source and rely on
24
+ * a single canonical format on disk. Matches what `@prisma-next/emitter`
25
+ * already does for generated `contract.d.ts`.
23
26
  */
24
27
  export async function writeMigrationTs(packageDir: string, content: string): Promise<void> {
25
- const isExecutable = content.startsWith('#!');
28
+ const formatted = await formatMigrationTsSource(content);
29
+ const isExecutable = formatted.startsWith('#!');
26
30
  await writeFile(
27
31
  join(packageDir, MIGRATION_TS_FILE),
28
- content,
32
+ formatted,
29
33
  isExecutable ? { mode: 0o755 } : undefined,
30
34
  );
31
35
  }
32
36
 
37
+ async function formatMigrationTsSource(source: string): Promise<string> {
38
+ return format(source, {
39
+ parser: 'typescript',
40
+ singleQuote: true,
41
+ semi: true,
42
+ printWidth: 100,
43
+ });
44
+ }
45
+
33
46
  /**
34
47
  * Checks whether a migration.ts file exists in the package directory.
35
48
  */
@@ -41,42 +54,3 @@ export async function hasMigrationTs(packageDir: string): Promise<boolean> {
41
54
  return false;
42
55
  }
43
56
  }
44
-
45
- /**
46
- * Evaluates a descriptor-flow migration.ts file by loading it via native
47
- * Node import. Returns the result of calling the default export (expected
48
- * to be a function returning an array of operation descriptors).
49
- *
50
- * Class-flow migration.ts files use a different shape — their default
51
- * export is a class that extends `Migration` — and are evaluated by the
52
- * target's `emit` capability, not this helper.
53
- *
54
- * Requires Node ≥24 for native TypeScript support.
55
- */
56
- export async function evaluateMigrationTs(packageDir: string): Promise<readonly unknown[]> {
57
- const filePath = resolve(join(packageDir, MIGRATION_TS_FILE));
58
-
59
- try {
60
- await stat(filePath);
61
- } catch {
62
- throw new Error(`migration.ts not found at "${filePath}"`);
63
- }
64
-
65
- const mod = (await import(filePath)) as { default?: unknown };
66
-
67
- if (typeof mod.default !== 'function') {
68
- throw new Error(
69
- `migration.ts must export a default function returning an operation list. Got: ${typeof mod.default}`,
70
- );
71
- }
72
-
73
- const result: unknown = mod.default();
74
-
75
- if (!Array.isArray(result)) {
76
- throw new Error(
77
- `migration.ts default export must return an array of operations. Got: ${typeof result}`,
78
- );
79
- }
80
-
81
- return result;
82
- }
package/src/queue.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * FIFO queue with amortised O(1) push and shift.
3
+ *
4
+ * Uses a head-index cursor over a backing array rather than
5
+ * `Array.prototype.shift()`, which is O(n) on V8. Intended for BFS-shaped
6
+ * traversals where the queue is drained in a single pass — it does not
7
+ * reclaim memory for already-shifted items, so it is not suitable for
8
+ * long-lived queues with many push/shift cycles.
9
+ */
10
+ export class Queue<T> {
11
+ private readonly items: T[];
12
+ private head = 0;
13
+
14
+ constructor(initial: Iterable<T> = []) {
15
+ this.items = [...initial];
16
+ }
17
+
18
+ push(item: T): void {
19
+ this.items.push(item);
20
+ }
21
+
22
+ /**
23
+ * Remove and return the next item. Caller must check `isEmpty` first —
24
+ * shifting an empty queue throws.
25
+ */
26
+ shift(): T {
27
+ if (this.head >= this.items.length) {
28
+ throw new Error('Queue.shift called on empty queue');
29
+ }
30
+ // biome-ignore lint/style/noNonNullAssertion: bounds-checked on the line above
31
+ return this.items[this.head++]!;
32
+ }
33
+
34
+ get isEmpty(): boolean {
35
+ return this.head >= this.items.length;
36
+ }
37
+ }
package/src/types.ts CHANGED
@@ -5,13 +5,22 @@ export interface MigrationHints {
5
5
  readonly used: readonly string[];
6
6
  readonly applied: readonly string[];
7
7
  readonly plannerVersion: string;
8
- readonly planningStrategy: string;
9
8
  }
10
9
 
11
10
  /**
12
- * Shared fields for all migration manifests (draft and attested).
11
+ * On-disk migration manifest. Every migration is content-addressed: the
12
+ * `migrationId` is a hash over the manifest envelope plus the operations
13
+ * list, computed at write time. There is no draft state — a migration
14
+ * directory either exists with a fully attested manifest or it does not.
15
+ *
16
+ * When the planner cannot lower an operation because of an unfilled
17
+ * `placeholder(...)` slot, the migration is still written with
18
+ * `migrationId` hashed over `ops: []`. Re-running self-emit after the
19
+ * user fills the placeholder produces a *different* `migrationId`
20
+ * (committed to the real ops); this is intentional.
13
21
  */
14
- interface MigrationManifestBase {
22
+ export interface MigrationManifest {
23
+ readonly migrationId: string;
15
24
  readonly from: string;
16
25
  readonly to: string;
17
26
  readonly kind: 'regular' | 'baseline';
@@ -24,37 +33,12 @@ interface MigrationManifestBase {
24
33
  readonly createdAt: string;
25
34
  }
26
35
 
27
- /**
28
- * A draft migration that has been planned but not yet attested.
29
- * Draft migrations have `migrationId: null` and are excluded from
30
- * graph reconstruction and apply.
31
- */
32
- export interface DraftMigrationManifest extends MigrationManifestBase {
33
- readonly migrationId: null;
34
- }
35
-
36
- /**
37
- * An attested migration with a content-addressed migrationId.
38
- * Only attested migrations participate in the migration graph.
39
- */
40
- export interface AttestedMigrationManifest extends MigrationManifestBase {
41
- readonly migrationId: string;
42
- }
43
-
44
- /**
45
- * Union of draft and attested manifests. This is what the on-disk
46
- * format represents — `migrationId` is `null` for drafts, a string
47
- * for attested migrations.
48
- */
49
- export type MigrationManifest = DraftMigrationManifest | AttestedMigrationManifest;
50
-
51
36
  export type MigrationOps = readonly MigrationPlanOperation[];
52
37
 
53
38
  /**
54
39
  * An on-disk migration directory containing a manifest and operations.
55
- * The manifest may be draft or attested.
56
40
  */
57
- export interface BaseMigrationBundle {
41
+ export interface MigrationBundle {
58
42
  readonly dirName: string;
59
43
  readonly dirPath: string;
60
44
  readonly manifest: MigrationManifest;
@@ -62,20 +46,8 @@ export interface BaseMigrationBundle {
62
46
  }
63
47
 
64
48
  /**
65
- * A bundle known to be attested (migrationId is a string).
66
- * Use this after filtering bundles to attested-only.
67
- */
68
- export interface AttestedMigrationBundle extends BaseMigrationBundle {
69
- readonly manifest: AttestedMigrationManifest;
70
- }
71
-
72
- export interface DraftMigrationBundle extends BaseMigrationBundle {
73
- readonly manifest: DraftMigrationManifest;
74
- }
75
-
76
- /**
77
- * An entry in the migration graph. Only attested migrations appear in the
78
- * graph, so `migrationId` is always a string.
49
+ * An entry in the migration graph. All on-disk migrations are attested,
50
+ * so `migrationId` is always a string.
79
51
  */
80
52
  export interface MigrationChainEntry {
81
53
  readonly from: string;
@@ -92,15 +64,3 @@ export interface MigrationGraph {
92
64
  readonly reverseChain: ReadonlyMap<string, readonly MigrationChainEntry[]>;
93
65
  readonly migrationById: ReadonlyMap<string, MigrationChainEntry>;
94
66
  }
95
-
96
- /**
97
- * Type guard that narrows a MigrationBundle to an AttestedMigrationBundle.
98
- * Use with `.filter(isAttested)` to get a typed array of attested bundles.
99
- */
100
- export function isAttested(bundle: BaseMigrationBundle): bundle is AttestedMigrationBundle {
101
- return typeof bundle.manifest.migrationId === 'string';
102
- }
103
-
104
- export function isDraft(bundle: BaseMigrationBundle): bundle is DraftMigrationBundle {
105
- return bundle.manifest.migrationId === null;
106
- }
@@ -1 +0,0 @@
1
- {"version":3,"file":"attestation-DnebS4XZ.mjs","names":["sorted: Record<string, unknown>"],"sources":["../src/canonicalize-json.ts","../src/attestation.ts"],"sourcesContent":["function sortKeys(value: unknown): unknown {\n if (value === null || typeof value !== 'object') {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map(sortKeys);\n }\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(value).sort()) {\n sorted[key] = sortKeys((value as Record<string, unknown>)[key]);\n }\n return sorted;\n}\n\nexport function canonicalizeJson(value: unknown): string {\n return JSON.stringify(sortKeys(value));\n}\n","import { createHash } from 'node:crypto';\nimport { canonicalizeJson } from './canonicalize-json';\nimport { readMigrationPackage, writeMigrationManifest } from './io';\nimport type { MigrationManifest, MigrationOps } from './types';\n\nexport interface VerifyResult {\n readonly ok: boolean;\n readonly reason?: 'draft' | 'mismatch';\n readonly storedMigrationId?: string;\n readonly computedMigrationId?: string;\n}\n\nfunction sha256Hex(input: string): string {\n return createHash('sha256').update(input).digest('hex');\n}\n\n/**\n * Content-addressed migration identity over (manifest envelope sans\n * contracts/hints, ops). See ADR 199 \"Storage-only migration identity\"\n * for the rationale: contracts are anchored separately by the\n * storage-hash bookends inside the envelope; planner hints are advisory\n * and must not affect identity.\n */\nexport function computeMigrationId(manifest: MigrationManifest, ops: MigrationOps): string {\n const {\n migrationId: _migrationId,\n signature: _signature,\n fromContract: _fromContract,\n toContract: _toContract,\n hints: _hints,\n ...strippedMeta\n } = manifest;\n\n const canonicalManifest = canonicalizeJson(strippedMeta);\n const canonicalOps = canonicalizeJson(ops);\n\n const partHashes = [canonicalManifest, canonicalOps].map(sha256Hex);\n const hash = sha256Hex(canonicalizeJson(partHashes));\n\n return `sha256:${hash}`;\n}\n\n/** Compute and persist `migrationId` to `manifest.json`. */\nexport async function attestMigration(dir: string): Promise<string> {\n const pkg = await readMigrationPackage(dir);\n const migrationId = computeMigrationId(pkg.manifest, pkg.ops);\n\n const updated = { ...pkg.manifest, migrationId };\n await writeMigrationManifest(dir, updated);\n\n return migrationId;\n}\n\nexport async function verifyMigration(dir: string): Promise<VerifyResult> {\n const pkg = await readMigrationPackage(dir);\n\n if (pkg.manifest.migrationId === null) {\n return { ok: false, reason: 'draft' };\n }\n\n const computed = computeMigrationId(pkg.manifest, pkg.ops);\n\n if (pkg.manifest.migrationId === computed) {\n return { ok: true, storedMigrationId: pkg.manifest.migrationId, computedMigrationId: computed };\n }\n\n return {\n ok: false,\n reason: 'mismatch',\n storedMigrationId: pkg.manifest.migrationId,\n computedMigrationId: computed,\n };\n}\n"],"mappings":";;;;AAAA,SAAS,SAAS,OAAyB;AACzC,KAAI,UAAU,QAAQ,OAAO,UAAU,SACrC,QAAO;AAET,KAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,MAAM,IAAI,SAAS;CAE5B,MAAMA,SAAkC,EAAE;AAC1C,MAAK,MAAM,OAAO,OAAO,KAAK,MAAM,CAAC,MAAM,CACzC,QAAO,OAAO,SAAU,MAAkC,KAAK;AAEjE,QAAO;;AAGT,SAAgB,iBAAiB,OAAwB;AACvD,QAAO,KAAK,UAAU,SAAS,MAAM,CAAC;;;;;ACHxC,SAAS,UAAU,OAAuB;AACxC,QAAO,WAAW,SAAS,CAAC,OAAO,MAAM,CAAC,OAAO,MAAM;;;;;;;;;AAUzD,SAAgB,mBAAmB,UAA6B,KAA2B;CACzF,MAAM,EACJ,aAAa,cACb,WAAW,YACX,cAAc,eACd,YAAY,aACZ,OAAO,QACP,GAAG,iBACD;AAQJ,QAAO,UAFM,UAAU,iBADJ,CAHO,iBAAiB,aAAa,EACnC,iBAAiB,IAAI,CAEU,CAAC,IAAI,UAAU,CAChB,CAAC;;;AAMtD,eAAsB,gBAAgB,KAA8B;CAClE,MAAM,MAAM,MAAM,qBAAqB,IAAI;CAC3C,MAAM,cAAc,mBAAmB,IAAI,UAAU,IAAI,IAAI;AAG7D,OAAM,uBAAuB,KADb;EAAE,GAAG,IAAI;EAAU;EAAa,CACN;AAE1C,QAAO;;AAGT,eAAsB,gBAAgB,KAAoC;CACxE,MAAM,MAAM,MAAM,qBAAqB,IAAI;AAE3C,KAAI,IAAI,SAAS,gBAAgB,KAC/B,QAAO;EAAE,IAAI;EAAO,QAAQ;EAAS;CAGvC,MAAM,WAAW,mBAAmB,IAAI,UAAU,IAAI,IAAI;AAE1D,KAAI,IAAI,SAAS,gBAAgB,SAC/B,QAAO;EAAE,IAAI;EAAM,mBAAmB,IAAI,SAAS;EAAa,qBAAqB;EAAU;AAGjG,QAAO;EACL,IAAI;EACJ,QAAQ;EACR,mBAAmB,IAAI,SAAS;EAChC,qBAAqB;EACtB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"errors-C_XuSbX7.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\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, attestation,\n * migration history reconstruction), distinct from the runtime MIGRATION.* codes for apply-time\n * failures (PRECHECK_FAILED, POSTCHECK_FAILED, 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: 'Ensure the migration directory contains both migration.json and ops.json. If the directory is corrupt, delete it and re-run migration plan.',\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: 'Fix the JSON syntax error, or delete the migration directory and re-run migration plan.',\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: `Manifest at \"${filePath}\" is invalid: ${reason}`,\n fix: 'Ensure the manifest has all required fields (from, to, kind, toContract). If corrupt, delete and re-plan.',\n details: { filePath, reason },\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 errorSameSourceAndTarget(dirName: string, hash: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.SAME_SOURCE_AND_TARGET',\n 'Migration has same source and target',\n {\n why: `Migration \"${dirName}\" has from === to === \"${hash}\". A migration must transition between two different contract states.`,\n fix: 'Delete the invalid migration directory and re-run migration plan.',\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 `migration 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 errorDuplicateMigrationId(migrationId: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.DUPLICATE_MIGRATION_ID',\n 'Duplicate migrationId in migration graph',\n {\n why: `Multiple migrations share migrationId \"${migrationId}\". Each migration must have a unique content-addressed identity.`,\n fix: 'Regenerate one of the conflicting migrations so each migrationId is unique, then re-run migration commands.',\n details: { migrationId },\n },\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAeA,IAAa,sBAAb,cAAyC,MAAM;CAC7C,AAAS;CACT,AAAS,WAAW;CACpB,AAAS;CACT,AAAS;CACT,AAAS;CAET,YACE,MACA,SACA,SAKA;AACA,QAAM,QAAQ;AACd,OAAK,OAAO;AACZ,OAAK,OAAO;AACZ,OAAK,MAAM,QAAQ;AACnB,OAAK,MAAM,QAAQ;AACnB,OAAK,UAAU,QAAQ;;CAGzB,OAAO,GAAG,OAA8C;AACtD,MAAI,EAAE,iBAAiB,OAAQ,QAAO;EACtC,MAAM,YAAY;AAClB,SAAO,UAAU,SAAS,yBAAyB,OAAO,UAAU,SAAS;;;AAIjF,SAAgB,qBAAqB,KAAkC;AACrE,QAAO,IAAI,oBAAoB,wBAAwB,sCAAsC;EAC3F,KAAK,kBAAkB,IAAI;EAC3B,KAAK;EACL,SAAS,EAAE,KAAK;EACjB,CAAC;;AAGJ,SAAgB,iBAAiB,MAAc,KAAkC;AAC/E,QAAO,IAAI,oBAAoB,0BAA0B,WAAW,QAAQ;EAC1E,KAAK,aAAa,KAAK,QAAQ,IAAI;EACnC,KAAK;EACL,SAAS;GAAE;GAAM;GAAK;EACvB,CAAC;;AAGJ,SAAgB,iBAAiB,UAAkB,YAAyC;AAC1F,QAAO,IAAI,oBAAoB,0BAA0B,kCAAkC;EACzF,KAAK,oBAAoB,SAAS,KAAK;EACvC,KAAK;EACL,SAAS;GAAE;GAAU;GAAY;EAClC,CAAC;;AAGJ,SAAgB,qBAAqB,UAAkB,QAAqC;AAC1F,QAAO,IAAI,oBAAoB,8BAA8B,8BAA8B;EACzF,KAAK,gBAAgB,SAAS,gBAAgB;EAC9C,KAAK;EACL,SAAS;GAAE;GAAU;GAAQ;EAC9B,CAAC;;AAGJ,SAAgB,iBAAiB,MAAmC;AAClE,QAAO,IAAI,oBAAoB,0BAA0B,0BAA0B;EACjF,KAAK,aAAa,KAAK;EACvB,KAAK;EACL,SAAS,EAAE,MAAM;EAClB,CAAC;;AAGJ,SAAgB,yBAAyB,SAAiB,MAAmC;AAC3F,QAAO,IAAI,oBACT,oCACA,wCACA;EACE,KAAK,cAAc,QAAQ,yBAAyB,KAAK;EACzD,KAAK;EACL,SAAS;GAAE;GAAS;GAAM;EAC3B,CACF;;AAGH,SAAgB,qBACd,YACA,SAOqB;CACrB,MAAM,iBAAiB,UACnB,uBAAuB,QAAQ,gBAAgB,eAAe,QAAQ,SAAS,KAAK,MAAM,OAAO,EAAE,IAAI,IAAI,EAAE,MAAM,OAAO,YAAY,EAAE,MAAM,KAAK,MAAM,EAAE,QAAQ,CAAC,KAAK,MAAM,IAAI,SAAS,GAAG,CAAC,KAAK,KAAK,KAC1M;AACJ,QAAO,IAAI,oBAAoB,8BAA8B,8BAA8B;EACzF,KAAK,8DAA8D,WAAW,KAAK,KAAK,CAAC,4FAA4F;EACrL,KAAK;EACL,SAAS;GACP;GACA,GAAI,UAAU;IAAE,iBAAiB,QAAQ;IAAiB,UAAU,QAAQ;IAAU,GAAG,EAAE;GAC5F;EACF,CAAC;;AAGJ,SAAgB,wBAAwB,OAA+C;AACrF,QAAO,IAAI,oBAAoB,kCAAkC,8BAA8B;EAC7F,KAAK,oEAAoE,MAAM,KAAK,KAAK,CAAC;EAC1F,KAAK;EACL,SAAS,EAAE,OAAO;EACnB,CAAC;;AAGJ,SAAgB,iBAAiB,UAAkB,QAAqC;AACtF,QAAO,IAAI,oBAAoB,0BAA0B,qBAAqB;EAC5E,KAAK,iBAAiB,SAAS,gBAAgB;EAC/C,KAAK;EACL,SAAS;GAAE,MAAM;GAAU;GAAQ;EACpC,CAAC;;AAWJ,SAAgB,oBAAoB,SAAsC;AACxE,QAAO,IAAI,oBAAoB,8BAA8B,oBAAoB;EAC/E,KAAK,aAAa,QAAQ;EAC1B,KAAK;EACL,SAAS,EAAE,SAAS;EACrB,CAAC;;AAGJ,SAAgB,cAAc,iBAAyD;AACrF,QAAO,IAAI,oBAAoB,uBAAuB,yCAAyC;EAC7F,KAAK,wGAAwG,gBAAgB,KAAK,KAAK,CAAC;EACxI,KAAK;EACL,SAAS,EAAE,iBAAiB;EAC7B,CAAC;;AAGJ,SAAgB,qBAAqB,OAAoC;AACvE,QAAO,IAAI,oBAAoB,+BAA+B,qBAAqB;EACjF,KAAK,cAAc,MAAM;EACzB,KAAK;EACL,SAAS,EAAE,OAAO;EACnB,CAAC;;AAGJ,SAAgB,0BAA0B,aAA0C;AAClF,QAAO,IAAI,oBACT,oCACA,4CACA;EACE,KAAK,0CAA0C,YAAY;EAC3D,KAAK;EACL,SAAS,EAAE,aAAa;EACzB,CACF"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"types.mjs","names":[],"sources":["../../src/types.ts"],"sourcesContent":["import type { Contract } from '@prisma-next/contract/types';\nimport type { MigrationPlanOperation } from '@prisma-next/framework-components/control';\n\nexport interface MigrationHints {\n readonly used: readonly string[];\n readonly applied: readonly string[];\n readonly plannerVersion: string;\n readonly planningStrategy: string;\n}\n\n/**\n * Shared fields for all migration manifests (draft and attested).\n */\ninterface MigrationManifestBase {\n readonly from: string;\n readonly to: string;\n readonly kind: 'regular' | 'baseline';\n readonly fromContract: Contract | null;\n readonly toContract: Contract;\n readonly hints: MigrationHints;\n readonly labels: readonly string[];\n readonly authorship?: { readonly author?: string; readonly email?: string };\n readonly signature?: { readonly keyId: string; readonly value: string } | null;\n readonly createdAt: string;\n}\n\n/**\n * A draft migration that has been planned but not yet attested.\n * Draft migrations have `migrationId: null` and are excluded from\n * graph reconstruction and apply.\n */\nexport interface DraftMigrationManifest extends MigrationManifestBase {\n readonly migrationId: null;\n}\n\n/**\n * An attested migration with a content-addressed migrationId.\n * Only attested migrations participate in the migration graph.\n */\nexport interface AttestedMigrationManifest extends MigrationManifestBase {\n readonly migrationId: string;\n}\n\n/**\n * Union of draft and attested manifests. This is what the on-disk\n * format represents — `migrationId` is `null` for drafts, a string\n * for attested migrations.\n */\nexport type MigrationManifest = DraftMigrationManifest | AttestedMigrationManifest;\n\nexport type MigrationOps = readonly MigrationPlanOperation[];\n\n/**\n * An on-disk migration directory containing a manifest and operations.\n * The manifest may be draft or attested.\n */\nexport interface BaseMigrationBundle {\n readonly dirName: string;\n readonly dirPath: string;\n readonly manifest: MigrationManifest;\n readonly ops: MigrationOps;\n}\n\n/**\n * A bundle known to be attested (migrationId is a string).\n * Use this after filtering bundles to attested-only.\n */\nexport interface AttestedMigrationBundle extends BaseMigrationBundle {\n readonly manifest: AttestedMigrationManifest;\n}\n\nexport interface DraftMigrationBundle extends BaseMigrationBundle {\n readonly manifest: DraftMigrationManifest;\n}\n\n/**\n * An entry in the migration graph. Only attested migrations appear in the\n * graph, so `migrationId` is always a string.\n */\nexport interface MigrationChainEntry {\n readonly from: string;\n readonly to: string;\n readonly migrationId: string;\n readonly dirName: string;\n readonly createdAt: string;\n readonly labels: readonly string[];\n}\n\nexport interface MigrationGraph {\n readonly nodes: ReadonlySet<string>;\n readonly forwardChain: ReadonlyMap<string, readonly MigrationChainEntry[]>;\n readonly reverseChain: ReadonlyMap<string, readonly MigrationChainEntry[]>;\n readonly migrationById: ReadonlyMap<string, MigrationChainEntry>;\n}\n\n/**\n * Type guard that narrows a MigrationBundle to an AttestedMigrationBundle.\n * Use with `.filter(isAttested)` to get a typed array of attested bundles.\n */\nexport function isAttested(bundle: BaseMigrationBundle): bundle is AttestedMigrationBundle {\n return typeof bundle.manifest.migrationId === 'string';\n}\n\nexport function isDraft(bundle: BaseMigrationBundle): bundle is DraftMigrationBundle {\n return bundle.manifest.migrationId === null;\n}\n"],"mappings":";;;;;;;AAmGA,SAAgB,WAAW,QAAgE;AACzF,QAAO,OAAO,OAAO,SAAS,gBAAgB;;AAGhD,SAAgB,QAAQ,QAA6D;AACnF,QAAO,OAAO,SAAS,gBAAgB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"io-Cun81AIZ.mjs","names":["manifestRaw: string","opsRaw: string","manifest: MigrationManifest","ops: MigrationOps","entries: string[]","packages: BaseMigrationBundle[]"],"sources":["../src/io.ts"],"sourcesContent":["import { copyFile, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';\nimport { type } from 'arktype';\nimport { basename, dirname, join } from 'pathe';\nimport {\n errorDirectoryExists,\n errorInvalidJson,\n errorInvalidManifest,\n errorInvalidSlug,\n errorMissingFile,\n} from './errors';\nimport type { BaseMigrationBundle, MigrationManifest, MigrationOps } from './types';\n\nconst 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 planningStrategy: 'string',\n});\n\nconst MigrationManifestSchema = type({\n from: 'string',\n to: 'string',\n migrationId: 'string | null',\n kind: \"'regular' | 'baseline'\",\n fromContract: 'object | null',\n toContract: 'object',\n hints: MigrationHintsSchema,\n labels: 'string[]',\n 'authorship?': type({\n 'author?': 'string',\n 'email?': 'string',\n }),\n 'signature?': type({\n keyId: 'string',\n value: 'string',\n }).or('null'),\n createdAt: 'string',\n});\n\nconst MigrationOpSchema = type({\n id: 'string',\n label: 'string',\n operationClass: \"'additive' | 'widening' | 'destructive' | 'data'\",\n});\n\n// Intentionally shallow: operation-specific payload validation is owned by planner/runner layers.\nconst MigrationOpsSchema = MigrationOpSchema.array();\n\nexport async function writeMigrationPackage(\n dir: string,\n manifest: MigrationManifest,\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(manifest, null, 2), { flag: 'wx' });\n await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: 'wx' });\n}\n\n/**\n * Copy the destination contract artifacts (`contract.json` and the\n * colocated `contract.d.ts`) into the migration package directory so\n * authors of the scaffolded `migration.ts` can import the typed\n * contract relative to the migration directory\n * (`import type { Contract } from './contract'`).\n *\n * A missing `.d.ts` is tolerated (only the `.json` is required) so the\n * helper stays usable in tests that hand-roll a bare `contract.json`.\n * A missing `contract.json` — or any other I/O failure — throws.\n */\nexport async function copyContractToMigrationDir(\n packageDir: string,\n contractJsonPath: string,\n): Promise<void> {\n await copyFile(contractJsonPath, join(packageDir, 'contract.json'));\n const dtsPath = `${contractJsonPath.slice(0, -'.json'.length)}.d.ts`;\n try {\n await copyFile(dtsPath, join(packageDir, 'contract.d.ts'));\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) return;\n throw error;\n }\n}\n\nexport async function writeMigrationManifest(\n dir: string,\n manifest: MigrationManifest,\n): Promise<void> {\n await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(manifest, 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<BaseMigrationBundle> {\n const manifestPath = join(dir, MANIFEST_FILE);\n const opsPath = join(dir, 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, dir);\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, dir);\n }\n throw error;\n }\n\n let manifest: MigrationManifest;\n try {\n manifest = 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 validateManifest(manifest, manifestPath);\n validateOps(ops, opsPath);\n\n return {\n dirName: basename(dir),\n dirPath: dir,\n manifest,\n ops,\n };\n}\n\nfunction validateManifest(\n manifest: unknown,\n filePath: string,\n): asserts manifest is MigrationManifest {\n const result = MigrationManifestSchema(manifest);\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 BaseMigrationBundle[]> {\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: BaseMigrationBundle[] = [];\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,100 +0,0 @@
1
- import { Contract } from "@prisma-next/contract/types";
2
- import { MigrationPlanOperation } from "@prisma-next/framework-components/control";
3
-
4
- //#region src/types.d.ts
5
- interface MigrationHints {
6
- readonly used: readonly string[];
7
- readonly applied: readonly string[];
8
- readonly plannerVersion: string;
9
- readonly planningStrategy: string;
10
- }
11
- /**
12
- * Shared fields for all migration manifests (draft and attested).
13
- */
14
- interface MigrationManifestBase {
15
- readonly from: string;
16
- readonly to: string;
17
- readonly kind: 'regular' | 'baseline';
18
- readonly fromContract: Contract | null;
19
- readonly toContract: Contract;
20
- readonly hints: MigrationHints;
21
- readonly labels: readonly string[];
22
- readonly authorship?: {
23
- readonly author?: string;
24
- readonly email?: string;
25
- };
26
- readonly signature?: {
27
- readonly keyId: string;
28
- readonly value: string;
29
- } | null;
30
- readonly createdAt: string;
31
- }
32
- /**
33
- * A draft migration that has been planned but not yet attested.
34
- * Draft migrations have `migrationId: null` and are excluded from
35
- * graph reconstruction and apply.
36
- */
37
- interface DraftMigrationManifest extends MigrationManifestBase {
38
- readonly migrationId: null;
39
- }
40
- /**
41
- * An attested migration with a content-addressed migrationId.
42
- * Only attested migrations participate in the migration graph.
43
- */
44
- interface AttestedMigrationManifest extends MigrationManifestBase {
45
- readonly migrationId: string;
46
- }
47
- /**
48
- * Union of draft and attested manifests. This is what the on-disk
49
- * format represents — `migrationId` is `null` for drafts, a string
50
- * for attested migrations.
51
- */
52
- type MigrationManifest = DraftMigrationManifest | AttestedMigrationManifest;
53
- type MigrationOps = readonly MigrationPlanOperation[];
54
- /**
55
- * An on-disk migration directory containing a manifest and operations.
56
- * The manifest may be draft or attested.
57
- */
58
- interface BaseMigrationBundle {
59
- readonly dirName: string;
60
- readonly dirPath: string;
61
- readonly manifest: MigrationManifest;
62
- readonly ops: MigrationOps;
63
- }
64
- /**
65
- * A bundle known to be attested (migrationId is a string).
66
- * Use this after filtering bundles to attested-only.
67
- */
68
- interface AttestedMigrationBundle extends BaseMigrationBundle {
69
- readonly manifest: AttestedMigrationManifest;
70
- }
71
- interface DraftMigrationBundle extends BaseMigrationBundle {
72
- readonly manifest: DraftMigrationManifest;
73
- }
74
- /**
75
- * An entry in the migration graph. Only attested migrations appear in the
76
- * graph, so `migrationId` is always a string.
77
- */
78
- interface MigrationChainEntry {
79
- readonly from: string;
80
- readonly to: string;
81
- readonly migrationId: string;
82
- readonly dirName: string;
83
- readonly createdAt: string;
84
- readonly labels: readonly string[];
85
- }
86
- interface MigrationGraph {
87
- readonly nodes: ReadonlySet<string>;
88
- readonly forwardChain: ReadonlyMap<string, readonly MigrationChainEntry[]>;
89
- readonly reverseChain: ReadonlyMap<string, readonly MigrationChainEntry[]>;
90
- readonly migrationById: ReadonlyMap<string, MigrationChainEntry>;
91
- }
92
- /**
93
- * Type guard that narrows a MigrationBundle to an AttestedMigrationBundle.
94
- * Use with `.filter(isAttested)` to get a typed array of attested bundles.
95
- */
96
- declare function isAttested(bundle: BaseMigrationBundle): bundle is AttestedMigrationBundle;
97
- declare function isDraft(bundle: BaseMigrationBundle): bundle is DraftMigrationBundle;
98
- //#endregion
99
- export { DraftMigrationManifest as a, MigrationHints as c, isAttested as d, isDraft as f, DraftMigrationBundle as i, MigrationManifest as l, AttestedMigrationManifest as n, MigrationChainEntry as o, BaseMigrationBundle as r, MigrationGraph as s, AttestedMigrationBundle as t, MigrationOps as u };
100
- //# sourceMappingURL=types-D2uX4ql7.d.mts.map