@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/README.md CHANGED
@@ -4,24 +4,32 @@
4
4
  > and is published only to support its runtime. Its API is unstable and may change
5
5
  > without notice. Do not depend on this package directly; install `prisma-next` instead.
6
6
 
7
- On-disk migration persistence, attestation, and history reconstruction for Prisma Next.
7
+ On-disk migration persistence, hash verification, and history reconstruction for Prisma Next.
8
8
 
9
9
  ## Responsibilities
10
10
 
11
- - **Types**: Define the on-disk migration format (`MigrationManifest`, `MigrationOps`, `MigrationPackage`, `MigrationGraph`)
11
+ - **Types**: Define the migration package shape (`MigrationMetadata`, `MigrationOps`, `MigrationPackage`, `MigrationGraph`)
12
12
  - **I/O**: Read and write migration packages to/from disk (`migration.json` + `ops.json`)
13
- - **Attestation**: Compute and verify content-addressed migration IDs for tamper detection
13
+ - **Hashing**: Compute and verify content-addressed migration hashes for tamper detection
14
14
  - **History reconstruction**: Reconstruct and navigate migration history (path finding, latest migration detection, cycle/orphan detection)
15
15
 
16
- ## Attestation framing
16
+ ## Hash framing
17
17
 
18
- `computeMigrationId` in `attestation.ts` uses explicit framing:
18
+ `computeMigrationHash` in `hash.ts` uses explicit framing:
19
19
 
20
- 1. Canonicalize migration manifest metadata, ops, and embedded contracts.
21
- 2. Hash each canonical part independently with SHA-256.
22
- 3. Hash the canonical JSON tuple of those part hashes.
20
+ 1. Strip identity-affecting fields (`migrationHash`, `signature`, `fromContract`,
21
+ `toContract`, `hints`) from the metadata envelope, then canonicalize the
22
+ stripped envelope and the ops array.
23
+ 2. SHA-256 each canonical part independently.
24
+ 3. SHA-256 the canonical JSON tuple `[hash(metadata), hash(ops)]`.
23
25
 
24
- This avoids delimiter-ambiguity and ensures `migrationId` commits to the exact 4-part tuple.
26
+ This avoids delimiter-ambiguity and pins `migrationHash` to a 2-tuple over
27
+ the on-disk storage shape. Per [ADR 199 — Storage-only migration identity],
28
+ contracts are anchored separately via storage-hash bookends inside the
29
+ metadata envelope, and planner hints are advisory and must not affect
30
+ identity.
31
+
32
+ [ADR 199 — Storage-only migration identity]: ../../../docs/architecture%20docs/adrs/ADR%20199%20-%20Storage-only%20migration%20identity.md
25
33
 
26
34
  ## Ops validation boundary
27
35
 
@@ -36,14 +44,15 @@ Full semantic validation happens in target/family migration planners and runners
36
44
 
37
45
  ```mermaid
38
46
  graph TD
39
- CLI["CLI commands<br/>(migration plan, apply, verify, show, status)"] --> IO["io.ts<br/>File I/O"]
40
- CLI --> ATT["attestation.ts<br/>Migration attestation"]
41
- CLI --> GRAPH["dag.ts<br/>Graph operations"]
42
- IO --> TYPES["types.ts<br/>MigrationManifest, etc."]
43
- ATT --> IO
44
- ATT --> CAN["canonicalize-json.ts"]
45
- ATT --> CP["@prisma-next/emitter<br/>canonicalizeContract"]
46
- GRAPH --> TYPES
47
+ CLI["CLI commands<br/>(migration new, plan, apply, show, status)"] --> IO["io.ts<br/>File I/O"]
48
+ CLI --> HASH["hash.ts<br/>Migration hashing"]
49
+ CLI --> GRAPH["migration-graph.ts<br/>Graph operations"]
50
+ IO --> META["metadata.ts<br/>MigrationMetadata, MigrationHints"]
51
+ IO --> PKG["package.ts<br/>MigrationPackage, MigrationOps"]
52
+ HASH --> IO
53
+ HASH --> CAN["canonicalize-json.ts"]
54
+ HASH --> CP["@prisma-next/emitter<br/>canonicalizeContract"]
55
+ GRAPH --> GR["graph.ts<br/>MigrationGraph, MigrationEdge"]
47
56
  GRAPH --> ABS["@prisma-next/migration-tools/constants<br/>EMPTY_CONTRACT_HASH"]
48
57
  ```
49
58
 
@@ -51,7 +60,7 @@ graph TD
51
60
 
52
61
  | Package | Why |
53
62
  |---|---|
54
- | `@prisma-next/contract` | `Contract` type for embedded contracts in manifests |
63
+ | `@prisma-next/contract` | `Contract` type for embedded contracts in metadata |
55
64
  | `@prisma-next/framework-components` | `MigrationPlanOperation` types (via `./control`) |
56
65
  | `@prisma-next/emitter` | `canonicalizeContract` |
57
66
  | `arktype` | Runtime shape validation for `migration.json` and `ops.json` |
@@ -66,10 +75,13 @@ graph TD
66
75
 
67
76
  | Subpath | Contents |
68
77
  |---|---|
69
- | `./types` | `MigrationManifest`, `MigrationOps`, `MigrationPackage`, `MigrationGraph`, `MigrationChainEntry`, `MigrationHints` |
78
+ | `./metadata` | `MigrationMetadata`, `MigrationHints` |
79
+ | `./package` | `MigrationPackage`, `MigrationOps` |
80
+ | `./graph` | `MigrationGraph`, `MigrationEdge` |
70
81
  | `./io` | `writeMigrationPackage`, `readMigrationPackage`, `readMigrationsDir`, `formatMigrationDirName` |
71
- | `./attestation` | `computeMigrationId`, `verifyMigration`, `verifyMigrationBundle` |
72
- | `./dag` | `reconstructGraph`, `findLeaf`, `findPath`, `detectCycles`, `detectOrphans` |
82
+ | `./hash` | `computeMigrationHash`, `verifyMigrationHash` |
83
+ | `./migration-graph` | `reconstructGraph`, `findLeaf`, `findPath`, `detectCycles`, `detectOrphans` |
84
+ | `./errors` | `MigrationToolsError` |
73
85
  | `./constants` | `EMPTY_CONTRACT_HASH` |
74
86
 
75
87
  ## On-Disk Format
@@ -79,7 +91,7 @@ Each migration is a directory containing two files:
79
91
  ```
80
92
  migrations/
81
93
  20260225T1430_add_users/
82
- migration.json # MigrationManifest
94
+ migration.json # MigrationMetadata
83
95
  ops.json # MigrationPlanOperation[]
84
96
  ```
85
97
 
@@ -7,4 +7,4 @@ const EMPTY_CONTRACT_HASH = "sha256:empty";
7
7
 
8
8
  //#endregion
9
9
  export { EMPTY_CONTRACT_HASH as t };
10
- //# sourceMappingURL=constants-BRi0X7B_.mjs.map
10
+ //# sourceMappingURL=constants-WVGVMOdu.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants-BRi0X7B_.mjs","names":[],"sources":["../src/constants.ts"],"sourcesContent":["/**\n * Sentinel value representing the absence of a contract (empty/new project).\n * This is a human-readable marker, not a real SHA-256 hash.\n */\nexport const EMPTY_CONTRACT_HASH = 'sha256:empty' as const;\n"],"mappings":";;;;;AAIA,MAAa,sBAAsB"}
1
+ {"version":3,"file":"constants-WVGVMOdu.mjs","names":[],"sources":["../src/constants.ts"],"sourcesContent":["/**\n * Sentinel value representing the absence of a contract (empty/new project).\n * This is a human-readable marker, not a real SHA-256 hash.\n */\nexport const EMPTY_CONTRACT_HASH = 'sha256:empty' as const;\n"],"mappings":";;;;;AAIA,MAAa,sBAAsB"}
@@ -1,11 +1,28 @@
1
+ import { basename, dirname, relative } from "pathe";
2
+
1
3
  //#region src/errors.ts
2
4
  /**
5
+ * Build the canonical "re-emit this package" remediation hint.
6
+ *
7
+ * Every on-disk migration package ships its own `migration.ts` author-time
8
+ * file. Running it regenerates `migration.json` and `ops.json` with the
9
+ * correct hash + metadata, so it is the right primitive whenever a single
10
+ * package's on-disk artifacts are missing, malformed, or otherwise corrupt.
11
+ * Pointing users at `migration plan` would emit a *new* package rather than
12
+ * heal the broken one.
13
+ */
14
+ function reemitHint(dir, fallback) {
15
+ const reemit = `Re-emit the package by running \`node "${relative(process.cwd(), dir)}/migration.ts"\``;
16
+ return fallback ? `${reemit}, ${fallback}` : `${reemit}.`;
17
+ }
18
+ /**
3
19
  * Structured error for migration tooling operations.
4
20
  *
5
21
  * Follows the NAMESPACE.SUBCODE convention from ADR 027. All codes live under
6
- * the MIGRATION namespace. These are tooling-time errors (file I/O, attestation,
7
- * migration history reconstruction), distinct from the runtime MIGRATION.* codes for apply-time
8
- * failures (PRECHECK_FAILED, POSTCHECK_FAILED, etc.).
22
+ * the MIGRATION namespace. These are tooling-time errors (file I/O, hash
23
+ * verification, migration history reconstruction), distinct from the runtime
24
+ * MIGRATION.* codes for apply-time failures (PRECHECK_FAILED, POSTCHECK_FAILED,
25
+ * etc.).
9
26
  *
10
27
  * Fields:
11
28
  * - code: Stable machine-readable code (MIGRATION.SUBCODE)
@@ -44,7 +61,7 @@ function errorDirectoryExists(dir) {
44
61
  function errorMissingFile(file, dir) {
45
62
  return new MigrationToolsError("MIGRATION.FILE_MISSING", `Missing ${file}`, {
46
63
  why: `Expected "${file}" in "${dir}" but the file does not exist.`,
47
- fix: "Ensure the migration directory contains both migration.json and ops.json. If the directory is corrupt, delete it and re-run migration plan.",
64
+ fix: reemitHint(dir, "or delete the directory if the migration is unwanted and the source TypeScript is gone."),
48
65
  details: {
49
66
  file,
50
67
  dir
@@ -54,7 +71,7 @@ function errorMissingFile(file, dir) {
54
71
  function errorInvalidJson(filePath, parseError) {
55
72
  return new MigrationToolsError("MIGRATION.INVALID_JSON", "Invalid JSON in migration file", {
56
73
  why: `Failed to parse "${filePath}": ${parseError}`,
57
- fix: "Fix the JSON syntax error, or delete the migration directory and re-run migration plan.",
74
+ fix: reemitHint(dirname(filePath), "or restore the directory from version control."),
58
75
  details: {
59
76
  filePath,
60
77
  parseError
@@ -63,8 +80,8 @@ function errorInvalidJson(filePath, parseError) {
63
80
  }
64
81
  function errorInvalidManifest(filePath, reason) {
65
82
  return new MigrationToolsError("MIGRATION.INVALID_MANIFEST", "Invalid migration manifest", {
66
- why: `Manifest at "${filePath}" is invalid: ${reason}`,
67
- fix: "Ensure the manifest has all required fields (from, to, kind, toContract). If corrupt, delete and re-plan.",
83
+ why: `Migration manifest at "${filePath}" is invalid: ${reason}`,
84
+ fix: reemitHint(dirname(filePath), "or restore the directory from version control."),
68
85
  details: {
69
86
  filePath,
70
87
  reason
@@ -85,10 +102,11 @@ function errorInvalidDestName(destName) {
85
102
  details: { destName }
86
103
  });
87
104
  }
88
- function errorSameSourceAndTarget(dirName, hash) {
105
+ function errorSameSourceAndTarget(dir, hash) {
106
+ const dirName = basename(dir);
89
107
  return new MigrationToolsError("MIGRATION.SAME_SOURCE_AND_TARGET", "Migration has same source and target", {
90
108
  why: `Migration "${dirName}" has from === to === "${hash}". A migration must transition between two different contract states.`,
91
- fix: "Delete the invalid migration directory and re-run migration plan.",
109
+ fix: reemitHint(dir, "or delete the directory if the migration is unwanted and the source TypeScript is gone."),
92
110
  details: {
93
111
  dirName,
94
112
  hash
@@ -116,12 +134,12 @@ function errorNoInitialMigration(nodes) {
116
134
  details: { nodes }
117
135
  });
118
136
  }
119
- function errorInvalidRefs(refsPath, reason) {
120
- return new MigrationToolsError("MIGRATION.INVALID_REFS", "Invalid refs.json", {
121
- why: `refs.json at "${refsPath}" is invalid: ${reason}`,
122
- fix: "Ensure refs.json is a flat object mapping valid ref names to contract hash strings.",
137
+ function errorInvalidRefFile(filePath, reason) {
138
+ return new MigrationToolsError("MIGRATION.INVALID_REF_FILE", "Invalid ref file", {
139
+ why: `Ref file at "${filePath}" is invalid: ${reason}`,
140
+ fix: "Ensure the ref file contains valid JSON with { \"hash\": \"sha256:<64 hex chars>\", \"invariants\": [\"...\"] }.",
123
141
  details: {
124
- path: refsPath,
142
+ path: filePath,
125
143
  reason
126
144
  }
127
145
  });
@@ -147,14 +165,25 @@ function errorInvalidRefValue(value) {
147
165
  details: { value }
148
166
  });
149
167
  }
150
- function errorDuplicateMigrationId(migrationId) {
151
- return new MigrationToolsError("MIGRATION.DUPLICATE_MIGRATION_ID", "Duplicate migrationId in migration graph", {
152
- why: `Multiple migrations share migrationId "${migrationId}". Each migration must have a unique content-addressed identity.`,
153
- fix: "Regenerate one of the conflicting migrations so each migrationId is unique, then re-run migration commands.",
154
- details: { migrationId }
168
+ function errorDuplicateMigrationHash(migrationHash) {
169
+ return new MigrationToolsError("MIGRATION.DUPLICATE_MIGRATION_HASH", "Duplicate migrationHash in migration graph", {
170
+ why: `Multiple migrations share migrationHash "${migrationHash}". Each migration must have a unique content-addressed identity.`,
171
+ fix: "Regenerate one of the conflicting migrations so each migrationHash is unique, then re-run migration commands.",
172
+ details: { migrationHash }
173
+ });
174
+ }
175
+ function errorMigrationHashMismatch(dir, storedHash, computedHash) {
176
+ return new MigrationToolsError("MIGRATION.HASH_MISMATCH", "Migration package is corrupt", {
177
+ why: `Stored migrationHash "${storedHash}" does not match the recomputed hash "${computedHash}" for "${relative(process.cwd(), dir)}". The migration.json or ops.json has been edited or partially written since emit.`,
178
+ fix: reemitHint(dir, "or restore the directory from version control."),
179
+ details: {
180
+ dir,
181
+ storedHash,
182
+ computedHash
183
+ }
155
184
  });
156
185
  }
157
186
 
158
187
  //#endregion
159
- export { errorInvalidDestName as a, errorInvalidRefName as c, errorInvalidSlug as d, errorMissingFile as f, errorSameSourceAndTarget as h, errorDuplicateMigrationId as i, errorInvalidRefValue as l, errorNoTarget as m, errorAmbiguousTarget as n, errorInvalidJson as o, errorNoInitialMigration as p, errorDirectoryExists as r, errorInvalidManifest as s, MigrationToolsError as t, errorInvalidRefs as u };
160
- //# sourceMappingURL=errors-BKbRGCJM.mjs.map
188
+ export { errorInvalidDestName as a, errorInvalidRefFile as c, errorInvalidSlug as d, errorMigrationHashMismatch as f, errorSameSourceAndTarget as g, errorNoTarget as h, errorDuplicateMigrationHash as i, errorInvalidRefName as l, errorNoInitialMigration as m, errorAmbiguousTarget as n, errorInvalidJson as o, errorMissingFile as p, errorDirectoryExists as r, errorInvalidManifest as s, MigrationToolsError as t, errorInvalidRefValue as u };
189
+ //# sourceMappingURL=errors-CZ9JD4sd.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors-CZ9JD4sd.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["import { basename, dirname, relative } from 'pathe';\n\n/**\n * Build the canonical \"re-emit this package\" remediation hint.\n *\n * Every on-disk migration package ships its own `migration.ts` author-time\n * file. Running it regenerates `migration.json` and `ops.json` with the\n * correct hash + metadata, so it is the right primitive whenever a single\n * package's on-disk artifacts are missing, malformed, or otherwise corrupt.\n * Pointing users at `migration plan` would emit a *new* package rather than\n * heal the broken one.\n */\nfunction reemitHint(dir: string, fallback?: string): string {\n const relativeDir = relative(process.cwd(), dir);\n const reemit = `Re-emit the package by running \\`node \"${relativeDir}/migration.ts\"\\``;\n return fallback ? `${reemit}, ${fallback}` : `${reemit}.`;\n}\n\n/**\n * Structured error for migration tooling operations.\n *\n * Follows the NAMESPACE.SUBCODE convention from ADR 027. All codes live under\n * the MIGRATION namespace. These are tooling-time errors (file I/O, hash\n * verification, migration history reconstruction), distinct from the runtime\n * MIGRATION.* codes for apply-time failures (PRECHECK_FAILED, POSTCHECK_FAILED,\n * etc.).\n *\n * Fields:\n * - code: Stable machine-readable code (MIGRATION.SUBCODE)\n * - category: Always 'MIGRATION'\n * - why: Explains the cause in plain language\n * - fix: Actionable remediation step\n * - details: Machine-readable structured data for agents\n */\nexport class MigrationToolsError extends Error {\n readonly code: string;\n readonly category = 'MIGRATION' as const;\n readonly why: string;\n readonly fix: string;\n readonly details: Record<string, unknown> | undefined;\n\n constructor(\n code: string,\n summary: string,\n options: {\n readonly why: string;\n readonly fix: string;\n readonly details?: Record<string, unknown>;\n },\n ) {\n super(summary);\n this.name = 'MigrationToolsError';\n this.code = code;\n this.why = options.why;\n this.fix = options.fix;\n this.details = options.details;\n }\n\n static is(error: unknown): error is MigrationToolsError {\n if (!(error instanceof Error)) return false;\n const candidate = error as MigrationToolsError;\n return candidate.name === 'MigrationToolsError' && typeof candidate.code === 'string';\n }\n}\n\nexport function errorDirectoryExists(dir: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.DIR_EXISTS', 'Migration directory already exists', {\n why: `The directory \"${dir}\" already exists. Each migration must have a unique directory.`,\n fix: 'Use --name to pick a different name, or delete the existing directory and re-run.',\n details: { dir },\n });\n}\n\nexport function errorMissingFile(file: string, dir: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.FILE_MISSING', `Missing ${file}`, {\n why: `Expected \"${file}\" in \"${dir}\" but the file does not exist.`,\n fix: reemitHint(\n dir,\n 'or delete the directory if the migration is unwanted and the source TypeScript is gone.',\n ),\n details: { file, dir },\n });\n}\n\nexport function errorInvalidJson(filePath: string, parseError: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_JSON', 'Invalid JSON in migration file', {\n why: `Failed to parse \"${filePath}\": ${parseError}`,\n fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),\n details: { filePath, parseError },\n });\n}\n\nexport function errorInvalidManifest(filePath: string, reason: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_MANIFEST', 'Invalid migration manifest', {\n why: `Migration manifest at \"${filePath}\" is invalid: ${reason}`,\n fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),\n details: { filePath, reason },\n });\n}\n\nexport function errorInvalidSlug(slug: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_NAME', 'Invalid migration name', {\n why: `The slug \"${slug}\" contains no valid characters after sanitization (only a-z, 0-9 are kept).`,\n fix: 'Provide a name with at least one alphanumeric character, e.g. --name add_users.',\n details: { slug },\n });\n}\n\nexport function errorInvalidDestName(destName: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_DEST_NAME', 'Invalid copy destination name', {\n why: `The destination name \"${destName}\" must be a single path segment (no \"..\" or directory separators).`,\n fix: 'Use a simple file name such as \"contract.json\" for each destination in the copy list.',\n details: { destName },\n });\n}\n\nexport function errorSameSourceAndTarget(dir: string, hash: string): MigrationToolsError {\n const dirName = basename(dir);\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: reemitHint(\n dir,\n 'or delete the directory if the migration is unwanted and the source TypeScript is gone.',\n ),\n details: { dirName, hash },\n },\n );\n}\n\nexport function errorAmbiguousTarget(\n branchTips: readonly string[],\n context?: {\n divergencePoint: string;\n branches: readonly {\n tip: string;\n edges: readonly { dirName: string; from: string; to: string }[];\n }[];\n },\n): MigrationToolsError {\n const divergenceInfo = context\n ? `\\nDivergence point: ${context.divergencePoint}\\nBranches:\\n${context.branches.map((b) => ` → ${b.tip} (${b.edges.length} edge(s): ${b.edges.map((e) => e.dirName).join(' → ') || 'direct'})`).join('\\n')}`\n : '';\n return new MigrationToolsError('MIGRATION.AMBIGUOUS_TARGET', 'Ambiguous migration target', {\n why: `The migration history has diverged into multiple branches: ${branchTips.join(', ')}. This typically happens when two developers plan migrations from the same starting point.${divergenceInfo}`,\n fix: 'Use `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 errorDuplicateMigrationHash(migrationHash: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.DUPLICATE_MIGRATION_HASH',\n 'Duplicate migrationHash in migration graph',\n {\n why: `Multiple migrations share migrationHash \"${migrationHash}\". Each migration must have a unique content-addressed identity.`,\n fix: 'Regenerate one of the conflicting migrations so each migrationHash is unique, then re-run migration commands.',\n details: { migrationHash },\n },\n );\n}\n\nexport function errorMigrationHashMismatch(\n dir: string,\n storedHash: string,\n computedHash: string,\n): MigrationToolsError {\n // Render a cwd-relative path in the human-readable diagnostic so users\n // running CLI commands from the project root see a familiar short path.\n // Keep the absolute path in `details.dir` for machine consumers.\n const relativeDir = relative(process.cwd(), dir);\n return new MigrationToolsError('MIGRATION.HASH_MISMATCH', 'Migration package is corrupt', {\n why: `Stored migrationHash \"${storedHash}\" does not match the recomputed hash \"${computedHash}\" for \"${relativeDir}\". The migration.json or ops.json has been edited or partially written since emit.`,\n fix: reemitHint(dir, 'or restore the directory from version control.'),\n details: { dir, storedHash, computedHash },\n });\n}\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
- import { t as EMPTY_CONTRACT_HASH } from "../constants-BRi0X7B_.mjs";
1
+ import { t as EMPTY_CONTRACT_HASH } from "../constants-WVGVMOdu.mjs";
2
2
 
3
3
  export { EMPTY_CONTRACT_HASH };
@@ -1,14 +1,12 @@
1
- import { a as MigrationManifest, i as MigrationHints, n as MigrationChainEntry, o as MigrationOps, r as MigrationGraph, t as MigrationBundle } from "../types-DyGXcWWp.mjs";
2
-
3
1
  //#region src/errors.d.ts
4
-
5
2
  /**
6
3
  * Structured error for migration tooling operations.
7
4
  *
8
5
  * Follows the NAMESPACE.SUBCODE convention from ADR 027. All codes live under
9
- * the MIGRATION namespace. These are tooling-time errors (file I/O, attestation,
10
- * migration history reconstruction), distinct from the runtime MIGRATION.* codes for apply-time
11
- * failures (PRECHECK_FAILED, POSTCHECK_FAILED, etc.).
6
+ * the MIGRATION namespace. These are tooling-time errors (file I/O, hash
7
+ * verification, migration history reconstruction), distinct from the runtime
8
+ * MIGRATION.* codes for apply-time failures (PRECHECK_FAILED, POSTCHECK_FAILED,
9
+ * etc.).
12
10
  *
13
11
  * Fields:
14
12
  * - code: Stable machine-readable code (MIGRATION.SUBCODE)
@@ -31,5 +29,5 @@ declare class MigrationToolsError extends Error {
31
29
  static is(error: unknown): error is MigrationToolsError;
32
30
  }
33
31
  //#endregion
34
- export { type MigrationBundle, type MigrationBundle as MigrationPackage, type MigrationChainEntry, type MigrationGraph, type MigrationHints, type MigrationManifest, type MigrationOps, MigrationToolsError };
35
- //# sourceMappingURL=types.d.mts.map
32
+ export { MigrationToolsError };
33
+ //# sourceMappingURL=errors.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.mts","names":[],"sources":["../../src/errors.ts"],"sourcesContent":[],"mappings":";;AAkCA;;;;;;;;;;;;;;;cAAa,mBAAA,SAA4B,KAAA;;;;;oBAKrB;;;;uBAQK;;sCAWa"}
@@ -0,0 +1,3 @@
1
+ import { t as MigrationToolsError } from "../errors-CZ9JD4sd.mjs";
2
+
3
+ export { MigrationToolsError };
@@ -0,0 +1,2 @@
1
+ import { n as MigrationGraph, t as MigrationEdge } from "../graph-B5wbCSna.mjs";
2
+ export { type MigrationEdge, type MigrationGraph };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,52 @@
1
+ import { n as MigrationMetadata } from "../metadata-DDa5L-uD.mjs";
2
+ import { n as MigrationPackage, t as MigrationOps } from "../package-BJ5KAEcD.mjs";
3
+
4
+ //#region src/hash.d.ts
5
+ interface VerifyResult {
6
+ readonly ok: boolean;
7
+ readonly reason?: 'mismatch';
8
+ readonly storedHash: string;
9
+ readonly computedHash: string;
10
+ }
11
+ /**
12
+ * Content-addressed migration hash over (metadata envelope sans
13
+ * contracts/hints/signature, ops). See ADR 199 — Storage-only migration
14
+ * identity for the rationale: contracts are anchored separately by the
15
+ * storage-hash bookends inside the envelope; planner hints are advisory
16
+ * and must not affect identity.
17
+ *
18
+ * The integrity check is purely structural, not semantic. The function
19
+ * canonicalizes its inputs via `sortKeys` (recursive) + `JSON.stringify`
20
+ * and hashes the result. Target-specific operation payloads (`step.sql`,
21
+ * Mongo's pipeline AST, …) are hashed verbatim — no per-target
22
+ * normalization is required, because what's being verified is "do the
23
+ * on-disk bytes still produce their recorded hash", not "do two
24
+ * semantically-equivalent migrations hash the same". The latter is an
25
+ * emit-drift concern (ADR 192 step 2).
26
+ *
27
+ * The symmetry across write and read holds because `JSON.parse(
28
+ * JSON.stringify(x))` round-trips JSON-safe values losslessly and
29
+ * `sortKeys` is idempotent and deterministic — write-time and read-time
30
+ * canonicalization produce the same canonical bytes regardless of
31
+ * source-side key ordering or whitespace.
32
+ *
33
+ * The `migrationHash` field on the metadata is stripped before hashing
34
+ * so the function can be used both at write time (when no hash exists
35
+ * yet) and at verify time (rehashing an already-attested record).
36
+ */
37
+ declare function computeMigrationHash(metadata: Omit<MigrationMetadata, 'migrationHash'> & {
38
+ readonly migrationHash?: string;
39
+ }, ops: MigrationOps): string;
40
+ /**
41
+ * Re-hash an in-memory migration package and compare against the stored
42
+ * `migrationHash`. See `computeMigrationHash` for the canonicalization rules.
43
+ *
44
+ * Returns `{ ok: true }` when the package is internally consistent, or
45
+ * `{ ok: false, reason: 'mismatch', storedHash, computedHash }` when it is
46
+ * not — typically a sign of FS corruption, partial writes, or a post-emit
47
+ * hand edit.
48
+ */
49
+ declare function verifyMigrationHash(pkg: MigrationPackage): VerifyResult;
50
+ //#endregion
51
+ export { type VerifyResult, computeMigrationHash, verifyMigrationHash };
52
+ //# sourceMappingURL=hash.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.d.mts","names":[],"sources":["../../src/hash.ts"],"sourcesContent":[],"mappings":";;;;UAKiB,YAAA;;EAAA,SAAA,MAAY,CAAA,EAAA,UAAA;EAqCb,SAAA,UAAA,EAAA,MAAoB;EACnB,SAAA,YAAA,EAAA,MAAA;;;;AA8BjB;;;;;;;;;;;;;;;;;;;;;;;;iBA/BgB,oBAAA,WACJ,KAAK;;QACV;;;;;;;;;;iBA6BS,mBAAA,MAAyB,mBAAmB"}
@@ -0,0 +1,3 @@
1
+ import { n as verifyMigrationHash, t as computeMigrationHash } from "../hash-BNWumjn7.mjs";
2
+
3
+ export { computeMigrationHash, verifyMigrationHash };
@@ -1,7 +1,8 @@
1
- import { a as MigrationManifest, o as MigrationOps, t as MigrationBundle } from "../types-DyGXcWWp.mjs";
1
+ import { n as MigrationMetadata } from "../metadata-DDa5L-uD.mjs";
2
+ import { n as MigrationPackage, t as MigrationOps } from "../package-BJ5KAEcD.mjs";
2
3
 
3
4
  //#region src/io.d.ts
4
- declare function writeMigrationPackage(dir: string, manifest: MigrationManifest, ops: MigrationOps): Promise<void>;
5
+ declare function writeMigrationPackage(dir: string, metadata: MigrationMetadata, ops: MigrationOps): Promise<void>;
5
6
  /**
6
7
  * Copy a list of files into `destDir`, optionally renaming each one.
7
8
  *
@@ -17,11 +18,11 @@ declare function copyFilesWithRename(destDir: string, files: readonly {
17
18
  readonly sourcePath: string;
18
19
  readonly destName: string;
19
20
  }[]): Promise<void>;
20
- declare function writeMigrationManifest(dir: string, manifest: MigrationManifest): Promise<void>;
21
+ declare function writeMigrationMetadata(dir: string, metadata: MigrationMetadata): Promise<void>;
21
22
  declare function writeMigrationOps(dir: string, ops: MigrationOps): Promise<void>;
22
- declare function readMigrationPackage(dir: string): Promise<MigrationBundle>;
23
- declare function readMigrationsDir(migrationsRoot: string): Promise<readonly MigrationBundle[]>;
23
+ declare function readMigrationPackage(dir: string): Promise<MigrationPackage>;
24
+ declare function readMigrationsDir(migrationsRoot: string): Promise<readonly MigrationPackage[]>;
24
25
  declare function formatMigrationDirName(timestamp: Date, slug: string): string;
25
26
  //#endregion
26
- export { copyFilesWithRename, formatMigrationDirName, readMigrationPackage, readMigrationsDir, writeMigrationManifest, writeMigrationOps, writeMigrationPackage };
27
+ export { copyFilesWithRename, formatMigrationDirName, readMigrationPackage, readMigrationsDir, writeMigrationMetadata, writeMigrationOps, writeMigrationPackage };
27
28
  //# sourceMappingURL=io.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"io.d.mts","names":[],"sources":["../../src/io.ts"],"sourcesContent":[],"mappings":";;;iBAwDsB,qBAAA,wBAEV,wBACL,eACJ;;AAJH;;;;;AA+BA;AAaA;AAOA;AAIA;AAkEA;AAiCgB,iBA3HM,mBAAA,CA2H4B,OAAI,EAAA,MAAA,EAAA,KAAA,EAAA,SAAA;;;MAxHnD;iBAUmB,sBAAA,wBAEV,oBACT;iBAImB,iBAAA,mBAAoC,eAAe;iBAInD,oBAAA,eAAmC,QAAQ;iBAkE3C,iBAAA,0BAEnB,iBAAiB;iBA+BJ,sBAAA,YAAkC"}
1
+ {"version":3,"file":"io.d.mts","names":[],"sources":["../../src/io.ts"],"sourcesContent":[],"mappings":";;;;iBA2DsB,qBAAA,wBAEV,wBACL,eACJ;;AAJH;;;;;AAiCA;AAaA;AAOA;AAIA;AAyEA;AAiCgB,iBAlIM,mBAAA,CAkI4B,OAAI,EAAA,MAAA,EAAA,KAAA,EAAA,SAAA;;;MA/HnD;iBAUmB,sBAAA,wBAEV,oBACT;iBAImB,iBAAA,mBAAoC,eAAe;iBAInD,oBAAA,eAAmC,QAAQ;iBAyE3C,iBAAA,0BAEnB,iBAAiB;iBA+BJ,sBAAA,YAAkC"}
@@ -1,3 +1,157 @@
1
- import { a as writeMigrationManifest, i as readMigrationsDir, n as formatMigrationDirName, o as writeMigrationOps, r as readMigrationPackage, s as writeMigrationPackage, t as copyFilesWithRename } from "../io-CCnYsUHU.mjs";
1
+ import { a as errorInvalidDestName, d as errorInvalidSlug, f as errorMigrationHashMismatch, o as errorInvalidJson, p as errorMissingFile, r as errorDirectoryExists, s as errorInvalidManifest } from "../errors-CZ9JD4sd.mjs";
2
+ import { n as verifyMigrationHash } from "../hash-BNWumjn7.mjs";
3
+ import { basename, dirname, join } from "pathe";
4
+ import { copyFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
5
+ import { type } from "arktype";
2
6
 
3
- export { copyFilesWithRename, formatMigrationDirName, readMigrationPackage, readMigrationsDir, writeMigrationManifest, writeMigrationOps, writeMigrationPackage };
7
+ //#region src/io.ts
8
+ const MANIFEST_FILE = "migration.json";
9
+ const OPS_FILE = "ops.json";
10
+ const MAX_SLUG_LENGTH = 64;
11
+ function hasErrnoCode(error, code) {
12
+ return error instanceof Error && error.code === code;
13
+ }
14
+ const MigrationMetadataSchema = type({
15
+ from: "string",
16
+ to: "string",
17
+ migrationHash: "string",
18
+ kind: "'regular' | 'baseline'",
19
+ fromContract: "object | null",
20
+ toContract: "object",
21
+ hints: type({
22
+ used: "string[]",
23
+ applied: "string[]",
24
+ plannerVersion: "string"
25
+ }),
26
+ labels: "string[]",
27
+ "authorship?": type({
28
+ "author?": "string",
29
+ "email?": "string"
30
+ }),
31
+ "signature?": type({
32
+ keyId: "string",
33
+ value: "string"
34
+ }).or("null"),
35
+ createdAt: "string"
36
+ });
37
+ const MigrationOpsSchema = type({
38
+ id: "string",
39
+ label: "string",
40
+ operationClass: "'additive' | 'widening' | 'destructive' | 'data'"
41
+ }).array();
42
+ async function writeMigrationPackage(dir, metadata, ops) {
43
+ await mkdir(dirname(dir), { recursive: true });
44
+ try {
45
+ await mkdir(dir);
46
+ } catch (error) {
47
+ if (hasErrnoCode(error, "EEXIST")) throw errorDirectoryExists(dir);
48
+ throw error;
49
+ }
50
+ await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(metadata, null, 2), { flag: "wx" });
51
+ await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: "wx" });
52
+ }
53
+ /**
54
+ * Copy a list of files into `destDir`, optionally renaming each one.
55
+ *
56
+ * The destination directory is created (with `recursive: true`) if it
57
+ * does not already exist. Each source path is copied byte-for-byte into
58
+ * `destDir/<destName>`; missing sources throw `ENOENT`. The helper is
59
+ * intentionally generic: callers own the list of files (e.g. a contract
60
+ * emitter's emitted output) and the naming convention (e.g. renaming
61
+ * the destination contract to `end-contract.*` and the source contract
62
+ * to `start-contract.*`).
63
+ */
64
+ async function copyFilesWithRename(destDir, files) {
65
+ await mkdir(destDir, { recursive: true });
66
+ for (const file of files) {
67
+ if (basename(file.destName) !== file.destName) throw errorInvalidDestName(file.destName);
68
+ await copyFile(file.sourcePath, join(destDir, file.destName));
69
+ }
70
+ }
71
+ async function writeMigrationMetadata(dir, metadata) {
72
+ await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(metadata, null, 2)}\n`);
73
+ }
74
+ async function writeMigrationOps(dir, ops) {
75
+ await writeFile(join(dir, OPS_FILE), `${JSON.stringify(ops, null, 2)}\n`);
76
+ }
77
+ async function readMigrationPackage(dir) {
78
+ const manifestPath = join(dir, MANIFEST_FILE);
79
+ const opsPath = join(dir, OPS_FILE);
80
+ let manifestRaw;
81
+ try {
82
+ manifestRaw = await readFile(manifestPath, "utf-8");
83
+ } catch (error) {
84
+ if (hasErrnoCode(error, "ENOENT")) throw errorMissingFile(MANIFEST_FILE, dir);
85
+ throw error;
86
+ }
87
+ let opsRaw;
88
+ try {
89
+ opsRaw = await readFile(opsPath, "utf-8");
90
+ } catch (error) {
91
+ if (hasErrnoCode(error, "ENOENT")) throw errorMissingFile(OPS_FILE, dir);
92
+ throw error;
93
+ }
94
+ let metadata;
95
+ try {
96
+ metadata = JSON.parse(manifestRaw);
97
+ } catch (e) {
98
+ throw errorInvalidJson(manifestPath, e instanceof Error ? e.message : String(e));
99
+ }
100
+ let ops;
101
+ try {
102
+ ops = JSON.parse(opsRaw);
103
+ } catch (e) {
104
+ throw errorInvalidJson(opsPath, e instanceof Error ? e.message : String(e));
105
+ }
106
+ validateMetadata(metadata, manifestPath);
107
+ validateOps(ops, opsPath);
108
+ const pkg = {
109
+ dirName: basename(dir),
110
+ dirPath: dir,
111
+ metadata,
112
+ ops
113
+ };
114
+ const verification = verifyMigrationHash(pkg);
115
+ if (!verification.ok) throw errorMigrationHashMismatch(dir, verification.storedHash, verification.computedHash);
116
+ return pkg;
117
+ }
118
+ function validateMetadata(metadata, filePath) {
119
+ const result = MigrationMetadataSchema(metadata);
120
+ if (result instanceof type.errors) throw errorInvalidManifest(filePath, result.summary);
121
+ }
122
+ function validateOps(ops, filePath) {
123
+ const result = MigrationOpsSchema(ops);
124
+ if (result instanceof type.errors) throw errorInvalidManifest(filePath, result.summary);
125
+ }
126
+ async function readMigrationsDir(migrationsRoot) {
127
+ let entries;
128
+ try {
129
+ entries = await readdir(migrationsRoot);
130
+ } catch (error) {
131
+ if (hasErrnoCode(error, "ENOENT")) return [];
132
+ throw error;
133
+ }
134
+ const packages = [];
135
+ for (const entry of entries.sort()) {
136
+ const entryPath = join(migrationsRoot, entry);
137
+ if (!(await stat(entryPath)).isDirectory()) continue;
138
+ const manifestPath = join(entryPath, MANIFEST_FILE);
139
+ try {
140
+ await stat(manifestPath);
141
+ } catch {
142
+ continue;
143
+ }
144
+ packages.push(await readMigrationPackage(entryPath));
145
+ }
146
+ return packages;
147
+ }
148
+ function formatMigrationDirName(timestamp, slug) {
149
+ const sanitized = slug.toLowerCase().replace(/[^a-z0-9]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
150
+ if (sanitized.length === 0) throw errorInvalidSlug(slug);
151
+ const truncated = sanitized.slice(0, MAX_SLUG_LENGTH);
152
+ return `${timestamp.getUTCFullYear()}${String(timestamp.getUTCMonth() + 1).padStart(2, "0")}${String(timestamp.getUTCDate()).padStart(2, "0")}T${String(timestamp.getUTCHours()).padStart(2, "0")}${String(timestamp.getUTCMinutes()).padStart(2, "0")}_${truncated}`;
153
+ }
154
+
155
+ //#endregion
156
+ export { copyFilesWithRename, formatMigrationDirName, readMigrationPackage, readMigrationsDir, writeMigrationMetadata, writeMigrationOps, writeMigrationPackage };
157
+ //# sourceMappingURL=io.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"io.mjs","names":["manifestRaw: string","opsRaw: string","metadata: MigrationMetadata","ops: MigrationOps","pkg: MigrationPackage","entries: string[]","packages: MigrationPackage[]"],"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 errorInvalidDestName,\n errorInvalidJson,\n errorInvalidManifest,\n errorInvalidSlug,\n errorMigrationHashMismatch,\n errorMissingFile,\n} from './errors';\nimport { verifyMigrationHash } from './hash';\nimport type { MigrationMetadata } from './metadata';\nimport type { MigrationOps, MigrationPackage } from './package';\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});\n\nconst MigrationMetadataSchema = type({\n from: 'string',\n to: 'string',\n migrationHash: 'string',\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 metadata: MigrationMetadata,\n ops: MigrationOps,\n): Promise<void> {\n await mkdir(dirname(dir), { recursive: true });\n\n try {\n await mkdir(dir);\n } catch (error) {\n if (hasErrnoCode(error, 'EEXIST')) {\n throw errorDirectoryExists(dir);\n }\n throw error;\n }\n\n await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(metadata, null, 2), {\n flag: 'wx',\n });\n await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: 'wx' });\n}\n\n/**\n * Copy a list of files into `destDir`, optionally renaming each one.\n *\n * The destination directory is created (with `recursive: true`) if it\n * does not already exist. Each source path is copied byte-for-byte into\n * `destDir/<destName>`; missing sources throw `ENOENT`. The helper is\n * intentionally generic: callers own the list of files (e.g. a contract\n * emitter's emitted output) and the naming convention (e.g. renaming\n * the destination contract to `end-contract.*` and the source contract\n * to `start-contract.*`).\n */\nexport async function copyFilesWithRename(\n destDir: string,\n files: readonly { readonly sourcePath: string; readonly destName: string }[],\n): Promise<void> {\n await mkdir(destDir, { recursive: true });\n for (const file of files) {\n if (basename(file.destName) !== file.destName) {\n throw errorInvalidDestName(file.destName);\n }\n await copyFile(file.sourcePath, join(destDir, file.destName));\n }\n}\n\nexport async function writeMigrationMetadata(\n dir: string,\n metadata: MigrationMetadata,\n): Promise<void> {\n await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(metadata, null, 2)}\\n`);\n}\n\nexport async function writeMigrationOps(dir: string, ops: MigrationOps): Promise<void> {\n await writeFile(join(dir, OPS_FILE), `${JSON.stringify(ops, null, 2)}\\n`);\n}\n\nexport async function readMigrationPackage(dir: string): Promise<MigrationPackage> {\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 metadata: MigrationMetadata;\n try {\n metadata = JSON.parse(manifestRaw);\n } catch (e) {\n throw errorInvalidJson(manifestPath, e instanceof Error ? e.message : String(e));\n }\n\n let ops: MigrationOps;\n try {\n ops = JSON.parse(opsRaw);\n } catch (e) {\n throw errorInvalidJson(opsPath, e instanceof Error ? e.message : String(e));\n }\n\n validateMetadata(metadata, manifestPath);\n validateOps(ops, opsPath);\n\n const pkg: MigrationPackage = {\n dirName: basename(dir),\n dirPath: dir,\n metadata,\n ops,\n };\n\n const verification = verifyMigrationHash(pkg);\n if (!verification.ok) {\n throw errorMigrationHashMismatch(dir, verification.storedHash, verification.computedHash);\n }\n\n return pkg;\n}\n\nfunction validateMetadata(\n metadata: unknown,\n filePath: string,\n): asserts metadata is MigrationMetadata {\n const result = MigrationMetadataSchema(metadata);\n if (result instanceof type.errors) {\n throw errorInvalidManifest(filePath, result.summary);\n }\n}\n\nfunction validateOps(ops: unknown, filePath: string): asserts ops is MigrationOps {\n const result = MigrationOpsSchema(ops);\n if (result instanceof type.errors) {\n throw errorInvalidManifest(filePath, result.summary);\n }\n}\n\nexport async function readMigrationsDir(\n migrationsRoot: string,\n): Promise<readonly MigrationPackage[]> {\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: MigrationPackage[] = [];\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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"}
@@ -0,0 +1,2 @@
1
+ import { n as MigrationMetadata, t as MigrationHints } from "../metadata-DDa5L-uD.mjs";
2
+ export { type MigrationHints, type MigrationMetadata };
@@ -0,0 +1 @@
1
+ export { };