@prisma-next/migration-tools 0.5.0-dev.6 → 0.5.0-dev.60

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 (91) hide show
  1. package/README.md +34 -22
  2. package/dist/{constants-BRi0X7B_.mjs → constants-BQEHsaEx.mjs} +1 -1
  3. package/dist/{constants-BRi0X7B_.mjs.map → constants-BQEHsaEx.mjs.map} +1 -1
  4. package/dist/errors-CfmjBeK0.mjs +272 -0
  5. package/dist/errors-CfmjBeK0.mjs.map +1 -0
  6. package/dist/exports/constants.mjs +1 -1
  7. package/dist/exports/errors.d.mts +63 -0
  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/invariants.d.mts +24 -0
  16. package/dist/exports/invariants.d.mts.map +1 -0
  17. package/dist/exports/invariants.mjs +4 -0
  18. package/dist/exports/io.d.mts +7 -6
  19. package/dist/exports/io.d.mts.map +1 -1
  20. package/dist/exports/io.mjs +162 -2
  21. package/dist/exports/io.mjs.map +1 -0
  22. package/dist/exports/metadata.d.mts +2 -0
  23. package/dist/exports/metadata.mjs +1 -0
  24. package/dist/exports/migration-graph.d.mts +124 -0
  25. package/dist/exports/migration-graph.d.mts.map +1 -0
  26. package/dist/exports/migration-graph.mjs +526 -0
  27. package/dist/exports/migration-graph.mjs.map +1 -0
  28. package/dist/exports/migration-ts.mjs +1 -1
  29. package/dist/exports/migration.d.mts +15 -14
  30. package/dist/exports/migration.d.mts.map +1 -1
  31. package/dist/exports/migration.mjs +69 -41
  32. package/dist/exports/migration.mjs.map +1 -1
  33. package/dist/exports/package.d.mts +2 -0
  34. package/dist/exports/package.mjs +1 -0
  35. package/dist/exports/refs.mjs +2 -2
  36. package/dist/graph-BHPv-9Gl.d.mts +28 -0
  37. package/dist/graph-BHPv-9Gl.d.mts.map +1 -0
  38. package/dist/hash-BARZdVgW.mjs +76 -0
  39. package/dist/hash-BARZdVgW.mjs.map +1 -0
  40. package/dist/invariants-30VA65sB.mjs +42 -0
  41. package/dist/invariants-30VA65sB.mjs.map +1 -0
  42. package/dist/metadata-BP1cmU7Z.d.mts +50 -0
  43. package/dist/metadata-BP1cmU7Z.d.mts.map +1 -0
  44. package/dist/op-schema-DZKFua46.mjs +14 -0
  45. package/dist/op-schema-DZKFua46.mjs.map +1 -0
  46. package/dist/package-5HCCg0z-.d.mts +21 -0
  47. package/dist/package-5HCCg0z-.d.mts.map +1 -0
  48. package/package.json +31 -14
  49. package/src/errors.ts +210 -17
  50. package/src/exports/errors.ts +7 -0
  51. package/src/exports/graph.ts +1 -0
  52. package/src/exports/hash.ts +2 -0
  53. package/src/exports/invariants.ts +1 -0
  54. package/src/exports/io.ts +1 -1
  55. package/src/exports/metadata.ts +1 -0
  56. package/src/exports/{dag.ts → migration-graph.ts} +3 -2
  57. package/src/exports/migration.ts +0 -1
  58. package/src/exports/package.ts +1 -0
  59. package/src/graph-ops.ts +57 -30
  60. package/src/graph.ts +25 -0
  61. package/src/hash.ts +91 -0
  62. package/src/invariants.ts +45 -0
  63. package/src/io.ts +57 -31
  64. package/src/metadata.ts +41 -0
  65. package/src/migration-base.ts +97 -56
  66. package/src/migration-graph.ts +676 -0
  67. package/src/op-schema.ts +11 -0
  68. package/src/package.ts +18 -0
  69. package/dist/attestation-BnzTb0Qp.mjs +0 -65
  70. package/dist/attestation-BnzTb0Qp.mjs.map +0 -1
  71. package/dist/errors-BmiSgz1j.mjs +0 -160
  72. package/dist/errors-BmiSgz1j.mjs.map +0 -1
  73. package/dist/exports/attestation.d.mts +0 -37
  74. package/dist/exports/attestation.d.mts.map +0 -1
  75. package/dist/exports/attestation.mjs +0 -4
  76. package/dist/exports/dag.d.mts +0 -51
  77. package/dist/exports/dag.d.mts.map +0 -1
  78. package/dist/exports/dag.mjs +0 -386
  79. package/dist/exports/dag.mjs.map +0 -1
  80. package/dist/exports/types.d.mts +0 -35
  81. package/dist/exports/types.d.mts.map +0 -1
  82. package/dist/exports/types.mjs +0 -3
  83. package/dist/io-Cd6GLyjK.mjs +0 -153
  84. package/dist/io-Cd6GLyjK.mjs.map +0 -1
  85. package/dist/types-DyGXcWWp.d.mts +0 -71
  86. package/dist/types-DyGXcWWp.d.mts.map +0 -1
  87. package/src/attestation.ts +0 -81
  88. package/src/dag.ts +0 -426
  89. package/src/exports/attestation.ts +0 -2
  90. package/src/exports/types.ts +0 -10
  91. package/src/types.ts +0 -66
@@ -1,11 +1,10 @@
1
- import { a as MigrationManifest } from "../types-DyGXcWWp.mjs";
1
+ import { n as MigrationMetadata } from "../metadata-BP1cmU7Z.mjs";
2
2
  import { ControlStack, MigrationPlan, MigrationPlanOperation } from "@prisma-next/framework-components/control";
3
3
 
4
4
  //#region src/migration-base.d.ts
5
5
  interface MigrationMeta {
6
- readonly from: string;
6
+ readonly from: string | null;
7
7
  readonly to: string;
8
- readonly kind?: 'regular' | 'baseline';
9
8
  readonly labels?: readonly string[];
10
9
  }
11
10
  /**
@@ -13,7 +12,7 @@ interface MigrationMeta {
13
12
  *
14
13
  * A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the
15
14
  * runner can consume it directly via `targetId`, `operations`, `origin`, and
16
- * `destination`. The manifest-shaped inputs come from `describe()`, which
15
+ * `destination`. The metadata-shaped inputs come from `describe()`, which
17
16
  * every migration must implement — `migration.json` is required for a
18
17
  * migration to be valid.
19
18
  */
@@ -58,30 +57,32 @@ declare abstract class Migration<TOperation extends MigrationPlanOperation = Mig
58
57
  * than executed directly.
59
58
  */
60
59
  declare function isDirectEntrypoint(importMetaUrl: string): boolean;
61
- declare function printMigrationHelp(): void;
62
60
  /**
63
61
  * In-memory artifacts produced from a `Migration` instance: the
64
- * serialized `ops.json` body, the `migration.json` manifest object, and
62
+ * serialized `ops.json` body, the `migration.json` metadata object, and
65
63
  * its serialized form. Returned by `buildMigrationArtifacts` so callers
66
64
  * (today: `MigrationCLI.run` in `@prisma-next/cli/migration-cli`) can
67
65
  * decide how to persist them — write to disk, print in dry-run, ship
68
66
  * over the wire — without coupling artifact construction to file I/O.
67
+ *
68
+ * `metadataJson` is `JSON.stringify(metadata, null, 2)` — the canonical
69
+ * on-disk shape that the arktype loader-schema in `./io` validates.
69
70
  */
70
71
  interface MigrationArtifacts {
71
72
  readonly opsJson: string;
72
- readonly manifest: MigrationManifest;
73
- readonly manifestJson: string;
73
+ readonly metadata: MigrationMetadata;
74
+ readonly metadataJson: string;
74
75
  }
75
76
  /**
76
77
  * Pure conversion from a `Migration` instance (plus the previously
77
- * scaffolded manifest, when one exists on disk) to the in-memory
78
+ * scaffolded metadata, when one exists on disk) to the in-memory
78
79
  * artifacts that downstream tooling persists. Owns metadata validation,
79
- * manifest synthesis/preservation, hint normalization, and the
80
- * content-addressed `migrationId` computation, but performs no file I/O
80
+ * metadata synthesis/preservation, hint normalization, and the
81
+ * content-addressed `migrationHash` computation, but performs no file I/O
81
82
  * — callers handle reads (to source `existing`) and writes (to persist
82
- * `opsJson` / `manifestJson`).
83
+ * `opsJson` / `metadataJson`).
83
84
  */
84
- declare function buildMigrationArtifacts(instance: Migration, existing: Partial<MigrationManifest> | null): MigrationArtifacts;
85
+ declare function buildMigrationArtifacts(instance: Migration, existing: Partial<MigrationMetadata> | null): MigrationArtifacts;
85
86
  //#endregion
86
- export { Migration, type MigrationArtifacts, type MigrationMeta, buildMigrationArtifacts, isDirectEntrypoint, printMigrationHelp };
87
+ export { Migration, type MigrationArtifacts, type MigrationMeta, buildMigrationArtifacts, isDirectEntrypoint };
87
88
  //# sourceMappingURL=migration.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"migration.d.mts","names":[],"sources":["../../src/migration-base.ts"],"sourcesContent":[],"mappings":";;;;UAaiB,aAAA;;EAAA,SAAA,EAAA,EAAA,MAAa;EAuBR,SAAA,IAAS,CAAA,EAAA,SAAA,GAAA,UAAA;EACV,SAAA,MAAA,CAAA,EAAA,SAAA,MAAA,EAAA;;;;;;;;;;;AAGK,uBAJJ,SAII,CAAA,mBAHL,sBAGK,GAHoB,sBAGpB,EAAA,kBAAA,MAAA,GAAA,MAAA,EAAA,kBAAA,MAAA,GAAA,MAAA,CAAA,YAAb,aAAa,CAAA;EAuDV,kBAAA,QAAkB,EAAA,MAAA;EAWlB;AAyBhB;AAyEA;;;;;;;4BAvJ4B,aAAa,WAAW;sBAE9B,aAAa,WAAW;;;;;;;sCAUR;;;;;;uBAOf;;;;;;;;;;;;;;;iBAuBP,kBAAA;iBAWA,kBAAA,CAAA;;;;;;;;;UAyBC,kBAAA;;qBAEI;;;;;;;;;;;;iBAuEL,uBAAA,WACJ,qBACA,QAAQ,4BACjB"}
1
+ {"version":3,"file":"migration.d.mts","names":[],"sources":["../../src/migration-base.ts"],"sourcesContent":[],"mappings":";;;;UAiBiB,aAAA;;EAAA,SAAA,EAAA,EAAA,MAAa;EA0BR,SAAA,MAAS,CAAA,EAAA,SAAA,MAAA,EAAA;;;;;;;;;;;AAIlB,uBAJS,SAIT,CAAA,mBAHQ,sBAGR,GAHiC,sBAGjC,EAAA,kBAAA,MAAA,GAAA,MAAA,EAAA,kBAAA,MAAA,GAAA,MAAA,CAAA,YAAA,aAAA,CAAA;EAAa,kBAAA,QAAA,EAAA,MAAA;EAmDV;AAsBhB;AAsHA;;;;;;;4BAlL4B,aAAa,WAAW;sBAE9B,aAAa,WAAW;;;;;;;sCAUR;;;;;;uBAOf;;;;;;;;;;;;;;;iBAmBP,kBAAA;;;;;;;;;;;;UAsBC,kBAAA;;qBAEI;;;;;;;;;;;;iBAoHL,uBAAA,WACJ,qBACA,QAAQ,4BACjB"}
@@ -1,15 +1,16 @@
1
- import "../io-Cd6GLyjK.mjs";
2
- import { t as computeMigrationId } from "../attestation-BnzTb0Qp.mjs";
3
- import { type } from "arktype";
1
+ import { S as errorStaleContractBookends, u as errorInvalidOperationEntry } from "../errors-CfmjBeK0.mjs";
2
+ import { t as computeMigrationHash } from "../hash-BARZdVgW.mjs";
3
+ import { t as deriveProvidedInvariants } from "../invariants-30VA65sB.mjs";
4
+ import { t as MigrationOpSchema } from "../op-schema-DZKFua46.mjs";
4
5
  import { ifDefined } from "@prisma-next/utils/defined";
6
+ import { type } from "arktype";
5
7
  import { realpathSync } from "node:fs";
6
8
  import { fileURLToPath } from "node:url";
7
9
 
8
10
  //#region src/migration-base.ts
9
11
  const MigrationMetaSchema = type({
10
- from: "string",
12
+ from: "string > 0 | null",
11
13
  to: "string",
12
- "kind?": "'regular' | 'baseline'",
13
14
  "labels?": type("string").array()
14
15
  });
15
16
  /**
@@ -17,7 +18,7 @@ const MigrationMetaSchema = type({
17
18
  *
18
19
  * A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the
19
20
  * runner can consume it directly via `targetId`, `operations`, `origin`, and
20
- * `destination`. The manifest-shaped inputs come from `describe()`, which
21
+ * `destination`. The metadata-shaped inputs come from `describe()`, which
21
22
  * every migration must implement — `migration.json` is required for a
22
23
  * migration to be valid.
23
24
  */
@@ -37,7 +38,7 @@ var Migration = class {
37
38
  }
38
39
  get origin() {
39
40
  const from = this.describe().from;
40
- return from === "" ? null : { storageHash: from };
41
+ return from === null ? null : { storageHash: from };
41
42
  }
42
43
  get destination() {
43
44
  return { storageHash: this.describe().to };
@@ -60,57 +61,80 @@ function isDirectEntrypoint(importMetaUrl) {
60
61
  return false;
61
62
  }
62
63
  }
63
- function printMigrationHelp() {
64
- printHelp();
65
- }
66
- function printHelp() {
67
- process.stdout.write([
68
- "Usage: node <migration-file> [options]",
69
- "",
70
- "Options:",
71
- " --dry-run Print operations to stdout without writing files",
72
- " --help Show this help message",
73
- ""
74
- ].join("\n"));
75
- }
76
64
  /**
77
- * Build the attested manifest from `describe()`-derived metadata, the
78
- * operations list, and the previously-scaffolded manifest (if any).
65
+ * Build the attested metadata from `describe()`-derived metadata, the
66
+ * operations list, and the previously-scaffolded metadata (if any).
79
67
  *
80
68
  * When a `migration.json` already exists for this package (the common
81
69
  * case: it was scaffolded by `migration plan`), preserve the contract
82
70
  * bookends, hints, labels, and `createdAt` set there — those fields are
83
71
  * owned by the CLI scaffolder, not the authored class. Only the
84
- * `describe()`-derived fields (`from`, `to`, `kind`) and the operations
85
- * change as the author iterates. When no manifest exists yet (a bare
72
+ * `describe()`-derived fields (`from`, `to`) and the operations
73
+ * change as the author iterates. When no metadata exists yet (a bare
86
74
  * `migration.ts` run from scratch), synthesize a minimal but
87
- * schema-conformant manifest so the resulting package can still be read,
75
+ * schema-conformant record so the resulting package can still be read,
88
76
  * verified, and applied.
89
77
  *
90
- * The `migrationId` is recomputed against the current manifest + ops so
78
+ * The `migrationHash` is recomputed against the current metadata + ops so
91
79
  * the on-disk artifacts are always fully attested.
92
80
  */
93
- function buildAttestedManifest(meta, ops, existing) {
94
- const baseManifest = {
81
+ function buildAttestedMetadata(meta, ops, existing) {
82
+ assertBookendsMatchMeta(meta, existing);
83
+ const baseMetadata = {
95
84
  from: meta.from,
96
85
  to: meta.to,
97
- kind: meta.kind ?? "regular",
98
86
  labels: meta.labels ?? existing?.labels ?? [],
87
+ providedInvariants: deriveProvidedInvariants(ops),
99
88
  createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
100
89
  fromContract: existing?.fromContract ?? null,
101
90
  toContract: existing?.toContract ?? { storage: { storageHash: meta.to } },
102
91
  hints: normalizeHints(existing?.hints),
103
92
  ...ifDefined("authorship", existing?.authorship)
104
93
  };
105
- const migrationId = computeMigrationId(baseManifest, ops);
94
+ const migrationHash = computeMigrationHash(baseMetadata, ops);
106
95
  return {
107
- ...baseManifest,
108
- migrationId
96
+ ...baseMetadata,
97
+ migrationHash
109
98
  };
110
99
  }
111
100
  /**
101
+ * Verify each preserved contract bookend in `existing` agrees with the
102
+ * corresponding side of `describe()`'s output. A mismatch indicates the
103
+ * migration's `describe()` was edited after `migration plan` scaffolded
104
+ * the package, leaving a self-inconsistent manifest. Failing fast at
105
+ * write-time turns a silent foot-gun into an actionable diagnostic.
106
+ *
107
+ * Skipped when a side's `existing.<side>Contract` is null/absent (the
108
+ * synthesis path stays open for origin-less initial migrations and for
109
+ * bare `migration.ts` runs from scratch). When a bookend is *present*
110
+ * but its `storage.storageHash` is missing, that's treated as a
111
+ * mismatch — a malformed bookend is not equivalent to "no bookend".
112
+ *
113
+ * This check is paired with TML-2274, which removes `fromContract` /
114
+ * `toContract` from the manifest entirely; once that lands, this
115
+ * function and its error code are deleted.
116
+ */
117
+ function assertBookendsMatchMeta(meta, existing) {
118
+ if (existing?.fromContract != null) {
119
+ const contractHash = existing.fromContract.storage?.storageHash ?? "";
120
+ if (contractHash !== meta.from) throw errorStaleContractBookends({
121
+ side: "from",
122
+ metaHash: meta.from,
123
+ contractHash
124
+ });
125
+ }
126
+ if (existing?.toContract != null) {
127
+ const contractHash = existing.toContract.storage?.storageHash ?? "";
128
+ if (contractHash !== meta.to) throw errorStaleContractBookends({
129
+ side: "to",
130
+ metaHash: meta.to,
131
+ contractHash
132
+ });
133
+ }
134
+ }
135
+ /**
112
136
  * Project `existing.hints` down to the known `MigrationHints` shape, dropping
113
- * any legacy keys that may linger in manifests scaffolded by older CLI
137
+ * any legacy keys that may linger in metadata scaffolded by older CLI
114
138
  * versions (e.g. `planningStrategy`). Picking fields explicitly instead of
115
139
  * spreading keeps refreshed `migration.json` files schema-clean regardless
116
140
  * of what was on disk before.
@@ -124,26 +148,30 @@ function normalizeHints(existing) {
124
148
  }
125
149
  /**
126
150
  * Pure conversion from a `Migration` instance (plus the previously
127
- * scaffolded manifest, when one exists on disk) to the in-memory
151
+ * scaffolded metadata, when one exists on disk) to the in-memory
128
152
  * artifacts that downstream tooling persists. Owns metadata validation,
129
- * manifest synthesis/preservation, hint normalization, and the
130
- * content-addressed `migrationId` computation, but performs no file I/O
153
+ * metadata synthesis/preservation, hint normalization, and the
154
+ * content-addressed `migrationHash` computation, but performs no file I/O
131
155
  * — callers handle reads (to source `existing`) and writes (to persist
132
- * `opsJson` / `manifestJson`).
156
+ * `opsJson` / `metadataJson`).
133
157
  */
134
158
  function buildMigrationArtifacts(instance, existing) {
135
159
  const ops = instance.operations;
136
160
  if (!Array.isArray(ops)) throw new Error("operations must be an array");
161
+ for (let index = 0; index < ops.length; index++) {
162
+ const result = MigrationOpSchema(ops[index]);
163
+ if (result instanceof type.errors) throw errorInvalidOperationEntry(index, result.summary);
164
+ }
137
165
  const parsed = MigrationMetaSchema(instance.describe());
138
166
  if (parsed instanceof type.errors) throw new Error(`describe() returned invalid metadata: ${parsed.summary}`);
139
- const manifest = buildAttestedManifest(parsed, ops, existing);
167
+ const metadata = buildAttestedMetadata(parsed, ops, existing);
140
168
  return {
141
169
  opsJson: JSON.stringify(ops, null, 2),
142
- manifest,
143
- manifestJson: JSON.stringify(manifest, null, 2)
170
+ metadata,
171
+ metadataJson: JSON.stringify(metadata, null, 2)
144
172
  };
145
173
  }
146
174
 
147
175
  //#endregion
148
- export { Migration, buildMigrationArtifacts, isDirectEntrypoint, printMigrationHelp };
176
+ export { Migration, buildMigrationArtifacts, isDirectEntrypoint };
149
177
  //# sourceMappingURL=migration.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"migration.mjs","names":["baseManifest: Omit<MigrationManifest, 'migrationId'>"],"sources":["../../src/migration-base.ts"],"sourcesContent":["import { realpathSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport type { Contract } from '@prisma-next/contract/types';\nimport type {\n ControlStack,\n MigrationPlan,\n MigrationPlanOperation,\n} from '@prisma-next/framework-components/control';\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport { type } from 'arktype';\nimport { computeMigrationId } from './attestation';\nimport type { MigrationHints, MigrationManifest, MigrationOps } from './types';\n\nexport interface MigrationMeta {\n readonly from: string;\n readonly to: string;\n readonly kind?: 'regular' | 'baseline';\n readonly labels?: readonly string[];\n}\n\nconst MigrationMetaSchema = type({\n from: 'string',\n to: 'string',\n 'kind?': \"'regular' | 'baseline'\",\n 'labels?': type('string').array(),\n});\n\n/**\n * Base class for migrations.\n *\n * A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the\n * runner can consume it directly via `targetId`, `operations`, `origin`, and\n * `destination`. The manifest-shaped inputs come from `describe()`, which\n * every migration must implement — `migration.json` is required for a\n * migration to be valid.\n */\nexport abstract class Migration<\n TOperation extends MigrationPlanOperation = MigrationPlanOperation,\n TFamilyId extends string = string,\n TTargetId extends string = string,\n> implements MigrationPlan\n{\n abstract readonly targetId: string;\n\n /**\n * Assembled `ControlStack` injected by the orchestrator (`runMigration`).\n *\n * Subclasses (e.g. `PostgresMigration`) read the stack to materialize their\n * adapter once per instance. Optional at the abstract level so unit tests can\n * construct `Migration` instances purely for `operations` / `describe`\n * assertions without needing a real stack; concrete subclasses that need the\n * stack at runtime should narrow the parameter to required.\n */\n protected readonly stack: ControlStack<TFamilyId, TTargetId> | undefined;\n\n constructor(stack?: ControlStack<TFamilyId, TTargetId>) {\n this.stack = stack;\n }\n\n /**\n * Ordered list of operations this migration performs.\n *\n * Implemented as a getter so that subclasses can either precompute the list\n * in their constructor or build it lazily per access.\n */\n abstract get operations(): readonly TOperation[];\n\n /**\n * Metadata inputs used to build `migration.json` and to derive the plan's\n * origin/destination identities. Every migration must provide this —\n * omitting it would produce an invalid on-disk migration package.\n */\n abstract describe(): MigrationMeta;\n\n get origin(): { readonly storageHash: string } | null {\n const from = this.describe().from;\n // An empty `from` represents a migration with no prior origin (e.g.\n // initial baseline, or an in-process plan that was never persisted).\n // Surface that as a null origin so runners treat the plan as\n // origin-less rather than matching against an empty storage hash.\n return from === '' ? null : { storageHash: from };\n }\n\n get destination(): { readonly storageHash: string } {\n return { storageHash: this.describe().to };\n }\n}\n\n/**\n * Returns true when `import.meta.url` resolves to the same file that was\n * invoked as the node entrypoint (`process.argv[1]`). Used by\n * `MigrationCLI.run` (in `@prisma-next/cli/migration-cli`) to no-op when\n * the migration module is being imported (e.g. by another script) rather\n * than executed directly.\n */\nexport function isDirectEntrypoint(importMetaUrl: string): boolean {\n const metaFilename = fileURLToPath(importMetaUrl);\n const argv1 = process.argv[1];\n if (!argv1) return false;\n try {\n return realpathSync(metaFilename) === realpathSync(argv1);\n } catch {\n return false;\n }\n}\n\nexport function printMigrationHelp(): void {\n printHelp();\n}\n\nfunction printHelp(): void {\n process.stdout.write(\n [\n 'Usage: node <migration-file> [options]',\n '',\n 'Options:',\n ' --dry-run Print operations to stdout without writing files',\n ' --help Show this help message',\n '',\n ].join('\\n'),\n );\n}\n\n/**\n * In-memory artifacts produced from a `Migration` instance: the\n * serialized `ops.json` body, the `migration.json` manifest object, and\n * its serialized form. Returned by `buildMigrationArtifacts` so callers\n * (today: `MigrationCLI.run` in `@prisma-next/cli/migration-cli`) can\n * decide how to persist them — write to disk, print in dry-run, ship\n * over the wire — without coupling artifact construction to file I/O.\n */\nexport interface MigrationArtifacts {\n readonly opsJson: string;\n readonly manifest: MigrationManifest;\n readonly manifestJson: string;\n}\n\n/**\n * Build the attested manifest from `describe()`-derived metadata, the\n * operations list, and the previously-scaffolded manifest (if any).\n *\n * When a `migration.json` already exists for this package (the common\n * case: it was scaffolded by `migration plan`), preserve the contract\n * bookends, hints, labels, and `createdAt` set there — those fields are\n * owned by the CLI scaffolder, not the authored class. Only the\n * `describe()`-derived fields (`from`, `to`, `kind`) and the operations\n * change as the author iterates. When no manifest exists yet (a bare\n * `migration.ts` run from scratch), synthesize a minimal but\n * schema-conformant manifest so the resulting package can still be read,\n * verified, and applied.\n *\n * The `migrationId` is recomputed against the current manifest + ops so\n * the on-disk artifacts are always fully attested.\n */\nfunction buildAttestedManifest(\n meta: MigrationMeta,\n ops: MigrationOps,\n existing: Partial<MigrationManifest> | null,\n): MigrationManifest {\n const baseManifest: Omit<MigrationManifest, 'migrationId'> = {\n from: meta.from,\n to: meta.to,\n kind: meta.kind ?? 'regular',\n labels: meta.labels ?? existing?.labels ?? [],\n createdAt: existing?.createdAt ?? new Date().toISOString(),\n fromContract: existing?.fromContract ?? null,\n // When no scaffolded manifest exists we synthesize a minimal contract\n // stub so the package is still readable end-to-end. The cast is\n // intentional: only the storage bookend matters for hash computation\n // (everything else is stripped by `computeMigrationId`), and a real\n // contract bookend would only be available after `migration plan`.\n toContract: existing?.toContract ?? ({ storage: { storageHash: meta.to } } as Contract),\n hints: normalizeHints(existing?.hints),\n ...ifDefined('authorship', existing?.authorship),\n };\n\n const migrationId = computeMigrationId(baseManifest, ops);\n return { ...baseManifest, migrationId };\n}\n\n/**\n * Project `existing.hints` down to the known `MigrationHints` shape, dropping\n * any legacy keys that may linger in manifests scaffolded by older CLI\n * versions (e.g. `planningStrategy`). Picking fields explicitly instead of\n * spreading keeps refreshed `migration.json` files schema-clean regardless\n * of what was on disk before.\n */\nfunction normalizeHints(existing: MigrationHints | undefined): MigrationHints {\n return {\n used: existing?.used ?? [],\n applied: existing?.applied ?? [],\n plannerVersion: existing?.plannerVersion ?? '2.0.0',\n };\n}\n\n/**\n * Pure conversion from a `Migration` instance (plus the previously\n * scaffolded manifest, when one exists on disk) to the in-memory\n * artifacts that downstream tooling persists. Owns metadata validation,\n * manifest synthesis/preservation, hint normalization, and the\n * content-addressed `migrationId` computation, but performs no file I/O\n * — callers handle reads (to source `existing`) and writes (to persist\n * `opsJson` / `manifestJson`).\n */\nexport function buildMigrationArtifacts(\n instance: Migration,\n existing: Partial<MigrationManifest> | null,\n): MigrationArtifacts {\n const ops = instance.operations;\n if (!Array.isArray(ops)) {\n throw new Error('operations must be an array');\n }\n\n const rawMeta: unknown = instance.describe();\n const parsed = MigrationMetaSchema(rawMeta);\n if (parsed instanceof type.errors) {\n throw new Error(`describe() returned invalid metadata: ${parsed.summary}`);\n }\n\n const manifest = buildAttestedManifest(parsed, ops, existing);\n\n return {\n opsJson: JSON.stringify(ops, null, 2),\n manifest,\n manifestJson: JSON.stringify(manifest, null, 2),\n };\n}\n"],"mappings":";;;;;;;;AAoBA,MAAM,sBAAsB,KAAK;CAC/B,MAAM;CACN,IAAI;CACJ,SAAS;CACT,WAAW,KAAK,SAAS,CAAC,OAAO;CAClC,CAAC;;;;;;;;;;AAWF,IAAsB,YAAtB,MAKA;;;;;;;;;;CAYE,AAAmB;CAEnB,YAAY,OAA4C;AACtD,OAAK,QAAQ;;CAkBf,IAAI,SAAkD;EACpD,MAAM,OAAO,KAAK,UAAU,CAAC;AAK7B,SAAO,SAAS,KAAK,OAAO,EAAE,aAAa,MAAM;;CAGnD,IAAI,cAAgD;AAClD,SAAO,EAAE,aAAa,KAAK,UAAU,CAAC,IAAI;;;;;;;;;;AAW9C,SAAgB,mBAAmB,eAAgC;CACjE,MAAM,eAAe,cAAc,cAAc;CACjD,MAAM,QAAQ,QAAQ,KAAK;AAC3B,KAAI,CAAC,MAAO,QAAO;AACnB,KAAI;AACF,SAAO,aAAa,aAAa,KAAK,aAAa,MAAM;SACnD;AACN,SAAO;;;AAIX,SAAgB,qBAA2B;AACzC,YAAW;;AAGb,SAAS,YAAkB;AACzB,SAAQ,OAAO,MACb;EACE;EACA;EACA;EACA;EACA;EACA;EACD,CAAC,KAAK,KAAK,CACb;;;;;;;;;;;;;;;;;;;AAkCH,SAAS,sBACP,MACA,KACA,UACmB;CACnB,MAAMA,eAAuD;EAC3D,MAAM,KAAK;EACX,IAAI,KAAK;EACT,MAAM,KAAK,QAAQ;EACnB,QAAQ,KAAK,UAAU,UAAU,UAAU,EAAE;EAC7C,WAAW,UAAU,8BAAa,IAAI,MAAM,EAAC,aAAa;EAC1D,cAAc,UAAU,gBAAgB;EAMxC,YAAY,UAAU,cAAe,EAAE,SAAS,EAAE,aAAa,KAAK,IAAI,EAAE;EAC1E,OAAO,eAAe,UAAU,MAAM;EACtC,GAAG,UAAU,cAAc,UAAU,WAAW;EACjD;CAED,MAAM,cAAc,mBAAmB,cAAc,IAAI;AACzD,QAAO;EAAE,GAAG;EAAc;EAAa;;;;;;;;;AAUzC,SAAS,eAAe,UAAsD;AAC5E,QAAO;EACL,MAAM,UAAU,QAAQ,EAAE;EAC1B,SAAS,UAAU,WAAW,EAAE;EAChC,gBAAgB,UAAU,kBAAkB;EAC7C;;;;;;;;;;;AAYH,SAAgB,wBACd,UACA,UACoB;CACpB,MAAM,MAAM,SAAS;AACrB,KAAI,CAAC,MAAM,QAAQ,IAAI,CACrB,OAAM,IAAI,MAAM,8BAA8B;CAIhD,MAAM,SAAS,oBADU,SAAS,UAAU,CACD;AAC3C,KAAI,kBAAkB,KAAK,OACzB,OAAM,IAAI,MAAM,yCAAyC,OAAO,UAAU;CAG5E,MAAM,WAAW,sBAAsB,QAAQ,KAAK,SAAS;AAE7D,QAAO;EACL,SAAS,KAAK,UAAU,KAAK,MAAM,EAAE;EACrC;EACA,cAAc,KAAK,UAAU,UAAU,MAAM,EAAE;EAChD"}
1
+ {"version":3,"file":"migration.mjs","names":["baseMetadata: Omit<MigrationMetadata, 'migrationHash'>"],"sources":["../../src/migration-base.ts"],"sourcesContent":["import { realpathSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport type { Contract } from '@prisma-next/contract/types';\nimport type {\n ControlStack,\n MigrationPlan,\n MigrationPlanOperation,\n} from '@prisma-next/framework-components/control';\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport { type } from 'arktype';\nimport { errorInvalidOperationEntry, errorStaleContractBookends } from './errors';\nimport { computeMigrationHash } from './hash';\nimport { deriveProvidedInvariants } from './invariants';\nimport type { MigrationHints, MigrationMetadata } from './metadata';\nimport { MigrationOpSchema } from './op-schema';\nimport type { MigrationOps } from './package';\n\nexport interface MigrationMeta {\n readonly from: string | null;\n readonly to: string;\n readonly labels?: readonly string[];\n}\n\n// `from` rejects empty strings to mirror `MigrationMetadataSchema` in\n// `./io.ts`. Without this match, an authored migration could `describe()` with\n// `from: ''` and pass `buildMigrationArtifacts`'s validation, only to have\n// `readMigrationPackage` reject the resulting `migration.json` later — the\n// two validators must agree on the legal value space.\nconst MigrationMetaSchema = type({\n from: 'string > 0 | null',\n to: 'string',\n 'labels?': type('string').array(),\n});\n\n/**\n * Base class for migrations.\n *\n * A `Migration` subclass is itself a `MigrationPlan`: CLI commands and the\n * runner can consume it directly via `targetId`, `operations`, `origin`, and\n * `destination`. The metadata-shaped inputs come from `describe()`, which\n * every migration must implement — `migration.json` is required for a\n * migration to be valid.\n */\nexport abstract class Migration<\n TOperation extends MigrationPlanOperation = MigrationPlanOperation,\n TFamilyId extends string = string,\n TTargetId extends string = string,\n> implements MigrationPlan\n{\n abstract readonly targetId: string;\n\n /**\n * Assembled `ControlStack` injected by the orchestrator (`runMigration`).\n *\n * Subclasses (e.g. `PostgresMigration`) read the stack to materialize their\n * adapter once per instance. Optional at the abstract level so unit tests can\n * construct `Migration` instances purely for `operations` / `describe`\n * assertions without needing a real stack; concrete subclasses that need the\n * stack at runtime should narrow the parameter to required.\n */\n protected readonly stack: ControlStack<TFamilyId, TTargetId> | undefined;\n\n constructor(stack?: ControlStack<TFamilyId, TTargetId>) {\n this.stack = stack;\n }\n\n /**\n * Ordered list of operations this migration performs.\n *\n * Implemented as a getter so that subclasses can either precompute the list\n * in their constructor or build it lazily per access.\n */\n abstract get operations(): readonly TOperation[];\n\n /**\n * Metadata inputs used to build `migration.json` and to derive the plan's\n * origin/destination identities. Every migration must provide this —\n * omitting it would produce an invalid on-disk migration package.\n */\n abstract describe(): MigrationMeta;\n\n get origin(): { readonly storageHash: string } | null {\n const from = this.describe().from;\n return from === null ? null : { storageHash: from };\n }\n\n get destination(): { readonly storageHash: string } {\n return { storageHash: this.describe().to };\n }\n}\n\n/**\n * Returns true when `import.meta.url` resolves to the same file that was\n * invoked as the node entrypoint (`process.argv[1]`). Used by\n * `MigrationCLI.run` (in `@prisma-next/cli/migration-cli`) to no-op when\n * the migration module is being imported (e.g. by another script) rather\n * than executed directly.\n */\nexport function isDirectEntrypoint(importMetaUrl: string): boolean {\n const metaFilename = fileURLToPath(importMetaUrl);\n const argv1 = process.argv[1];\n if (!argv1) return false;\n try {\n return realpathSync(metaFilename) === realpathSync(argv1);\n } catch {\n return false;\n }\n}\n\n/**\n * In-memory artifacts produced from a `Migration` instance: the\n * serialized `ops.json` body, the `migration.json` metadata object, and\n * its serialized form. Returned by `buildMigrationArtifacts` so callers\n * (today: `MigrationCLI.run` in `@prisma-next/cli/migration-cli`) can\n * decide how to persist them — write to disk, print in dry-run, ship\n * over the wire — without coupling artifact construction to file I/O.\n *\n * `metadataJson` is `JSON.stringify(metadata, null, 2)` — the canonical\n * on-disk shape that the arktype loader-schema in `./io` validates.\n */\nexport interface MigrationArtifacts {\n readonly opsJson: string;\n readonly metadata: MigrationMetadata;\n readonly metadataJson: string;\n}\n\n/**\n * Build the attested metadata from `describe()`-derived metadata, the\n * operations list, and the previously-scaffolded metadata (if any).\n *\n * When a `migration.json` already exists for this package (the common\n * case: it was scaffolded by `migration plan`), preserve the contract\n * bookends, hints, labels, and `createdAt` set there — those fields are\n * owned by the CLI scaffolder, not the authored class. Only the\n * `describe()`-derived fields (`from`, `to`) and the operations\n * change as the author iterates. When no metadata exists yet (a bare\n * `migration.ts` run from scratch), synthesize a minimal but\n * schema-conformant record so the resulting package can still be read,\n * verified, and applied.\n *\n * The `migrationHash` is recomputed against the current metadata + ops so\n * the on-disk artifacts are always fully attested.\n */\nfunction buildAttestedMetadata(\n meta: MigrationMeta,\n ops: MigrationOps,\n existing: Partial<MigrationMetadata> | null,\n): MigrationMetadata {\n assertBookendsMatchMeta(meta, existing);\n\n const baseMetadata: Omit<MigrationMetadata, 'migrationHash'> = {\n from: meta.from,\n to: meta.to,\n labels: meta.labels ?? existing?.labels ?? [],\n providedInvariants: deriveProvidedInvariants(ops),\n createdAt: existing?.createdAt ?? new Date().toISOString(),\n fromContract: existing?.fromContract ?? null,\n // When no scaffolded metadata exists we synthesize a minimal contract\n // stub so the package is still readable end-to-end. The cast is\n // intentional: only the storage bookend matters for hash computation\n // (everything else is stripped by `computeMigrationHash`), and a real\n // contract bookend would only be available after `migration plan`.\n toContract: existing?.toContract ?? ({ storage: { storageHash: meta.to } } as Contract),\n hints: normalizeHints(existing?.hints),\n ...ifDefined('authorship', existing?.authorship),\n };\n\n const migrationHash = computeMigrationHash(baseMetadata, ops);\n return { ...baseMetadata, migrationHash };\n}\n\n/**\n * Verify each preserved contract bookend in `existing` agrees with the\n * corresponding side of `describe()`'s output. A mismatch indicates the\n * migration's `describe()` was edited after `migration plan` scaffolded\n * the package, leaving a self-inconsistent manifest. Failing fast at\n * write-time turns a silent foot-gun into an actionable diagnostic.\n *\n * Skipped when a side's `existing.<side>Contract` is null/absent (the\n * synthesis path stays open for origin-less initial migrations and for\n * bare `migration.ts` runs from scratch). When a bookend is *present*\n * but its `storage.storageHash` is missing, that's treated as a\n * mismatch — a malformed bookend is not equivalent to \"no bookend\".\n *\n * This check is paired with TML-2274, which removes `fromContract` /\n * `toContract` from the manifest entirely; once that lands, this\n * function and its error code are deleted.\n */\nfunction assertBookendsMatchMeta(\n meta: MigrationMeta,\n existing: Partial<MigrationMetadata> | null,\n): void {\n if (existing?.fromContract != null) {\n const contractHash = existing.fromContract.storage?.storageHash ?? '';\n if (contractHash !== meta.from) {\n throw errorStaleContractBookends({\n side: 'from',\n metaHash: meta.from,\n contractHash,\n });\n }\n }\n if (existing?.toContract != null) {\n const contractHash = existing.toContract.storage?.storageHash ?? '';\n if (contractHash !== meta.to) {\n throw errorStaleContractBookends({\n side: 'to',\n metaHash: meta.to,\n contractHash,\n });\n }\n }\n}\n\n/**\n * Project `existing.hints` down to the known `MigrationHints` shape, dropping\n * any legacy keys that may linger in metadata scaffolded by older CLI\n * versions (e.g. `planningStrategy`). Picking fields explicitly instead of\n * spreading keeps refreshed `migration.json` files schema-clean regardless\n * of what was on disk before.\n */\nfunction normalizeHints(existing: MigrationHints | undefined): MigrationHints {\n return {\n used: existing?.used ?? [],\n applied: existing?.applied ?? [],\n plannerVersion: existing?.plannerVersion ?? '2.0.0',\n };\n}\n\n/**\n * Pure conversion from a `Migration` instance (plus the previously\n * scaffolded metadata, when one exists on disk) to the in-memory\n * artifacts that downstream tooling persists. Owns metadata validation,\n * metadata synthesis/preservation, hint normalization, and the\n * content-addressed `migrationHash` computation, but performs no file I/O\n * — callers handle reads (to source `existing`) and writes (to persist\n * `opsJson` / `metadataJson`).\n */\nexport function buildMigrationArtifacts(\n instance: Migration,\n existing: Partial<MigrationMetadata> | null,\n): MigrationArtifacts {\n const ops = instance.operations;\n if (!Array.isArray(ops)) {\n throw new Error('operations must be an array');\n }\n\n for (let index = 0; index < ops.length; index++) {\n const result = MigrationOpSchema(ops[index]);\n if (result instanceof type.errors) {\n throw errorInvalidOperationEntry(index, result.summary);\n }\n }\n\n const rawMeta: unknown = instance.describe();\n const parsed = MigrationMetaSchema(rawMeta);\n if (parsed instanceof type.errors) {\n throw new Error(`describe() returned invalid metadata: ${parsed.summary}`);\n }\n\n const metadata = buildAttestedMetadata(parsed, ops, existing);\n\n return {\n opsJson: JSON.stringify(ops, null, 2),\n metadata,\n metadataJson: JSON.stringify(metadata, null, 2),\n };\n}\n"],"mappings":";;;;;;;;;;AA4BA,MAAM,sBAAsB,KAAK;CAC/B,MAAM;CACN,IAAI;CACJ,WAAW,KAAK,SAAS,CAAC,OAAO;CAClC,CAAC;;;;;;;;;;AAWF,IAAsB,YAAtB,MAKA;;;;;;;;;;CAYE,AAAmB;CAEnB,YAAY,OAA4C;AACtD,OAAK,QAAQ;;CAkBf,IAAI,SAAkD;EACpD,MAAM,OAAO,KAAK,UAAU,CAAC;AAC7B,SAAO,SAAS,OAAO,OAAO,EAAE,aAAa,MAAM;;CAGrD,IAAI,cAAgD;AAClD,SAAO,EAAE,aAAa,KAAK,UAAU,CAAC,IAAI;;;;;;;;;;AAW9C,SAAgB,mBAAmB,eAAgC;CACjE,MAAM,eAAe,cAAc,cAAc;CACjD,MAAM,QAAQ,QAAQ,KAAK;AAC3B,KAAI,CAAC,MAAO,QAAO;AACnB,KAAI;AACF,SAAO,aAAa,aAAa,KAAK,aAAa,MAAM;SACnD;AACN,SAAO;;;;;;;;;;;;;;;;;;;;AAsCX,SAAS,sBACP,MACA,KACA,UACmB;AACnB,yBAAwB,MAAM,SAAS;CAEvC,MAAMA,eAAyD;EAC7D,MAAM,KAAK;EACX,IAAI,KAAK;EACT,QAAQ,KAAK,UAAU,UAAU,UAAU,EAAE;EAC7C,oBAAoB,yBAAyB,IAAI;EACjD,WAAW,UAAU,8BAAa,IAAI,MAAM,EAAC,aAAa;EAC1D,cAAc,UAAU,gBAAgB;EAMxC,YAAY,UAAU,cAAe,EAAE,SAAS,EAAE,aAAa,KAAK,IAAI,EAAE;EAC1E,OAAO,eAAe,UAAU,MAAM;EACtC,GAAG,UAAU,cAAc,UAAU,WAAW;EACjD;CAED,MAAM,gBAAgB,qBAAqB,cAAc,IAAI;AAC7D,QAAO;EAAE,GAAG;EAAc;EAAe;;;;;;;;;;;;;;;;;;;AAoB3C,SAAS,wBACP,MACA,UACM;AACN,KAAI,UAAU,gBAAgB,MAAM;EAClC,MAAM,eAAe,SAAS,aAAa,SAAS,eAAe;AACnE,MAAI,iBAAiB,KAAK,KACxB,OAAM,2BAA2B;GAC/B,MAAM;GACN,UAAU,KAAK;GACf;GACD,CAAC;;AAGN,KAAI,UAAU,cAAc,MAAM;EAChC,MAAM,eAAe,SAAS,WAAW,SAAS,eAAe;AACjE,MAAI,iBAAiB,KAAK,GACxB,OAAM,2BAA2B;GAC/B,MAAM;GACN,UAAU,KAAK;GACf;GACD,CAAC;;;;;;;;;;AAYR,SAAS,eAAe,UAAsD;AAC5E,QAAO;EACL,MAAM,UAAU,QAAQ,EAAE;EAC1B,SAAS,UAAU,WAAW,EAAE;EAChC,gBAAgB,UAAU,kBAAkB;EAC7C;;;;;;;;;;;AAYH,SAAgB,wBACd,UACA,UACoB;CACpB,MAAM,MAAM,SAAS;AACrB,KAAI,CAAC,MAAM,QAAQ,IAAI,CACrB,OAAM,IAAI,MAAM,8BAA8B;AAGhD,MAAK,IAAI,QAAQ,GAAG,QAAQ,IAAI,QAAQ,SAAS;EAC/C,MAAM,SAAS,kBAAkB,IAAI,OAAO;AAC5C,MAAI,kBAAkB,KAAK,OACzB,OAAM,2BAA2B,OAAO,OAAO,QAAQ;;CAK3D,MAAM,SAAS,oBADU,SAAS,UAAU,CACD;AAC3C,KAAI,kBAAkB,KAAK,OACzB,OAAM,IAAI,MAAM,yCAAyC,OAAO,UAAU;CAG5E,MAAM,WAAW,sBAAsB,QAAQ,KAAK,SAAS;AAE7D,QAAO;EACL,SAAS,KAAK,UAAU,KAAK,MAAM,EAAE;EACrC;EACA,cAAc,KAAK,UAAU,UAAU,MAAM,EAAE;EAChD"}
@@ -0,0 +1,2 @@
1
+ import { n as MigrationPackage, t as MigrationOps } from "../package-5HCCg0z-.mjs";
2
+ export { type MigrationOps, type MigrationPackage };
@@ -0,0 +1 @@
1
+ export { };
@@ -1,7 +1,7 @@
1
- import { c as errorInvalidRefFile, l as errorInvalidRefName, t as MigrationToolsError, u as errorInvalidRefValue } from "../errors-BmiSgz1j.mjs";
1
+ import { d as errorInvalidRefFile, f as errorInvalidRefName, p as errorInvalidRefValue, t as MigrationToolsError } from "../errors-CfmjBeK0.mjs";
2
+ import { dirname, join, relative } from "pathe";
2
3
  import { mkdir, readFile, readdir, rename, rmdir, unlink, writeFile } from "node:fs/promises";
3
4
  import { type } from "arktype";
4
- import { dirname, join, relative } from "pathe";
5
5
 
6
6
  //#region src/refs.ts
7
7
  const REF_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\/[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/;
@@ -0,0 +1,28 @@
1
+ //#region src/graph.d.ts
2
+ /**
3
+ * An entry in the migration graph. All on-disk migrations are attested,
4
+ * so `migrationHash` is always a string.
5
+ */
6
+ interface MigrationEdge {
7
+ readonly from: string;
8
+ readonly to: string;
9
+ readonly migrationHash: string;
10
+ readonly dirName: string;
11
+ readonly createdAt: string;
12
+ readonly labels: readonly string[];
13
+ /**
14
+ * Sorted, deduplicated list of `invariantId`s this edge provides.
15
+ * An empty array means the migration declares no routing-visible
16
+ * data transforms.
17
+ */
18
+ readonly invariants: readonly string[];
19
+ }
20
+ interface MigrationGraph {
21
+ readonly nodes: ReadonlySet<string>;
22
+ readonly forwardChain: ReadonlyMap<string, readonly MigrationEdge[]>;
23
+ readonly reverseChain: ReadonlyMap<string, readonly MigrationEdge[]>;
24
+ readonly migrationByHash: ReadonlyMap<string, MigrationEdge>;
25
+ }
26
+ //#endregion
27
+ export { MigrationGraph as n, MigrationEdge as t };
28
+ //# sourceMappingURL=graph-BHPv-9Gl.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-BHPv-9Gl.d.mts","names":[],"sources":["../src/graph.ts"],"sourcesContent":[],"mappings":";;AAIA;AAeA;;AAEsD,UAjBrC,aAAA,CAiBqC;EAA7B,SAAA,IAAA,EAAA,MAAA;EAC6B,SAAA,EAAA,EAAA,MAAA;EAA7B,SAAA,aAAA,EAAA,MAAA;EACuB,SAAA,OAAA,EAAA,MAAA;EAApB,SAAA,SAAA,EAAA,MAAA;EAAW,SAAA,MAAA,EAAA,SAAA,MAAA,EAAA;;;;;;;;UAJtB,cAAA;kBACC;yBACO,6BAA6B;yBAC7B,6BAA6B;4BAC1B,oBAAoB"}
@@ -0,0 +1,76 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ //#region src/canonicalize-json.ts
4
+ function sortKeys(value) {
5
+ if (value === null || typeof value !== "object") return value;
6
+ if (Array.isArray(value)) return value.map(sortKeys);
7
+ const sorted = {};
8
+ for (const key of Object.keys(value).sort()) sorted[key] = sortKeys(value[key]);
9
+ return sorted;
10
+ }
11
+ function canonicalizeJson(value) {
12
+ return JSON.stringify(sortKeys(value));
13
+ }
14
+
15
+ //#endregion
16
+ //#region src/hash.ts
17
+ function sha256Hex(input) {
18
+ return createHash("sha256").update(input).digest("hex");
19
+ }
20
+ /**
21
+ * Content-addressed migration hash over (metadata envelope sans
22
+ * contracts/hints/signature, ops). See ADR 199 — Storage-only migration
23
+ * identity for the rationale: contracts are anchored separately by the
24
+ * storage-hash bookends inside the envelope; planner hints are advisory
25
+ * and must not affect identity.
26
+ *
27
+ * The integrity check is purely structural, not semantic. The function
28
+ * canonicalizes its inputs via `sortKeys` (recursive) + `JSON.stringify`
29
+ * and hashes the result. Target-specific operation payloads (`step.sql`,
30
+ * Mongo's pipeline AST, …) are hashed verbatim — no per-target
31
+ * normalization is required, because what's being verified is "do the
32
+ * on-disk bytes still produce their recorded hash", not "do two
33
+ * semantically-equivalent migrations hash the same". The latter is an
34
+ * emit-drift concern (ADR 192 step 2).
35
+ *
36
+ * The symmetry across write and read holds because `JSON.parse(
37
+ * JSON.stringify(x))` round-trips JSON-safe values losslessly and
38
+ * `sortKeys` is idempotent and deterministic — write-time and read-time
39
+ * canonicalization produce the same canonical bytes regardless of
40
+ * source-side key ordering or whitespace.
41
+ *
42
+ * The `migrationHash` field on the metadata is stripped before hashing
43
+ * so the function can be used both at write time (when no hash exists
44
+ * yet) and at verify time (rehashing an already-attested record).
45
+ */
46
+ function computeMigrationHash(metadata, ops) {
47
+ const { migrationHash: _migrationHash, signature: _signature, fromContract: _fromContract, toContract: _toContract, hints: _hints, ...strippedMeta } = metadata;
48
+ return `sha256:${sha256Hex(canonicalizeJson([canonicalizeJson(strippedMeta), canonicalizeJson(ops)].map(sha256Hex)))}`;
49
+ }
50
+ /**
51
+ * Re-hash an in-memory migration package and compare against the stored
52
+ * `migrationHash`. See `computeMigrationHash` for the canonicalization rules.
53
+ *
54
+ * Returns `{ ok: true }` when the package is internally consistent, or
55
+ * `{ ok: false, reason: 'mismatch', storedHash, computedHash }` when it is
56
+ * not — typically a sign of FS corruption, partial writes, or a post-emit
57
+ * hand edit.
58
+ */
59
+ function verifyMigrationHash(pkg) {
60
+ const computed = computeMigrationHash(pkg.metadata, pkg.ops);
61
+ if (pkg.metadata.migrationHash === computed) return {
62
+ ok: true,
63
+ storedHash: pkg.metadata.migrationHash,
64
+ computedHash: computed
65
+ };
66
+ return {
67
+ ok: false,
68
+ reason: "mismatch",
69
+ storedHash: pkg.metadata.migrationHash,
70
+ computedHash: computed
71
+ };
72
+ }
73
+
74
+ //#endregion
75
+ export { verifyMigrationHash as n, computeMigrationHash as t };
76
+ //# sourceMappingURL=hash-BARZdVgW.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash-BARZdVgW.mjs","names":["sorted: Record<string, unknown>"],"sources":["../src/canonicalize-json.ts","../src/hash.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 type { MigrationMetadata } from './metadata';\nimport type { MigrationOps, MigrationPackage } 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\n * contracts/hints/signature, ops). See ADR 199 — Storage-only migration\n * identity 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 *\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 {\n migrationHash: _migrationHash,\n signature: _signature,\n fromContract: _fromContract,\n toContract: _toContract,\n hints: _hints,\n ...strippedMeta\n } = 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: MigrationPackage): 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":";;;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BzD,SAAgB,qBACd,UACA,KACQ;CACR,MAAM,EACJ,eAAe,gBACf,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;;;;;;;;;;;AActD,SAAgB,oBAAoB,KAAqC;CACvE,MAAM,WAAW,qBAAqB,IAAI,UAAU,IAAI,IAAI;AAE5D,KAAI,IAAI,SAAS,kBAAkB,SACjC,QAAO;EACL,IAAI;EACJ,YAAY,IAAI,SAAS;EACzB,cAAc;EACf;AAGH,QAAO;EACL,IAAI;EACJ,QAAQ;EACR,YAAY,IAAI,SAAS;EACzB,cAAc;EACf"}
@@ -0,0 +1,42 @@
1
+ import { i as errorDuplicateInvariantInEdge, s as errorInvalidInvariantId } from "./errors-CfmjBeK0.mjs";
2
+
3
+ //#region src/invariants.ts
4
+ /**
5
+ * Hygiene check for `invariantId`. Rejects empty values plus any
6
+ * whitespace or control character (including Unicode whitespace like
7
+ * NBSP and em space, which are visually identical to ASCII space and
8
+ * routinely sneak in via paste).
9
+ */
10
+ function validateInvariantId(invariantId) {
11
+ if (invariantId.length === 0) return false;
12
+ return !/[\p{Cc}\p{White_Space}]/u.test(invariantId);
13
+ }
14
+ /**
15
+ * Walk a migration's operations and produce its `providedInvariants`
16
+ * aggregate: the sorted, deduplicated list of `invariantId`s declared
17
+ * by data-transform ops. Ops without `operationClass === 'data'` are
18
+ * skipped; data ops without an `invariantId` are skipped.
19
+ *
20
+ * Throws `MIGRATION.INVALID_INVARIANT_ID` on a malformed id and
21
+ * `MIGRATION.DUPLICATE_INVARIANT_IN_EDGE` on duplicates.
22
+ */
23
+ function deriveProvidedInvariants(ops) {
24
+ const seen = /* @__PURE__ */ new Set();
25
+ for (const op of ops) {
26
+ const invariantId = readInvariantId(op);
27
+ if (invariantId === void 0) continue;
28
+ if (!validateInvariantId(invariantId)) throw errorInvalidInvariantId(invariantId);
29
+ if (seen.has(invariantId)) throw errorDuplicateInvariantInEdge(invariantId);
30
+ seen.add(invariantId);
31
+ }
32
+ return [...seen].sort();
33
+ }
34
+ function readInvariantId(op) {
35
+ if (op.operationClass !== "data") return void 0;
36
+ const candidate = op.invariantId;
37
+ return typeof candidate === "string" ? candidate : void 0;
38
+ }
39
+
40
+ //#endregion
41
+ export { validateInvariantId as n, deriveProvidedInvariants as t };
42
+ //# sourceMappingURL=invariants-30VA65sB.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invariants-30VA65sB.mjs","names":[],"sources":["../src/invariants.ts"],"sourcesContent":["import type { MigrationPlanOperation } from '@prisma-next/framework-components/control';\nimport { errorDuplicateInvariantInEdge, errorInvalidInvariantId } from './errors';\nimport type { MigrationOps } from './package';\n\n/**\n * Hygiene check for `invariantId`. Rejects empty values plus any\n * whitespace or control character (including Unicode whitespace like\n * NBSP and em space, which are visually identical to ASCII space and\n * routinely sneak in via paste).\n */\nexport function validateInvariantId(invariantId: string): boolean {\n if (invariantId.length === 0) return false;\n return !/[\\p{Cc}\\p{White_Space}]/u.test(invariantId);\n}\n\n/**\n * Walk a migration's operations and produce its `providedInvariants`\n * aggregate: the sorted, deduplicated list of `invariantId`s declared\n * by data-transform ops. Ops without `operationClass === 'data'` are\n * skipped; data ops without an `invariantId` are skipped.\n *\n * Throws `MIGRATION.INVALID_INVARIANT_ID` on a malformed id and\n * `MIGRATION.DUPLICATE_INVARIANT_IN_EDGE` on duplicates.\n */\nexport function deriveProvidedInvariants(ops: MigrationOps): readonly string[] {\n const seen = new Set<string>();\n for (const op of ops) {\n const invariantId = readInvariantId(op);\n if (invariantId === undefined) continue;\n if (!validateInvariantId(invariantId)) {\n throw errorInvalidInvariantId(invariantId);\n }\n if (seen.has(invariantId)) {\n throw errorDuplicateInvariantInEdge(invariantId);\n }\n seen.add(invariantId);\n }\n return [...seen].sort();\n}\n\nfunction readInvariantId(op: MigrationPlanOperation): string | undefined {\n if (op.operationClass !== 'data') return undefined;\n const candidate = (op as { invariantId?: unknown }).invariantId;\n return typeof candidate === 'string' ? candidate : undefined;\n}\n"],"mappings":";;;;;;;;;AAUA,SAAgB,oBAAoB,aAA8B;AAChE,KAAI,YAAY,WAAW,EAAG,QAAO;AACrC,QAAO,CAAC,2BAA2B,KAAK,YAAY;;;;;;;;;;;AAYtD,SAAgB,yBAAyB,KAAsC;CAC7E,MAAM,uBAAO,IAAI,KAAa;AAC9B,MAAK,MAAM,MAAM,KAAK;EACpB,MAAM,cAAc,gBAAgB,GAAG;AACvC,MAAI,gBAAgB,OAAW;AAC/B,MAAI,CAAC,oBAAoB,YAAY,CACnC,OAAM,wBAAwB,YAAY;AAE5C,MAAI,KAAK,IAAI,YAAY,CACvB,OAAM,8BAA8B,YAAY;AAElD,OAAK,IAAI,YAAY;;AAEvB,QAAO,CAAC,GAAG,KAAK,CAAC,MAAM;;AAGzB,SAAS,gBAAgB,IAAgD;AACvE,KAAI,GAAG,mBAAmB,OAAQ,QAAO;CACzC,MAAM,YAAa,GAAiC;AACpD,QAAO,OAAO,cAAc,WAAW,YAAY"}
@@ -0,0 +1,50 @@
1
+ import { Contract } from "@prisma-next/contract/types";
2
+
3
+ //#region src/metadata.d.ts
4
+ interface MigrationHints {
5
+ readonly used: readonly string[];
6
+ readonly applied: readonly string[];
7
+ readonly plannerVersion: string;
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
+ interface MigrationMetadata {
25
+ readonly migrationHash: string;
26
+ readonly from: string | null;
27
+ readonly to: string;
28
+ readonly fromContract: Contract | null;
29
+ readonly toContract: Contract;
30
+ readonly hints: MigrationHints;
31
+ readonly labels: readonly string[];
32
+ /**
33
+ * Sorted, deduplicated list of `invariantId`s declared by the
34
+ * migration's data-transform ops. Always present; an empty array
35
+ * means the migration has no routing-visible data transforms.
36
+ */
37
+ readonly providedInvariants: readonly string[];
38
+ readonly authorship?: {
39
+ readonly author?: string;
40
+ readonly email?: string;
41
+ };
42
+ readonly signature?: {
43
+ readonly keyId: string;
44
+ readonly value: string;
45
+ } | null;
46
+ readonly createdAt: string;
47
+ }
48
+ //#endregion
49
+ export { MigrationMetadata as n, MigrationHints as t };
50
+ //# sourceMappingURL=metadata-BP1cmU7Z.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata-BP1cmU7Z.d.mts","names":[],"sources":["../src/metadata.ts"],"sourcesContent":[],"mappings":";;;UAEiB,cAAA;;EAAA,SAAA,OAAA,EAAc,SAAA,MAAA,EAAA;EAqBd,SAAA,cAAiB,EAAA,MAAA;;;;;;;;;;;;;;;;;UAAjB,iBAAA;;;;yBAIQ;uBACF;kBACL"}
@@ -0,0 +1,14 @@
1
+ import { type } from "arktype";
2
+
3
+ //#region src/op-schema.ts
4
+ const MigrationOpSchema = type({
5
+ id: "string",
6
+ label: "string",
7
+ operationClass: "'additive' | 'widening' | 'destructive' | 'data'",
8
+ "invariantId?": "string"
9
+ });
10
+ const MigrationOpsSchema = MigrationOpSchema.array();
11
+
12
+ //#endregion
13
+ export { MigrationOpsSchema as n, MigrationOpSchema as t };
14
+ //# sourceMappingURL=op-schema-DZKFua46.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"op-schema-DZKFua46.mjs","names":[],"sources":["../src/op-schema.ts"],"sourcesContent":["import { type } from 'arktype';\n\nexport const MigrationOpSchema = type({\n id: 'string',\n label: 'string',\n operationClass: \"'additive' | 'widening' | 'destructive' | 'data'\",\n 'invariantId?': 'string',\n});\n\n// Intentionally shallow: operation-specific payload validation is owned by planner/runner layers.\nexport const MigrationOpsSchema = MigrationOpSchema.array();\n"],"mappings":";;;AAEA,MAAa,oBAAoB,KAAK;CACpC,IAAI;CACJ,OAAO;CACP,gBAAgB;CAChB,gBAAgB;CACjB,CAAC;AAGF,MAAa,qBAAqB,kBAAkB,OAAO"}
@@ -0,0 +1,21 @@
1
+ import { n as MigrationMetadata } from "./metadata-BP1cmU7Z.mjs";
2
+ import { MigrationPlanOperation } from "@prisma-next/framework-components/control";
3
+
4
+ //#region src/package.d.ts
5
+ type MigrationOps = readonly MigrationPlanOperation[];
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
+ interface MigrationPackage {
14
+ readonly dirName: string;
15
+ readonly dirPath: string;
16
+ readonly metadata: MigrationMetadata;
17
+ readonly ops: MigrationOps;
18
+ }
19
+ //#endregion
20
+ export { MigrationPackage as n, MigrationOps as t };
21
+ //# sourceMappingURL=package-5HCCg0z-.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package-5HCCg0z-.d.mts","names":[],"sources":["../src/package.ts"],"sourcesContent":[],"mappings":";;;;KAGY,YAAA,YAAwB;;AAApC;AASA;;;;;UAAiB,gBAAA;;;qBAGI;gBACL"}