@prisma-next/migration-tools 0.5.0-dev.1 → 0.5.0-dev.11

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 (79) hide show
  1. package/README.md +34 -22
  2. package/dist/{constants-BRi0X7B_.mjs → constants-WVGVMOdu.mjs} +1 -1
  3. package/dist/{constants-BRi0X7B_.mjs.map → constants-WVGVMOdu.mjs.map} +1 -1
  4. package/dist/{errors-BKbRGCJM.mjs → errors-CZ9JD4sd.mjs} +50 -21
  5. package/dist/errors-CZ9JD4sd.mjs.map +1 -0
  6. package/dist/exports/constants.mjs +1 -1
  7. package/dist/exports/{types.d.mts → errors.d.mts} +6 -8
  8. package/dist/exports/errors.d.mts.map +1 -0
  9. package/dist/exports/errors.mjs +3 -0
  10. package/dist/exports/graph.d.mts +2 -0
  11. package/dist/exports/graph.mjs +1 -0
  12. package/dist/exports/hash.d.mts +52 -0
  13. package/dist/exports/hash.d.mts.map +1 -0
  14. package/dist/exports/hash.mjs +3 -0
  15. package/dist/exports/io.d.mts +7 -6
  16. package/dist/exports/io.d.mts.map +1 -1
  17. package/dist/exports/io.mjs +156 -2
  18. package/dist/exports/io.mjs.map +1 -0
  19. package/dist/exports/metadata.d.mts +2 -0
  20. package/dist/exports/metadata.mjs +1 -0
  21. package/dist/exports/{dag.d.mts → migration-graph.d.mts} +10 -9
  22. package/dist/exports/migration-graph.d.mts.map +1 -0
  23. package/dist/exports/{dag.mjs → migration-graph.mjs} +17 -17
  24. package/dist/exports/migration-graph.mjs.map +1 -0
  25. package/dist/exports/migration-ts.mjs +1 -1
  26. package/dist/exports/migration.d.mts +13 -10
  27. package/dist/exports/migration.d.mts.map +1 -1
  28. package/dist/exports/migration.mjs +20 -21
  29. package/dist/exports/migration.mjs.map +1 -1
  30. package/dist/exports/package.d.mts +2 -0
  31. package/dist/exports/package.mjs +1 -0
  32. package/dist/exports/refs.d.mts +11 -5
  33. package/dist/exports/refs.d.mts.map +1 -1
  34. package/dist/exports/refs.mjs +106 -30
  35. package/dist/exports/refs.mjs.map +1 -1
  36. package/dist/graph-B5wbCSna.d.mts +22 -0
  37. package/dist/graph-B5wbCSna.d.mts.map +1 -0
  38. package/dist/hash-BNWumjn7.mjs +76 -0
  39. package/dist/hash-BNWumjn7.mjs.map +1 -0
  40. package/dist/metadata-DDa5L-uD.d.mts +45 -0
  41. package/dist/metadata-DDa5L-uD.d.mts.map +1 -0
  42. package/dist/package-BJ5KAEcD.d.mts +21 -0
  43. package/dist/package-BJ5KAEcD.d.mts.map +1 -0
  44. package/package.json +26 -14
  45. package/src/errors.ts +57 -15
  46. package/src/exports/errors.ts +1 -0
  47. package/src/exports/graph.ts +1 -0
  48. package/src/exports/hash.ts +2 -0
  49. package/src/exports/io.ts +1 -1
  50. package/src/exports/metadata.ts +1 -0
  51. package/src/exports/{dag.ts → migration-graph.ts} +2 -2
  52. package/src/exports/package.ts +1 -0
  53. package/src/exports/refs.ts +10 -2
  54. package/src/graph.ts +19 -0
  55. package/src/hash.ts +91 -0
  56. package/src/io.ts +32 -20
  57. package/src/metadata.ts +36 -0
  58. package/src/migration-base.ts +32 -28
  59. package/src/{dag.ts → migration-graph.ts} +35 -38
  60. package/src/package.ts +18 -0
  61. package/src/refs.ts +148 -37
  62. package/dist/attestation-DtF8tEOM.mjs +0 -65
  63. package/dist/attestation-DtF8tEOM.mjs.map +0 -1
  64. package/dist/errors-BKbRGCJM.mjs.map +0 -1
  65. package/dist/exports/attestation.d.mts +0 -37
  66. package/dist/exports/attestation.d.mts.map +0 -1
  67. package/dist/exports/attestation.mjs +0 -4
  68. package/dist/exports/dag.d.mts.map +0 -1
  69. package/dist/exports/dag.mjs.map +0 -1
  70. package/dist/exports/types.d.mts.map +0 -1
  71. package/dist/exports/types.mjs +0 -3
  72. package/dist/io-CCnYsUHU.mjs +0 -153
  73. package/dist/io-CCnYsUHU.mjs.map +0 -1
  74. package/dist/types-DyGXcWWp.d.mts +0 -71
  75. package/dist/types-DyGXcWWp.d.mts.map +0 -1
  76. package/src/attestation.ts +0 -81
  77. package/src/exports/attestation.ts +0 -2
  78. package/src/exports/types.ts +0 -10
  79. package/src/types.ts +0 -66
package/src/io.ts CHANGED
@@ -7,9 +7,12 @@ import {
7
7
  errorInvalidJson,
8
8
  errorInvalidManifest,
9
9
  errorInvalidSlug,
10
+ errorMigrationHashMismatch,
10
11
  errorMissingFile,
11
12
  } from './errors';
12
- import type { MigrationBundle, MigrationManifest, MigrationOps } from './types';
13
+ import { verifyMigrationHash } from './hash';
14
+ import type { MigrationMetadata } from './metadata';
15
+ import type { MigrationOps, MigrationPackage } from './package';
13
16
 
14
17
  const MANIFEST_FILE = 'migration.json';
15
18
  const OPS_FILE = 'ops.json';
@@ -25,10 +28,10 @@ const MigrationHintsSchema = type({
25
28
  plannerVersion: 'string',
26
29
  });
27
30
 
28
- const MigrationManifestSchema = type({
31
+ const MigrationMetadataSchema = type({
29
32
  from: 'string',
30
33
  to: 'string',
31
- migrationId: 'string',
34
+ migrationHash: 'string',
32
35
  kind: "'regular' | 'baseline'",
33
36
  fromContract: 'object | null',
34
37
  toContract: 'object',
@@ -56,7 +59,7 @@ const MigrationOpsSchema = MigrationOpSchema.array();
56
59
 
57
60
  export async function writeMigrationPackage(
58
61
  dir: string,
59
- manifest: MigrationManifest,
62
+ metadata: MigrationMetadata,
60
63
  ops: MigrationOps,
61
64
  ): Promise<void> {
62
65
  await mkdir(dirname(dir), { recursive: true });
@@ -70,7 +73,9 @@ export async function writeMigrationPackage(
70
73
  throw error;
71
74
  }
72
75
 
73
- await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(manifest, null, 2), { flag: 'wx' });
76
+ await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(metadata, null, 2), {
77
+ flag: 'wx',
78
+ });
74
79
  await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: 'wx' });
75
80
  }
76
81
 
@@ -98,18 +103,18 @@ export async function copyFilesWithRename(
98
103
  }
99
104
  }
100
105
 
101
- export async function writeMigrationManifest(
106
+ export async function writeMigrationMetadata(
102
107
  dir: string,
103
- manifest: MigrationManifest,
108
+ metadata: MigrationMetadata,
104
109
  ): Promise<void> {
105
- await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`);
110
+ await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(metadata, null, 2)}\n`);
106
111
  }
107
112
 
108
113
  export async function writeMigrationOps(dir: string, ops: MigrationOps): Promise<void> {
109
114
  await writeFile(join(dir, OPS_FILE), `${JSON.stringify(ops, null, 2)}\n`);
110
115
  }
111
116
 
112
- export async function readMigrationPackage(dir: string): Promise<MigrationBundle> {
117
+ export async function readMigrationPackage(dir: string): Promise<MigrationPackage> {
113
118
  const manifestPath = join(dir, MANIFEST_FILE);
114
119
  const opsPath = join(dir, OPS_FILE);
115
120
 
@@ -133,9 +138,9 @@ export async function readMigrationPackage(dir: string): Promise<MigrationBundle
133
138
  throw error;
134
139
  }
135
140
 
136
- let manifest: MigrationManifest;
141
+ let metadata: MigrationMetadata;
137
142
  try {
138
- manifest = JSON.parse(manifestRaw);
143
+ metadata = JSON.parse(manifestRaw);
139
144
  } catch (e) {
140
145
  throw errorInvalidJson(manifestPath, e instanceof Error ? e.message : String(e));
141
146
  }
@@ -147,22 +152,29 @@ export async function readMigrationPackage(dir: string): Promise<MigrationBundle
147
152
  throw errorInvalidJson(opsPath, e instanceof Error ? e.message : String(e));
148
153
  }
149
154
 
150
- validateManifest(manifest, manifestPath);
155
+ validateMetadata(metadata, manifestPath);
151
156
  validateOps(ops, opsPath);
152
157
 
153
- return {
158
+ const pkg: MigrationPackage = {
154
159
  dirName: basename(dir),
155
160
  dirPath: dir,
156
- manifest,
161
+ metadata,
157
162
  ops,
158
163
  };
164
+
165
+ const verification = verifyMigrationHash(pkg);
166
+ if (!verification.ok) {
167
+ throw errorMigrationHashMismatch(dir, verification.storedHash, verification.computedHash);
168
+ }
169
+
170
+ return pkg;
159
171
  }
160
172
 
161
- function validateManifest(
162
- manifest: unknown,
173
+ function validateMetadata(
174
+ metadata: unknown,
163
175
  filePath: string,
164
- ): asserts manifest is MigrationManifest {
165
- const result = MigrationManifestSchema(manifest);
176
+ ): asserts metadata is MigrationMetadata {
177
+ const result = MigrationMetadataSchema(metadata);
166
178
  if (result instanceof type.errors) {
167
179
  throw errorInvalidManifest(filePath, result.summary);
168
180
  }
@@ -177,7 +189,7 @@ function validateOps(ops: unknown, filePath: string): asserts ops is MigrationOp
177
189
 
178
190
  export async function readMigrationsDir(
179
191
  migrationsRoot: string,
180
- ): Promise<readonly MigrationBundle[]> {
192
+ ): Promise<readonly MigrationPackage[]> {
181
193
  let entries: string[];
182
194
  try {
183
195
  entries = await readdir(migrationsRoot);
@@ -188,7 +200,7 @@ export async function readMigrationsDir(
188
200
  throw error;
189
201
  }
190
202
 
191
- const packages: MigrationBundle[] = [];
203
+ const packages: MigrationPackage[] = [];
192
204
 
193
205
  for (const entry of entries.sort()) {
194
206
  const entryPath = join(migrationsRoot, entry);
@@ -0,0 +1,36 @@
1
+ import type { Contract } from '@prisma-next/contract/types';
2
+
3
+ export interface MigrationHints {
4
+ readonly used: readonly string[];
5
+ readonly applied: readonly string[];
6
+ readonly plannerVersion: string;
7
+ }
8
+
9
+ /**
10
+ * In-memory migration metadata envelope. Every migration is content-addressed:
11
+ * the `migrationHash` is a hash over the metadata envelope plus the operations
12
+ * list, computed at write time. There is no draft state — a migration
13
+ * directory either exists with fully attested metadata or it does not.
14
+ *
15
+ * When the planner cannot lower an operation because of an unfilled
16
+ * `placeholder(...)` slot, the migration is still written with `migrationHash`
17
+ * hashed over `ops: []`. Re-running self-emit after the user fills the
18
+ * placeholder produces a *different* `migrationHash` (committed to the real
19
+ * ops); this is intentional.
20
+ *
21
+ * The on-disk JSON shape in `migration.json` matches this type field-for-field
22
+ * — `JSON.stringify(metadata, null, 2)` is the canonical writer output.
23
+ */
24
+ export interface MigrationMetadata {
25
+ readonly migrationHash: string;
26
+ readonly from: string;
27
+ readonly to: string;
28
+ readonly kind: 'regular' | 'baseline';
29
+ readonly fromContract: Contract | null;
30
+ readonly toContract: Contract;
31
+ readonly hints: MigrationHints;
32
+ readonly labels: readonly string[];
33
+ readonly authorship?: { readonly author?: string; readonly email?: string };
34
+ readonly signature?: { readonly keyId: string; readonly value: string } | null;
35
+ readonly createdAt: string;
36
+ }
@@ -8,8 +8,9 @@ import type {
8
8
  } from '@prisma-next/framework-components/control';
9
9
  import { ifDefined } from '@prisma-next/utils/defined';
10
10
  import { type } from 'arktype';
11
- import { computeMigrationId } from './attestation';
12
- import type { MigrationHints, MigrationManifest, MigrationOps } from './types';
11
+ import { computeMigrationHash } from './hash';
12
+ import type { MigrationHints, MigrationMetadata } from './metadata';
13
+ import type { MigrationOps } from './package';
13
14
 
14
15
  export interface MigrationMeta {
15
16
  readonly from: string;
@@ -30,7 +31,7 @@ const MigrationMetaSchema = type({
30
31
  *
31
32
  * A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the
32
33
  * runner can consume it directly via `targetId`, `operations`, `origin`, and
33
- * `destination`. The manifest-shaped inputs come from `describe()`, which
34
+ * `destination`. The metadata-shaped inputs come from `describe()`, which
34
35
  * every migration must implement — `migration.json` is required for a
35
36
  * migration to be valid.
36
37
  */
@@ -123,64 +124,67 @@ function printHelp(): void {
123
124
 
124
125
  /**
125
126
  * In-memory artifacts produced from a `Migration` instance: the
126
- * serialized `ops.json` body, the `migration.json` manifest object, and
127
+ * serialized `ops.json` body, the `migration.json` metadata object, and
127
128
  * its serialized form. Returned by `buildMigrationArtifacts` so callers
128
129
  * (today: `MigrationCLI.run` in `@prisma-next/cli/migration-cli`) can
129
130
  * decide how to persist them — write to disk, print in dry-run, ship
130
131
  * over the wire — without coupling artifact construction to file I/O.
132
+ *
133
+ * `metadataJson` is `JSON.stringify(metadata, null, 2)` — the canonical
134
+ * on-disk shape that the arktype loader-schema in `./io` validates.
131
135
  */
132
136
  export interface MigrationArtifacts {
133
137
  readonly opsJson: string;
134
- readonly manifest: MigrationManifest;
135
- readonly manifestJson: string;
138
+ readonly metadata: MigrationMetadata;
139
+ readonly metadataJson: string;
136
140
  }
137
141
 
138
142
  /**
139
- * Build the attested manifest from `describe()`-derived metadata, the
140
- * operations list, and the previously-scaffolded manifest (if any).
143
+ * Build the attested metadata from `describe()`-derived metadata, the
144
+ * operations list, and the previously-scaffolded metadata (if any).
141
145
  *
142
146
  * When a `migration.json` already exists for this package (the common
143
147
  * case: it was scaffolded by `migration plan`), preserve the contract
144
148
  * bookends, hints, labels, and `createdAt` set there — those fields are
145
149
  * owned by the CLI scaffolder, not the authored class. Only the
146
150
  * `describe()`-derived fields (`from`, `to`, `kind`) and the operations
147
- * change as the author iterates. When no manifest exists yet (a bare
151
+ * change as the author iterates. When no metadata exists yet (a bare
148
152
  * `migration.ts` run from scratch), synthesize a minimal but
149
- * schema-conformant manifest so the resulting package can still be read,
153
+ * schema-conformant record so the resulting package can still be read,
150
154
  * verified, and applied.
151
155
  *
152
- * The `migrationId` is recomputed against the current manifest + ops so
156
+ * The `migrationHash` is recomputed against the current metadata + ops so
153
157
  * the on-disk artifacts are always fully attested.
154
158
  */
155
- function buildAttestedManifest(
159
+ function buildAttestedMetadata(
156
160
  meta: MigrationMeta,
157
161
  ops: MigrationOps,
158
- existing: Partial<MigrationManifest> | null,
159
- ): MigrationManifest {
160
- const baseManifest: Omit<MigrationManifest, 'migrationId'> = {
162
+ existing: Partial<MigrationMetadata> | null,
163
+ ): MigrationMetadata {
164
+ const baseMetadata: Omit<MigrationMetadata, 'migrationHash'> = {
161
165
  from: meta.from,
162
166
  to: meta.to,
163
167
  kind: meta.kind ?? 'regular',
164
168
  labels: meta.labels ?? existing?.labels ?? [],
165
169
  createdAt: existing?.createdAt ?? new Date().toISOString(),
166
170
  fromContract: existing?.fromContract ?? null,
167
- // When no scaffolded manifest exists we synthesize a minimal contract
171
+ // When no scaffolded metadata exists we synthesize a minimal contract
168
172
  // stub so the package is still readable end-to-end. The cast is
169
173
  // intentional: only the storage bookend matters for hash computation
170
- // (everything else is stripped by `computeMigrationId`), and a real
174
+ // (everything else is stripped by `computeMigrationHash`), and a real
171
175
  // contract bookend would only be available after `migration plan`.
172
176
  toContract: existing?.toContract ?? ({ storage: { storageHash: meta.to } } as Contract),
173
177
  hints: normalizeHints(existing?.hints),
174
178
  ...ifDefined('authorship', existing?.authorship),
175
179
  };
176
180
 
177
- const migrationId = computeMigrationId(baseManifest, ops);
178
- return { ...baseManifest, migrationId };
181
+ const migrationHash = computeMigrationHash(baseMetadata, ops);
182
+ return { ...baseMetadata, migrationHash };
179
183
  }
180
184
 
181
185
  /**
182
186
  * Project `existing.hints` down to the known `MigrationHints` shape, dropping
183
- * any legacy keys that may linger in manifests scaffolded by older CLI
187
+ * any legacy keys that may linger in metadata scaffolded by older CLI
184
188
  * versions (e.g. `planningStrategy`). Picking fields explicitly instead of
185
189
  * spreading keeps refreshed `migration.json` files schema-clean regardless
186
190
  * of what was on disk before.
@@ -195,16 +199,16 @@ function normalizeHints(existing: MigrationHints | undefined): MigrationHints {
195
199
 
196
200
  /**
197
201
  * Pure conversion from a `Migration` instance (plus the previously
198
- * scaffolded manifest, when one exists on disk) to the in-memory
202
+ * scaffolded metadata, when one exists on disk) to the in-memory
199
203
  * artifacts that downstream tooling persists. Owns metadata validation,
200
- * manifest synthesis/preservation, hint normalization, and the
201
- * content-addressed `migrationId` computation, but performs no file I/O
204
+ * metadata synthesis/preservation, hint normalization, and the
205
+ * content-addressed `migrationHash` computation, but performs no file I/O
202
206
  * — callers handle reads (to source `existing`) and writes (to persist
203
- * `opsJson` / `manifestJson`).
207
+ * `opsJson` / `metadataJson`).
204
208
  */
205
209
  export function buildMigrationArtifacts(
206
210
  instance: Migration,
207
- existing: Partial<MigrationManifest> | null,
211
+ existing: Partial<MigrationMetadata> | null,
208
212
  ): MigrationArtifacts {
209
213
  const ops = instance.operations;
210
214
  if (!Array.isArray(ops)) {
@@ -217,11 +221,11 @@ export function buildMigrationArtifacts(
217
221
  throw new Error(`describe() returned invalid metadata: ${parsed.summary}`);
218
222
  }
219
223
 
220
- const manifest = buildAttestedManifest(parsed, ops, existing);
224
+ const metadata = buildAttestedMetadata(parsed, ops, existing);
221
225
 
222
226
  return {
223
227
  opsJson: JSON.stringify(ops, null, 2),
224
- manifest,
225
- manifestJson: JSON.stringify(manifest, null, 2),
228
+ metadata,
229
+ metadataJson: JSON.stringify(metadata, null, 2),
226
230
  };
227
231
  }
@@ -2,13 +2,14 @@ import { ifDefined } from '@prisma-next/utils/defined';
2
2
  import { EMPTY_CONTRACT_HASH } from './constants';
3
3
  import {
4
4
  errorAmbiguousTarget,
5
- errorDuplicateMigrationId,
5
+ errorDuplicateMigrationHash,
6
6
  errorNoInitialMigration,
7
7
  errorNoTarget,
8
8
  errorSameSourceAndTarget,
9
9
  } from './errors';
10
+ import type { MigrationEdge, MigrationGraph } from './graph';
10
11
  import { bfs } from './graph-ops';
11
- import type { MigrationBundle, MigrationChainEntry, MigrationGraph } from './types';
12
+ import type { MigrationPackage } from './package';
12
13
 
13
14
  /** Forward-edge neighbours for BFS: edge `e` from `n` visits `e.to` next. */
14
15
  function forwardNeighbours(graph: MigrationGraph, node: string) {
@@ -20,57 +21,53 @@ function reverseNeighbours(graph: MigrationGraph, node: string) {
20
21
  return (graph.reverseChain.get(node) ?? []).map((edge) => ({ next: edge.from, edge }));
21
22
  }
22
23
 
23
- function appendEdge(
24
- map: Map<string, MigrationChainEntry[]>,
25
- key: string,
26
- entry: MigrationChainEntry,
27
- ): void {
24
+ function appendEdge(map: Map<string, MigrationEdge[]>, key: string, entry: MigrationEdge): void {
28
25
  const bucket = map.get(key);
29
26
  if (bucket) bucket.push(entry);
30
27
  else map.set(key, [entry]);
31
28
  }
32
29
 
33
- export function reconstructGraph(packages: readonly MigrationBundle[]): MigrationGraph {
30
+ export function reconstructGraph(packages: readonly MigrationPackage[]): MigrationGraph {
34
31
  const nodes = new Set<string>();
35
- const forwardChain = new Map<string, MigrationChainEntry[]>();
36
- const reverseChain = new Map<string, MigrationChainEntry[]>();
37
- const migrationById = new Map<string, MigrationChainEntry>();
32
+ const forwardChain = new Map<string, MigrationEdge[]>();
33
+ const reverseChain = new Map<string, MigrationEdge[]>();
34
+ const migrationByHash = new Map<string, MigrationEdge>();
38
35
 
39
36
  for (const pkg of packages) {
40
- const { from, to } = pkg.manifest;
37
+ const { from, to } = pkg.metadata;
41
38
 
42
39
  if (from === to) {
43
- throw errorSameSourceAndTarget(pkg.dirName, from);
40
+ throw errorSameSourceAndTarget(pkg.dirPath, from);
44
41
  }
45
42
 
46
43
  nodes.add(from);
47
44
  nodes.add(to);
48
45
 
49
- const migration: MigrationChainEntry = {
46
+ const migration: MigrationEdge = {
50
47
  from,
51
48
  to,
52
- migrationId: pkg.manifest.migrationId,
49
+ migrationHash: pkg.metadata.migrationHash,
53
50
  dirName: pkg.dirName,
54
- createdAt: pkg.manifest.createdAt,
55
- labels: pkg.manifest.labels,
51
+ createdAt: pkg.metadata.createdAt,
52
+ labels: pkg.metadata.labels,
56
53
  };
57
54
 
58
- if (migrationById.has(migration.migrationId)) {
59
- throw errorDuplicateMigrationId(migration.migrationId);
55
+ if (migrationByHash.has(migration.migrationHash)) {
56
+ throw errorDuplicateMigrationHash(migration.migrationHash);
60
57
  }
61
- migrationById.set(migration.migrationId, migration);
58
+ migrationByHash.set(migration.migrationHash, migration);
62
59
 
63
60
  appendEdge(forwardChain, from, migration);
64
61
  appendEdge(reverseChain, to, migration);
65
62
  }
66
63
 
67
- return { nodes, forwardChain, reverseChain, migrationById };
64
+ return { nodes, forwardChain, reverseChain, migrationByHash };
68
65
  }
69
66
 
70
67
  // ---------------------------------------------------------------------------
71
68
  // Deterministic tie-breaking for BFS neighbour order.
72
69
  // Used by `findPath` and `findPathWithDecision` only; not a general-purpose
73
- // utility. Ordering: label priority → createdAt → to → migrationId.
70
+ // utility. Ordering: label priority → createdAt → to → migrationHash.
74
71
  // ---------------------------------------------------------------------------
75
72
 
76
73
  const LABEL_PRIORITY: Record<string, number> = { main: 0, default: 1, feature: 2 };
@@ -84,24 +81,24 @@ function labelPriority(labels: readonly string[]): number {
84
81
  return best;
85
82
  }
86
83
 
87
- function compareTieBreak(a: MigrationChainEntry, b: MigrationChainEntry): number {
84
+ function compareTieBreak(a: MigrationEdge, b: MigrationEdge): number {
88
85
  const lp = labelPriority(a.labels) - labelPriority(b.labels);
89
86
  if (lp !== 0) return lp;
90
87
  const ca = a.createdAt.localeCompare(b.createdAt);
91
88
  if (ca !== 0) return ca;
92
89
  const tc = a.to.localeCompare(b.to);
93
90
  if (tc !== 0) return tc;
94
- return a.migrationId.localeCompare(b.migrationId);
91
+ return a.migrationHash.localeCompare(b.migrationHash);
95
92
  }
96
93
 
97
- function sortedNeighbors(edges: readonly MigrationChainEntry[]): readonly MigrationChainEntry[] {
94
+ function sortedNeighbors(edges: readonly MigrationEdge[]): readonly MigrationEdge[] {
98
95
  return [...edges].sort(compareTieBreak);
99
96
  }
100
97
 
101
98
  /** Ordering adapter for `bfs` — sorts `{next, edge}` pairs by tie-break. */
102
99
  function bfsOrdering(
103
- items: readonly { next: string; edge: MigrationChainEntry }[],
104
- ): readonly { next: string; edge: MigrationChainEntry }[] {
100
+ items: readonly { next: string; edge: MigrationEdge }[],
101
+ ): readonly { next: string; edge: MigrationEdge }[] {
105
102
  return items.slice().sort((a, b) => compareTieBreak(a.edge, b.edge));
106
103
  }
107
104
 
@@ -111,22 +108,22 @@ function bfsOrdering(
111
108
  * exists. Returns an empty array when `fromHash === toHash` (no-op).
112
109
  *
113
110
  * Neighbor ordering is deterministic via the tie-break sort key:
114
- * label priority → createdAt → to → migrationId.
111
+ * label priority → createdAt → to → migrationHash.
115
112
  */
116
113
  export function findPath(
117
114
  graph: MigrationGraph,
118
115
  fromHash: string,
119
116
  toHash: string,
120
- ): readonly MigrationChainEntry[] | null {
117
+ ): readonly MigrationEdge[] | null {
121
118
  if (fromHash === toHash) return [];
122
119
 
123
- const parents = new Map<string, { parent: string; edge: MigrationChainEntry }>();
120
+ const parents = new Map<string, { parent: string; edge: MigrationEdge }>();
124
121
  for (const step of bfs([fromHash], (n) => forwardNeighbours(graph, n), bfsOrdering)) {
125
122
  if (step.parent !== null && step.incomingEdge !== null) {
126
123
  parents.set(step.node, { parent: step.parent, edge: step.incomingEdge });
127
124
  }
128
125
  if (step.node === toHash) {
129
- const path: MigrationChainEntry[] = [];
126
+ const path: MigrationEdge[] = [];
130
127
  let cur = toHash;
131
128
  let p = parents.get(cur);
132
129
  while (p) {
@@ -155,7 +152,7 @@ function collectNodesReachingTarget(graph: MigrationGraph, toHash: string): Set<
155
152
  }
156
153
 
157
154
  export interface PathDecision {
158
- readonly selectedPath: readonly MigrationChainEntry[];
155
+ readonly selectedPath: readonly MigrationEdge[];
159
156
  readonly fromHash: string;
160
157
  readonly toHash: string;
161
158
  readonly alternativeCount: number;
@@ -202,8 +199,8 @@ export function findPathWithDecision(
202
199
  if (reachable.length > 1) {
203
200
  alternativeCount += reachable.length - 1;
204
201
  const sorted = sortedNeighbors(reachable);
205
- if (sorted[0] && sorted[0].migrationId === edge.migrationId) {
206
- if (reachable.some((e) => e.migrationId !== edge.migrationId)) {
202
+ if (sorted[0] && sorted[0].migrationHash === edge.migrationHash) {
203
+ if (reachable.some((e) => e.migrationHash !== edge.migrationHash)) {
207
204
  tieBreakReasons.push(
208
205
  `at ${edge.from}: ${reachable.length} candidates, selected by tie-break`,
209
206
  );
@@ -319,7 +316,7 @@ export function findLeaf(graph: MigrationGraph): string | null {
319
316
  * to the single target. Returns null for an empty graph.
320
317
  * Throws AMBIGUOUS_TARGET if the graph has multiple branch tips.
321
318
  */
322
- export function findLatestMigration(graph: MigrationGraph): MigrationChainEntry | null {
319
+ export function findLatestMigration(graph: MigrationGraph): MigrationEdge | null {
323
320
  const leafHash = findLeaf(graph);
324
321
  if (leafHash === null) return null;
325
322
 
@@ -343,7 +340,7 @@ export function detectCycles(graph: MigrationGraph): readonly string[][] {
343
340
  // Iterative three-color DFS. A frame is (node, outgoing edges, next-index).
344
341
  interface Frame {
345
342
  node: string;
346
- outgoing: readonly MigrationChainEntry[];
343
+ outgoing: readonly MigrationEdge[];
347
344
  index: number;
348
345
  }
349
346
  const stack: Frame[] = [];
@@ -389,7 +386,7 @@ export function detectCycles(graph: MigrationGraph): readonly string[][] {
389
386
  return cycles;
390
387
  }
391
388
 
392
- export function detectOrphans(graph: MigrationGraph): readonly MigrationChainEntry[] {
389
+ export function detectOrphans(graph: MigrationGraph): readonly MigrationEdge[] {
393
390
  if (graph.nodes.size === 0) return [];
394
391
 
395
392
  const reachable = new Set<string>();
@@ -415,7 +412,7 @@ export function detectOrphans(graph: MigrationGraph): readonly MigrationChainEnt
415
412
  reachable.add(step.node);
416
413
  }
417
414
 
418
- const orphans: MigrationChainEntry[] = [];
415
+ const orphans: MigrationEdge[] = [];
419
416
  for (const [from, migrations] of graph.forwardChain) {
420
417
  if (!reachable.has(from)) {
421
418
  orphans.push(...migrations);
package/src/package.ts ADDED
@@ -0,0 +1,18 @@
1
+ import type { MigrationPlanOperation } from '@prisma-next/framework-components/control';
2
+ import type { MigrationMetadata } from './metadata';
3
+
4
+ export type MigrationOps = readonly MigrationPlanOperation[];
5
+
6
+ /**
7
+ * An on-disk migration directory (a "package") with its parsed metadata and
8
+ * operations. Returned from `readMigrationPackage` / `readMigrationsDir` only
9
+ * after the loader has verified the package's integrity (hash recomputation
10
+ * against the stored `migrationHash`); holding a `MigrationPackage` value
11
+ * therefore implies the package is internally consistent.
12
+ */
13
+ export interface MigrationPackage {
14
+ readonly dirName: string;
15
+ readonly dirPath: string;
16
+ readonly metadata: MigrationMetadata;
17
+ readonly ops: MigrationOps;
18
+ }