@prisma-next/migration-tools 0.5.0-dev.66 → 0.5.0-dev.68

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 (98) hide show
  1. package/dist/{constants-B87kJAGj.mjs → constants-DWV9_o2Z.mjs} +2 -2
  2. package/dist/{constants-B87kJAGj.mjs.map → constants-DWV9_o2Z.mjs.map} +1 -1
  3. package/dist/{errors-DQsXvidG.mjs → errors-EPL_9p9f.mjs} +13 -8
  4. package/dist/errors-EPL_9p9f.mjs.map +1 -0
  5. package/dist/exports/aggregate.d.mts +534 -0
  6. package/dist/exports/aggregate.d.mts.map +1 -0
  7. package/dist/exports/aggregate.mjs +598 -0
  8. package/dist/exports/aggregate.mjs.map +1 -0
  9. package/dist/exports/constants.d.mts.map +1 -1
  10. package/dist/exports/constants.mjs +2 -3
  11. package/dist/exports/errors.d.mts +6 -1
  12. package/dist/exports/errors.d.mts.map +1 -1
  13. package/dist/exports/errors.mjs +2 -3
  14. package/dist/exports/graph.d.mts +1 -1
  15. package/dist/exports/graph.mjs +1 -1
  16. package/dist/exports/hash.d.mts +2 -2
  17. package/dist/exports/hash.d.mts.map +1 -1
  18. package/dist/exports/hash.mjs +2 -3
  19. package/dist/exports/invariants.d.mts +14 -4
  20. package/dist/exports/invariants.d.mts.map +1 -1
  21. package/dist/exports/invariants.mjs +2 -4
  22. package/dist/exports/io.d.mts +26 -2
  23. package/dist/exports/io.d.mts.map +1 -1
  24. package/dist/exports/io.mjs +2 -5
  25. package/dist/exports/metadata.d.mts +1 -1
  26. package/dist/exports/metadata.mjs +1 -1
  27. package/dist/exports/migration-graph.d.mts +2 -2
  28. package/dist/exports/migration-graph.d.mts.map +1 -1
  29. package/dist/exports/migration-graph.mjs +1 -525
  30. package/dist/exports/migration-ts.d.mts.map +1 -1
  31. package/dist/exports/migration-ts.mjs +1 -3
  32. package/dist/exports/migration-ts.mjs.map +1 -1
  33. package/dist/exports/migration.d.mts +1 -1
  34. package/dist/exports/migration.d.mts.map +1 -1
  35. package/dist/exports/migration.mjs +5 -6
  36. package/dist/exports/migration.mjs.map +1 -1
  37. package/dist/exports/package.d.mts +1 -1
  38. package/dist/exports/package.mjs +1 -1
  39. package/dist/exports/refs.d.mts.map +1 -1
  40. package/dist/exports/refs.mjs +2 -3
  41. package/dist/exports/refs.mjs.map +1 -1
  42. package/dist/exports/spaces.d.mts +341 -238
  43. package/dist/exports/spaces.d.mts.map +1 -1
  44. package/dist/exports/spaces.mjs +138 -348
  45. package/dist/exports/spaces.mjs.map +1 -1
  46. package/dist/{graph-Czaj8O2q.d.mts → graph-HMWAldoR.d.mts} +1 -1
  47. package/dist/graph-HMWAldoR.d.mts.map +1 -0
  48. package/dist/{hash-G0bAfIGh.mjs → hash-By50zM_E.mjs} +2 -4
  49. package/dist/hash-By50zM_E.mjs.map +1 -0
  50. package/dist/{invariants-4Avb_Yhy.mjs → invariants-Duc8f9NM.mjs} +17 -7
  51. package/dist/invariants-Duc8f9NM.mjs.map +1 -0
  52. package/dist/{io-CDJaWGbt.mjs → io-D13dLvUh.mjs} +46 -14
  53. package/dist/io-D13dLvUh.mjs.map +1 -0
  54. package/dist/migration-graph-DGNnKDY5.mjs +523 -0
  55. package/dist/migration-graph-DGNnKDY5.mjs.map +1 -0
  56. package/dist/{op-schema-BiF1ZYqH.mjs → op-schema-D5qkXfEf.mjs} +2 -3
  57. package/dist/{op-schema-BiF1ZYqH.mjs.map → op-schema-D5qkXfEf.mjs.map} +1 -1
  58. package/dist/{package-B3Yl6DTr.d.mts → package-BjiZ7KDy.d.mts} +1 -1
  59. package/dist/package-BjiZ7KDy.d.mts.map +1 -0
  60. package/dist/read-contract-space-contract-C3-1eyaI.mjs +298 -0
  61. package/dist/read-contract-space-contract-C3-1eyaI.mjs.map +1 -0
  62. package/package.json +13 -9
  63. package/src/aggregate/loader.ts +409 -0
  64. package/src/aggregate/marker-types.ts +16 -0
  65. package/src/aggregate/planner-types.ts +137 -0
  66. package/src/aggregate/planner.ts +158 -0
  67. package/src/aggregate/project-schema-to-space.ts +64 -0
  68. package/src/aggregate/strategies/graph-walk.ts +92 -0
  69. package/src/aggregate/strategies/synth.ts +122 -0
  70. package/src/aggregate/types.ts +89 -0
  71. package/src/aggregate/verifier.ts +230 -0
  72. package/src/assert-descriptor-self-consistency.ts +70 -0
  73. package/src/compute-extension-space-apply-path.ts +152 -0
  74. package/src/concatenate-space-apply-inputs.ts +2 -2
  75. package/src/detect-space-contract-drift.ts +22 -26
  76. package/src/{emit-pinned-space-artefacts.ts → emit-contract-space-artefacts.ts} +14 -33
  77. package/src/errors.ts +11 -5
  78. package/src/exports/aggregate.ts +37 -0
  79. package/src/exports/errors.ts +1 -0
  80. package/src/exports/io.ts +1 -0
  81. package/src/exports/spaces.ts +23 -10
  82. package/src/gather-disk-contract-space-state.ts +62 -0
  83. package/src/invariants.ts +14 -3
  84. package/src/io.ts +42 -0
  85. package/src/plan-all-spaces.ts +3 -7
  86. package/src/read-contract-space-contract.ts +44 -0
  87. package/src/read-contract-space-head-ref.ts +63 -0
  88. package/src/space-layout.ts +4 -11
  89. package/src/verify-contract-spaces.ts +45 -49
  90. package/dist/errors-DQsXvidG.mjs.map +0 -1
  91. package/dist/exports/migration-graph.mjs.map +0 -1
  92. package/dist/graph-Czaj8O2q.d.mts.map +0 -1
  93. package/dist/hash-G0bAfIGh.mjs.map +0 -1
  94. package/dist/invariants-4Avb_Yhy.mjs.map +0 -1
  95. package/dist/io-CDJaWGbt.mjs.map +0 -1
  96. package/dist/package-B3Yl6DTr.d.mts.map +0 -1
  97. package/src/read-pinned-contract-hash.ts +0 -77
  98. /package/dist/{metadata-CSjwljJx.d.mts → metadata-BnLFiI6B.d.mts} +0 -0
@@ -8,7 +8,7 @@ function hasErrnoCode(error: unknown, code: string): boolean {
8
8
  }
9
9
 
10
10
  /**
11
- * List the per-space pinned subdirectories under
11
+ * List the per-space subdirectories under
12
12
  * `<projectRoot>/migrations/`. Returns space-id directory names (sorted
13
13
  * alphabetically) — i.e. any non-dot-prefixed subdirectory whose root
14
14
  * does **not** contain a `migration.json` manifest. The manifest is the
@@ -22,10 +22,8 @@ function hasErrnoCode(error: unknown, code: string): boolean {
22
22
  * Reads only the user's repo. **No descriptor import.** The caller
23
23
  * (verifier) feeds the result into {@link verifyContractSpaces} alongside
24
24
  * the loaded-space set and the marker rows.
25
- *
26
- * @see specs/framework-mechanism.spec.md § 4 — Verifier (steps 5–6).
27
25
  */
28
- export async function listPinnedSpaceDirectories(
26
+ export async function listContractSpaceDirectories(
29
27
  projectMigrationsDir: string,
30
28
  ): Promise<readonly string[]> {
31
29
  let entries: { readonly name: string; readonly isDirectory: boolean }[];
@@ -63,20 +61,20 @@ export async function listPinnedSpaceDirectories(
63
61
  }
64
62
 
65
63
  /**
66
- * Pinned head value (`(hash, invariants)`) for one contract space.
64
+ * On-disk head value (`(hash, invariants)`) for one contract space.
67
65
  * The verifier compares this against the marker row for the same space
68
66
  * to detect drift between the user-emitted artefacts and the live DB
69
67
  * marker.
70
68
  */
71
- export interface SpacePinnedHashRecord {
69
+ export interface ContractSpaceHeadRecord {
72
70
  readonly hash: string;
73
71
  readonly invariants: readonly string[];
74
72
  }
75
73
 
76
74
  /**
77
75
  * Marker row read from `prisma_contract.marker` (one per `space`).
78
- * Caller resolves these via the family runtime's marker reader (T1.1)
79
- * before invoking {@link verifyContractSpaces}.
76
+ * Caller resolves these via the family runtime's marker reader before
77
+ * invoking {@link verifyContractSpaces}.
80
78
  */
81
79
  export interface SpaceMarkerRecord {
82
80
  readonly hash: string;
@@ -94,20 +92,20 @@ export interface VerifyContractSpacesInputs {
94
92
  readonly loadedSpaces: ReadonlySet<string>;
95
93
 
96
94
  /**
97
- * Pinned per-space subdirectories observed under
95
+ * Per-space subdirectories observed under
98
96
  * `<projectRoot>/migrations/`. Resolved via
99
- * {@link listPinnedSpaceDirectories}.
97
+ * {@link listContractSpaceDirectories}.
100
98
  */
101
- readonly pinnedDirsOnDisk: readonly string[];
99
+ readonly spaceDirsOnDisk: readonly string[];
102
100
 
103
101
  /**
104
- * Pinned head ref per space, keyed by space id. Caller reads
102
+ * Head ref per space, keyed by space id. Caller reads
105
103
  * `<projectRoot>/migrations/<space-id>/contract.json` and
106
- * `refs/head.json` (or, for app-space if its pinned shape ever moves
107
- * under `migrations/`, the equivalent files) to construct this map.
108
- * Spaces with no pinned dir on disk simply omit a map entry.
104
+ * `<projectRoot>/migrations/<space-id>/refs/head.json` to construct
105
+ * this map. Spaces with no contract-space dir on disk simply omit a
106
+ * map entry.
109
107
  */
110
- readonly pinnedHashesBySpace: ReadonlyMap<string, SpacePinnedHashRecord>;
108
+ readonly headRefsBySpace: ReadonlyMap<string, ContractSpaceHeadRecord>;
111
109
 
112
110
  /**
113
111
  * Marker rows keyed by `space`. Caller reads them from the
@@ -128,21 +126,21 @@ export type SpaceVerifierViolation =
128
126
  readonly remediation: string;
129
127
  }
130
128
  | {
131
- readonly kind: 'orphanPinnedDir';
129
+ readonly kind: 'orphanSpaceDir';
132
130
  readonly spaceId: string;
133
131
  readonly remediation: string;
134
132
  }
135
133
  | {
136
134
  readonly kind: 'hashMismatch';
137
135
  readonly spaceId: string;
138
- readonly pinnedHash: string;
136
+ readonly priorHeadHash: string;
139
137
  readonly markerHash: string;
140
138
  readonly remediation: string;
141
139
  }
142
140
  | {
143
141
  readonly kind: 'invariantsMismatch';
144
142
  readonly spaceId: string;
145
- readonly pinnedInvariants: readonly string[];
143
+ readonly onDiskInvariants: readonly string[];
146
144
  readonly markerInvariants: readonly string[];
147
145
  readonly remediation: string;
148
146
  };
@@ -153,36 +151,34 @@ export type VerifyContractSpacesResult =
153
151
 
154
152
  /**
155
153
  * Pure structural verifier for the per-space mechanism. Aggregates the
156
- * three orphan / missing checks (FR6 cases a–c) plus per-space hash and
157
- * invariant comparison.
154
+ * three orphan / missing checks plus per-space hash and invariant
155
+ * comparison.
158
156
  *
159
- * Algorithm (sub-spec § 4):
157
+ * Algorithm:
160
158
  *
161
159
  * - For every extension space declared in `loadedSpaces` (`'app'`
162
- * excluded — its pinned `contract.json` lives at the project root):
163
- * - If no pinned dir on disk → `declaredButUnmigrated`.
160
+ * excluded — the per-space verifier is scoped to extension members;
161
+ * the app is verified through the aggregate path):
162
+ * - If no contract-space dir on disk → `declaredButUnmigrated`.
164
163
  * - Else if `markerRowsBySpace` lacks an entry → no violation here;
165
- * the live-DB compare in step 8 (out of scope of this helper) is
166
- * where the absence shows up.
167
- * - Else compare marker hash / invariants vs. pinned hash /
164
+ * the live-DB compare done outside this helper is where the
165
+ * absence shows up.
166
+ * - Else compare marker hash / invariants vs. on-disk head hash /
168
167
  * invariants → `hashMismatch` / `invariantsMismatch` on drift.
169
- * - For every pinned dir on disk that is not in `loadedSpaces` →
170
- * `orphanPinnedDir`.
168
+ * - For every contract-space dir on disk that is not in `loadedSpaces` →
169
+ * `orphanSpaceDir`.
171
170
  * - For every marker row whose `space` is not in `loadedSpaces` →
172
171
  * `orphanMarker`. The app-space marker is always loaded (`'app'` is
173
172
  * in `loadedSpaces` by definition).
174
173
  *
175
- * Output is deterministic (NFR6): violations are sorted first by `kind`
176
- * (`declaredButUnmigrated` → `orphanMarker` → `orphanPinnedDir` →
174
+ * Output is deterministic: violations are sorted first by `kind`
175
+ * (`declaredButUnmigrated` → `orphanMarker` → `orphanSpaceDir` →
177
176
  * `hashMismatch` → `invariantsMismatch`) then by `spaceId`. Two callers
178
177
  * passing equivalent inputs see byte-identical violation lists.
179
178
  *
180
179
  * Synchronous, pure, no I/O. **Does not import the extension descriptor**
181
- * (the inputs are pre-resolved by the caller). This is the property
182
- * AC-15 / AC-26 ("verifier reads only the user repo, not
183
- * `node_modules`") locks in.
184
- *
185
- * @see specs/framework-mechanism.spec.md § 4 — Verifier (T1.5).
180
+ * (the inputs are pre-resolved by the caller); the verifier reads only
181
+ * the user repo, not `node_modules`.
186
182
  */
187
183
  export function verifyContractSpaces(
188
184
  inputs: VerifyContractSpacesInputs,
@@ -192,7 +188,7 @@ export function verifyContractSpaces(
192
188
  for (const spaceId of [...inputs.loadedSpaces].sort()) {
193
189
  if (spaceId === APP_SPACE_ID) continue;
194
190
 
195
- if (!inputs.pinnedDirsOnDisk.includes(spaceId)) {
191
+ if (!inputs.spaceDirsOnDisk.includes(spaceId)) {
196
192
  violations.push({
197
193
  kind: 'declaredButUnmigrated',
198
194
  spaceId,
@@ -201,43 +197,43 @@ export function verifyContractSpaces(
201
197
  continue;
202
198
  }
203
199
 
204
- const pinned = inputs.pinnedHashesBySpace.get(spaceId);
200
+ const head = inputs.headRefsBySpace.get(spaceId);
205
201
  const marker = inputs.markerRowsBySpace.get(spaceId);
206
- if (!pinned || !marker) {
202
+ if (!head || !marker) {
207
203
  continue;
208
204
  }
209
205
 
210
- if (pinned.hash !== marker.hash) {
206
+ if (head.hash !== marker.hash) {
211
207
  violations.push({
212
208
  kind: 'hashMismatch',
213
209
  spaceId,
214
- pinnedHash: pinned.hash,
210
+ priorHeadHash: head.hash,
215
211
  markerHash: marker.hash,
216
- remediation: `Marker row for space '${spaceId}' is keyed at ${marker.hash}, but the pinned ${join('migrations', spaceId, 'contract.json')} resolves to ${pinned.hash}. Run \`prisma-next db update\` to advance the database, or \`prisma-next migrate\` if the descriptor was bumped without re-emitting.`,
212
+ remediation: `Marker row for space '${spaceId}' is keyed at ${marker.hash}, but the on-disk ${join('migrations', spaceId, 'contract.json')} resolves to ${head.hash}. Run \`prisma-next db update\` to advance the database, or \`prisma-next migrate\` if the descriptor was bumped without re-emitting.`,
217
213
  });
218
214
  continue;
219
215
  }
220
216
 
221
- const pinnedInvariants = [...pinned.invariants].sort();
217
+ const onDiskInvariants = [...head.invariants].sort();
222
218
  const markerInvariants = new Set(marker.invariants);
223
- const missing = pinnedInvariants.filter((id) => !markerInvariants.has(id));
219
+ const missing = onDiskInvariants.filter((id) => !markerInvariants.has(id));
224
220
  if (missing.length > 0) {
225
221
  violations.push({
226
222
  kind: 'invariantsMismatch',
227
223
  spaceId,
228
- pinnedInvariants,
224
+ onDiskInvariants,
229
225
  markerInvariants: [...marker.invariants].sort(),
230
226
  remediation: `Marker row for space '${spaceId}' is missing invariants [${missing.map((s) => JSON.stringify(s)).join(', ')}]. Run \`prisma-next db update\` to apply the corresponding data-transform migrations.`,
231
227
  });
232
228
  }
233
229
  }
234
230
 
235
- for (const dir of [...inputs.pinnedDirsOnDisk].sort()) {
231
+ for (const dir of [...inputs.spaceDirsOnDisk].sort()) {
236
232
  if (!inputs.loadedSpaces.has(dir)) {
237
233
  violations.push({
238
- kind: 'orphanPinnedDir',
234
+ kind: 'orphanSpaceDir',
239
235
  spaceId: dir,
240
- remediation: `Orphan pinned directory \`${join('migrations', dir)}/\` for an extension not in extensionPacks; remove the directory or re-add the extension.`,
236
+ remediation: `Orphan contract-space directory \`${join('migrations', dir)}/\` for an extension not in extensionPacks; remove the directory or re-add the extension.`,
241
237
  });
242
238
  }
243
239
  }
@@ -259,7 +255,7 @@ export function verifyContractSpaces(
259
255
  const kindOrder: Record<SpaceVerifierViolation['kind'], number> = {
260
256
  declaredButUnmigrated: 0,
261
257
  orphanMarker: 1,
262
- orphanPinnedDir: 2,
258
+ orphanSpaceDir: 2,
263
259
  hashMismatch: 3,
264
260
  invariantsMismatch: 4,
265
261
  };
@@ -1 +0,0 @@
1
- {"version":3,"file":"errors-DQsXvidG.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["import { ifDefined } from '@prisma-next/utils/defined';\nimport { basename, dirname, relative } from 'pathe';\n\n/**\n * Build the canonical \"re-emit this package\" remediation hint.\n *\n * Every on-disk migration package ships its own `migration.ts` author-time\n * file. Running it regenerates `migration.json` and `ops.json` with the\n * correct hash + metadata, so it is the right primitive whenever a single\n * package's on-disk artifacts are missing, malformed, or otherwise corrupt.\n * Pointing users at `migration plan` would emit a *new* package rather than\n * heal the broken one.\n */\nfunction reemitHint(dir: string, fallback?: string): string {\n const relativeDir = relative(process.cwd(), dir);\n const reemit = `Re-emit the package by running \\`node \"${relativeDir}/migration.ts\"\\``;\n return fallback ? `${reemit}, ${fallback}` : `${reemit}.`;\n}\n\n/**\n * Structured error for migration tooling operations.\n *\n * Follows the NAMESPACE.SUBCODE convention from ADR 027. All codes live under\n * the MIGRATION namespace. These are tooling-time errors (file I/O, hash\n * verification, migration history reconstruction), distinct from the runtime\n * MIGRATION.* codes for apply-time failures (PRECHECK_FAILED, POSTCHECK_FAILED,\n * etc.).\n *\n * Fields:\n * - code: Stable machine-readable code (MIGRATION.SUBCODE)\n * - category: Always 'MIGRATION'\n * - why: Explains the cause in plain language\n * - fix: Actionable remediation step\n * - details: Machine-readable structured data for agents\n */\nexport class MigrationToolsError extends Error {\n readonly code: string;\n readonly category = 'MIGRATION' as const;\n readonly why: string;\n readonly fix: string;\n readonly details: Record<string, unknown> | undefined;\n\n constructor(\n code: string,\n summary: string,\n options: {\n readonly why: string;\n readonly fix: string;\n readonly details?: Record<string, unknown>;\n },\n ) {\n super(summary);\n this.name = 'MigrationToolsError';\n this.code = code;\n this.why = options.why;\n this.fix = options.fix;\n this.details = options.details;\n }\n\n static is(error: unknown): error is MigrationToolsError {\n if (!(error instanceof Error)) return false;\n const candidate = error as MigrationToolsError;\n return candidate.name === 'MigrationToolsError' && typeof candidate.code === 'string';\n }\n}\n\nexport function errorDirectoryExists(dir: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.DIR_EXISTS', 'Migration directory already exists', {\n why: `The directory \"${dir}\" already exists. Each migration must have a unique directory.`,\n fix: 'Use --name to pick a different name, or delete the existing directory and re-run.',\n details: { dir },\n });\n}\n\nexport function errorMissingFile(file: string, dir: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.FILE_MISSING', `Missing ${file}`, {\n why: `Expected \"${file}\" in \"${dir}\" but the file does not exist.`,\n fix: reemitHint(\n dir,\n 'or delete the directory if the migration is unwanted and the source TypeScript is gone.',\n ),\n details: { file, dir },\n });\n}\n\nexport function errorInvalidJson(filePath: string, parseError: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_JSON', 'Invalid JSON in migration file', {\n why: `Failed to parse \"${filePath}\": ${parseError}`,\n fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),\n details: { filePath, parseError },\n });\n}\n\nexport function errorInvalidManifest(filePath: string, reason: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_MANIFEST', 'Invalid migration manifest', {\n why: `Migration manifest at \"${filePath}\" is invalid: ${reason}`,\n fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),\n details: { filePath, reason },\n });\n}\n\nexport function errorInvalidOperationEntry(index: number, reason: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.INVALID_OPERATION_ENTRY',\n 'Migration operation entry is malformed',\n {\n why: `Operation at index ${index} returned by the migration class failed schema validation: ${reason}.`,\n fix: \"Update the migration class so each entry of `operations` carries `id` (string), `label` (string), and `operationClass` (one of 'additive' | 'widening' | 'destructive' | 'data').\",\n details: { index, reason },\n },\n );\n}\n\nexport function errorStaleContractBookends(args: {\n readonly side: 'from' | 'to';\n readonly metaHash: string | null;\n readonly contractHash: string;\n}): MigrationToolsError {\n const { side, metaHash, contractHash } = args;\n // `meta.from` is `string | null` (null = baseline). Render `null` as a\n // human-readable token in the diagnostic so the message stays clear when\n // the mismatch is a baseline-vs-non-baseline disagreement.\n const renderedMetaHash = metaHash === null ? 'null (baseline)' : `\"${metaHash}\"`;\n return new MigrationToolsError(\n 'MIGRATION.STALE_CONTRACT_BOOKENDS',\n 'Migration manifest contract bookends disagree with describe()',\n {\n why: `migration.json stores ${side}Contract.storage.storageHash \"${contractHash}\", but describe() returned meta.${side} = ${renderedMetaHash}. The bookend is stale — most likely the migration's describe() was edited after the package was scaffolded by \\`migration plan\\`.`,\n fix: 'Re-run `migration plan` to regenerate the package with fresh contract bookends, or restore the directory from version control.',\n details: { side, metaHash, contractHash },\n },\n );\n}\n\nexport function errorInvalidSlug(slug: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_NAME', 'Invalid migration name', {\n why: `The slug \"${slug}\" contains no valid characters after sanitization (only a-z, 0-9 are kept).`,\n fix: 'Provide a name with at least one alphanumeric character, e.g. --name add_users.',\n details: { slug },\n });\n}\n\nexport function errorInvalidDestName(destName: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_DEST_NAME', 'Invalid copy destination name', {\n why: `The destination name \"${destName}\" must be a single path segment (no \"..\" or directory separators).`,\n fix: 'Use a simple file name such as \"contract.json\" for each destination in the copy list.',\n details: { destName },\n });\n}\n\nexport function errorInvalidSpaceId(spaceId: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.INVALID_SPACE_ID',\n 'Invalid contract space identifier',\n {\n why: `The space id \"${spaceId}\" does not match the required pattern /^[a-z][a-z0-9_-]{0,63}$/. Space ids are used as filesystem directory names under \\`migrations/\\`, so the pattern is conservative on purpose.`,\n fix: 'Pick a lowercase identifier that begins with a letter and contains only lowercase letters, digits, hyphens, or underscores; max 64 characters total.',\n details: { spaceId },\n },\n );\n}\n\nexport function errorPinnedArtefactsAppSpace(): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.PINNED_ARTEFACTS_APP_SPACE',\n 'Pinned per-space artefacts do not apply to the app space',\n {\n why: \"Pinned `contract.json`/`contract.d.ts`/`refs/head.json` files only exist for extension spaces under `migrations/<space-id>/`. The app space's canonical contract lives at the project root (`contract.json`) — `emitPinnedSpaceArtefacts` is the wrong helper for it.\",\n fix: 'Pass an extension space id, or use the app-space contract emit pipeline for the project-root `contract.json` / `contract.d.ts`.',\n },\n );\n}\n\nexport function errorDuplicateSpaceId(spaceId: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.DUPLICATE_SPACE_ID',\n 'Duplicate contract space identifier',\n {\n why: `The space id \"${spaceId}\" appears more than once in the per-space planner input. Each space id must be unique across the inputs (the per-space planner emits one output entry per id).`,\n fix: 'Deduplicate the inputs before passing them to `planAllSpaces` — typically by checking your `extensionPacks` declaration for repeated entries.',\n details: { spaceId },\n },\n );\n}\n\nexport function errorSameSourceAndTarget(dir: string, hash: string): MigrationToolsError {\n const dirName = basename(dir);\n return new MigrationToolsError(\n 'MIGRATION.SAME_SOURCE_AND_TARGET',\n 'Migration without data-transform operations has same source and target',\n {\n why: `Migration \"${dirName}\" has from === to === \"${hash}\" and declares no data-transform operations. Self-edges are only allowed when the migration runs at least one dataTransform — otherwise the migration is a no-op.`,\n fix: reemitHint(\n dir,\n 'and either change the contract so from ≠ to, add a dataTransform op, or delete the directory if the migration is unwanted.',\n ),\n details: { dirName, hash },\n },\n );\n}\n\nexport function errorAmbiguousTarget(\n branchTips: readonly string[],\n context?: {\n divergencePoint: string;\n branches: readonly {\n tip: string;\n edges: readonly { dirName: string; from: string; to: string }[];\n }[];\n },\n): MigrationToolsError {\n const divergenceInfo = context\n ? `\\nDivergence point: ${context.divergencePoint}\\nBranches:\\n${context.branches.map((b) => ` → ${b.tip} (${b.edges.length} edge(s): ${b.edges.map((e) => e.dirName).join(' → ') || 'direct'})`).join('\\n')}`\n : '';\n return new MigrationToolsError('MIGRATION.AMBIGUOUS_TARGET', 'Ambiguous migration target', {\n why: `The migration history has diverged into multiple branches: ${branchTips.join(', ')}. This typically happens when two developers plan migrations from the same starting point.${divergenceInfo}`,\n fix: 'Use `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 errorInvalidInvariantId(invariantId: string): MigrationToolsError {\n return new MigrationToolsError('MIGRATION.INVALID_INVARIANT_ID', 'Invalid invariantId', {\n why: `invariantId ${JSON.stringify(invariantId)} is invalid. Ids must be non-empty and contain no whitespace or control characters (including Unicode whitespace like NBSP); other content (kebab-case, camelCase, namespaced, Unicode letters) is allowed.`,\n fix: 'Pick an invariantId without spaces, tabs, newlines, or control characters — e.g. \"backfill-user-phone\", \"users/backfill-phone\", or \"BackfillUserPhone\".',\n details: { invariantId },\n });\n}\n\nexport function errorDuplicateInvariantInEdge(invariantId: string): MigrationToolsError {\n return new MigrationToolsError(\n 'MIGRATION.DUPLICATE_INVARIANT_IN_EDGE',\n 'Duplicate invariantId on a single migration',\n {\n why: `invariantId \"${invariantId}\" is declared by more than one dataTransform on the same migration. The marker stores invariants as a set and the routing layer treats them as edge-level, so two ops cannot share a routing identity.`,\n fix: 'Rename one of the conflicting dataTransform invariantIds, or drop invariantId on the op that does not need to be routing-visible.',\n details: { invariantId },\n },\n );\n}\n\nexport function errorProvidedInvariantsMismatch(\n filePath: string,\n stored: readonly string[],\n derived: readonly string[],\n): MigrationToolsError {\n const storedSet = new Set(stored);\n const derivedSet = new Set(derived);\n const missing = [...derivedSet].filter((id) => !storedSet.has(id));\n const extra = [...storedSet].filter((id) => !derivedSet.has(id));\n // When sets agree but arrays don't, the only difference is ordering — call\n // it out so the reader doesn't stare at two visually-identical arrays.\n // Canonical providedInvariants is sorted ascending; a manifest with the\n // same ids in a different order is still a mismatch (the hash check would\n // also fail), but the human-readable diagnostic is otherwise unhelpful.\n const orderingOnly = missing.length === 0 && extra.length === 0;\n const why = orderingOnly\n ? `migration.json at \"${filePath}\" stores providedInvariants ${JSON.stringify(stored)}, but the canonical value derived from ops.json is ${JSON.stringify(derived)} — same ids, different order. Canonical providedInvariants is sorted ascending.`\n : `migration.json at \"${filePath}\" stores providedInvariants ${JSON.stringify(stored)}, but the value derived from ops.json is ${JSON.stringify(derived)}. The manifest copy was likely hand-edited without re-emitting.`;\n return new MigrationToolsError(\n 'MIGRATION.PROVIDED_INVARIANTS_MISMATCH',\n 'providedInvariants on migration.json disagrees with ops.json',\n {\n why,\n fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),\n details: { filePath, stored, derived, difference: { missing, extra } },\n },\n );\n}\n\n/**\n * Wire-shape edge surfaced through the JSON envelope's\n * `meta.structuralPath` of `MIGRATION.NO_INVARIANT_PATH`. Slim by design —\n * authoring metadata (`createdAt`, `labels`) lives on `MigrationEdge` but\n * is intentionally dropped here so the envelope stays stable across\n * graph-internal refactors.\n *\n * Stability: any field added here is part of the public CLI JSON contract.\n * Callers (CLI consumers, agents) must be able to treat\n * `(dirName, migrationHash, from, to, invariants)` as the canonical shape.\n */\nexport interface NoInvariantPathStructuralEdge {\n readonly dirName: string;\n readonly migrationHash: string;\n readonly from: string;\n readonly to: string;\n readonly invariants: readonly string[];\n}\n\nexport function errorNoInvariantPath(args: {\n readonly refName?: string;\n readonly required: readonly string[];\n readonly missing: readonly string[];\n readonly structuralPath: readonly NoInvariantPathStructuralEdge[];\n}): MigrationToolsError {\n const { refName, required, missing, structuralPath } = args;\n const refClause = refName ? `Ref \"${refName}\"` : 'Target';\n const missingList = missing.map((id) => JSON.stringify(id)).join(', ');\n const requiredList = required.map((id) => JSON.stringify(id)).join(', ');\n return new MigrationToolsError(\n 'MIGRATION.NO_INVARIANT_PATH',\n 'No path covers the required invariants',\n {\n why: `${refClause} requires invariants the reachable path doesn't cover. required=[${requiredList}], missing=[${missingList}].`,\n fix: 'Add a migration on the path that runs `dataTransform({ invariantId: \"<id>\", … })` for each missing invariant, or retarget the ref to a hash whose path already provides them.',\n details: {\n required,\n missing,\n structuralPath,\n ...ifDefined('refName', refName),\n },\n },\n );\n}\n\nexport function errorUnknownInvariant(args: {\n readonly refName?: string;\n readonly unknown: readonly string[];\n readonly declared: readonly string[];\n}): MigrationToolsError {\n const { refName, unknown, declared } = args;\n const refClause = refName ? `Ref \"${refName}\" declares` : 'Declares';\n const unknownList = unknown.map((id) => JSON.stringify(id)).join(', ');\n return new MigrationToolsError(\n 'MIGRATION.UNKNOWN_INVARIANT',\n 'Ref declares invariants no migration in the graph provides',\n {\n why: `${refClause} invariants no migration in the graph provides. unknown=[${unknownList}].`,\n fix: 'Either the ref has a typo, or the declaring migration has not been authored/attested yet. Re-check the ref file and the migrations directory.',\n details: {\n unknown,\n declared,\n ...ifDefined('refName', refName),\n },\n },\n );\n}\n\nexport function errorMigrationHashMismatch(\n dir: string,\n storedHash: string,\n computedHash: string,\n): MigrationToolsError {\n // Render a cwd-relative path in the human-readable diagnostic so users\n // running CLI commands from the project root see a familiar short path.\n // Keep the absolute path in `details.dir` for machine consumers.\n const relativeDir = relative(process.cwd(), dir);\n return new MigrationToolsError('MIGRATION.HASH_MISMATCH', 'Migration package is corrupt', {\n why: `Stored migrationHash \"${storedHash}\" does not match the recomputed hash \"${computedHash}\" for \"${relativeDir}\". The migration.json or ops.json has been edited or partially written since emit.`,\n fix: reemitHint(dir, 'or restore the directory from version control.'),\n details: { dir, storedHash, computedHash },\n });\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"migration-graph.mjs","names":["migration: MigrationEdge","LABEL_PRIORITY: Record<string, number>","path: MigrationEdge[]","next: Set<string> | null","cur: string | undefined","tieBreakReasons: string[]","comparisonPool: readonly MigrationEdge[]","prefixes: ReadonlySet<string>[]","leaves: string[]","cycles: string[][]","stack: Frame[]","cycle: string[]","startNodes: string[]","orphans: MigrationEdge[]"],"sources":["../../src/queue.ts","../../src/graph-ops.ts","../../src/migration-graph.ts"],"sourcesContent":["/**\n * FIFO queue with amortised O(1) push and shift.\n *\n * Uses a head-index cursor over a backing array rather than\n * `Array.prototype.shift()`, which is O(n) on V8. Intended for BFS-shaped\n * traversals where the queue is drained in a single pass — it does not\n * reclaim memory for already-shifted items, so it is not suitable for\n * long-lived queues with many push/shift cycles.\n */\nexport class Queue<T> {\n private readonly items: T[];\n private head = 0;\n\n constructor(initial: Iterable<T> = []) {\n this.items = [...initial];\n }\n\n push(item: T): void {\n this.items.push(item);\n }\n\n /**\n * Remove and return the next item. Caller must check `isEmpty` first —\n * shifting an empty queue throws.\n */\n shift(): T {\n if (this.head >= this.items.length) {\n throw new Error('Queue.shift called on empty queue');\n }\n // biome-ignore lint/style/noNonNullAssertion: bounds-checked on the line above\n return this.items[this.head++]!;\n }\n\n get isEmpty(): boolean {\n return this.head >= this.items.length;\n }\n}\n","import { Queue } from './queue';\n\n/**\n * One step of a BFS traversal.\n *\n * `parent` and `incomingEdge` are `null` for start states — they were not\n * reached via any edge. For every other state they record the predecessor\n * state and the edge by which this state was first reached.\n *\n * `state` is the BFS state, most often a string (graph node identifier) but\n * can be a composite object. The string overload keeps the common case\n * ergonomic; the generic overload accepts a caller-supplied `key` function\n * that produces a stable equality key for dedup.\n */\nexport interface BfsStep<S, E> {\n readonly state: S;\n readonly parent: S | null;\n readonly incomingEdge: E | null;\n}\n\n/**\n * Generic breadth-first traversal.\n *\n * Direction (forward/reverse) is expressed by the caller's `neighbours`\n * closure: return `{ next, edge }` pairs where `next` is the state to visit\n * next and `edge` is the edge that connects them. Callers that don't need\n * path reconstruction can ignore the `parent`/`incomingEdge` fields of each\n * yielded step.\n *\n * Ordering — when the result needs to be deterministic (path-finding) the\n * caller is responsible for sorting inside `neighbours`; this generator\n * does not impose an ordering hook of its own. State-dependent orderings\n * have full access to the source state inside the closure.\n *\n * Stops are intrinsic — callers `break` out of the `for..of` loop when\n * they've found what they're looking for.\n */\nexport function bfs<E>(\n starts: Iterable<string>,\n neighbours: (state: string) => Iterable<{ next: string; edge: E }>,\n): Generator<BfsStep<string, E>>;\nexport function bfs<S, E>(\n starts: Iterable<S>,\n neighbours: (state: S) => Iterable<{ next: S; edge: E }>,\n key: (state: S) => string,\n): Generator<BfsStep<S, E>>;\nexport function* bfs<S, E>(\n starts: Iterable<S>,\n neighbours: (state: S) => Iterable<{ next: S; edge: E }>,\n // Identity default for the string overload. TypeScript can't express\n // \"default applies only when S = string\", so this cast bridges the\n // generic implementation signature to the public overloads — which\n // guarantee `key` is omitted only when S = string at the call site.\n key: (state: S) => string = (state) => state as unknown as string,\n): Generator<BfsStep<S, E>> {\n // Queue entries carry the state alongside its key so we don't recompute\n // key() twice per visit (once on dedup, once on parent lookup). Composite\n // keys can be non-trivial to compute; string-overload callers pay nothing\n // since key() is identity there.\n interface Entry {\n readonly state: S;\n readonly key: string;\n }\n const visited = new Set<string>();\n const parentMap = new Map<string, { parent: S; edge: E }>();\n const queue = new Queue<Entry>();\n for (const start of starts) {\n const k = key(start);\n if (!visited.has(k)) {\n visited.add(k);\n queue.push({ state: start, key: k });\n }\n }\n while (!queue.isEmpty) {\n const { state: current, key: curKey } = queue.shift();\n const parentInfo = parentMap.get(curKey);\n yield {\n state: current,\n parent: parentInfo?.parent ?? null,\n incomingEdge: parentInfo?.edge ?? null,\n };\n\n for (const { next, edge } of neighbours(current)) {\n const k = key(next);\n if (!visited.has(k)) {\n visited.add(k);\n parentMap.set(k, { parent: current, edge });\n queue.push({ state: next, key: k });\n }\n }\n }\n}\n","import { ifDefined } from '@prisma-next/utils/defined';\nimport { EMPTY_CONTRACT_HASH } from './constants';\nimport {\n errorAmbiguousTarget,\n errorDuplicateMigrationHash,\n errorNoInitialMigration,\n errorNoTarget,\n errorSameSourceAndTarget,\n} from './errors';\nimport type { MigrationEdge, MigrationGraph } from './graph';\nimport { bfs } from './graph-ops';\nimport type { OnDiskMigrationPackage } from './package';\n\n/** Forward-edge neighbours: edge `e` from `n` visits `e.to` next. */\nfunction forwardNeighbours(graph: MigrationGraph, node: string) {\n return (graph.forwardChain.get(node) ?? []).map((edge) => ({ next: edge.to, edge }));\n}\n\n/**\n * Forward-edge neighbours, sorted by the deterministic tie-break.\n * Used by path-finding so the resulting shortest path is stable across runs.\n */\nfunction sortedForwardNeighbours(graph: MigrationGraph, node: string) {\n const edges = graph.forwardChain.get(node) ?? [];\n return [...edges].sort(compareTieBreak).map((edge) => ({ next: edge.to, edge }));\n}\n\n/** Reverse-edge neighbours: edge `e` from `n` visits `e.from` next. */\nfunction reverseNeighbours(graph: MigrationGraph, node: string) {\n return (graph.reverseChain.get(node) ?? []).map((edge) => ({ next: edge.from, edge }));\n}\n\nfunction appendEdge(map: Map<string, MigrationEdge[]>, key: string, entry: MigrationEdge): void {\n const bucket = map.get(key);\n if (bucket) bucket.push(entry);\n else map.set(key, [entry]);\n}\n\nexport function reconstructGraph(packages: readonly OnDiskMigrationPackage[]): MigrationGraph {\n const nodes = new Set<string>();\n const forwardChain = new Map<string, MigrationEdge[]>();\n const reverseChain = new Map<string, MigrationEdge[]>();\n const migrationByHash = new Map<string, MigrationEdge>();\n\n for (const pkg of packages) {\n // Manifest `from` is `string | null` (null = baseline). The graph layer\n // is the marker/path layer where \"no prior state\" is encoded as the\n // EMPTY_CONTRACT_HASH sentinel; bridge here so pathfinding stays string-\n // keyed.\n const from = pkg.metadata.from ?? EMPTY_CONTRACT_HASH;\n const { to } = pkg.metadata;\n\n if (from === to) {\n const hasDataOp = pkg.ops.some((op) => op.operationClass === 'data');\n if (!hasDataOp) {\n throw errorSameSourceAndTarget(pkg.dirPath, from);\n }\n }\n\n nodes.add(from);\n nodes.add(to);\n\n const migration: MigrationEdge = {\n from,\n to,\n migrationHash: pkg.metadata.migrationHash,\n dirName: pkg.dirName,\n createdAt: pkg.metadata.createdAt,\n labels: pkg.metadata.labels,\n invariants: pkg.metadata.providedInvariants,\n };\n\n if (migrationByHash.has(migration.migrationHash)) {\n throw errorDuplicateMigrationHash(migration.migrationHash);\n }\n migrationByHash.set(migration.migrationHash, migration);\n\n appendEdge(forwardChain, from, migration);\n appendEdge(reverseChain, to, migration);\n }\n\n return { nodes, forwardChain, reverseChain, migrationByHash };\n}\n\n// ---------------------------------------------------------------------------\n// Deterministic tie-breaking for BFS neighbour order.\n// Used by path-finders only; not a general-purpose utility.\n// Ordering: label priority → createdAt → to → migrationHash.\n// ---------------------------------------------------------------------------\n\nconst LABEL_PRIORITY: Record<string, number> = { main: 0, default: 1, feature: 2 };\n\nfunction labelPriority(labels: readonly string[]): number {\n let best = 3;\n for (const l of labels) {\n const p = LABEL_PRIORITY[l];\n if (p !== undefined && p < best) best = p;\n }\n return best;\n}\n\nfunction compareTieBreak(a: MigrationEdge, b: MigrationEdge): number {\n const lp = labelPriority(a.labels) - labelPriority(b.labels);\n if (lp !== 0) return lp;\n const ca = a.createdAt.localeCompare(b.createdAt);\n if (ca !== 0) return ca;\n const tc = a.to.localeCompare(b.to);\n if (tc !== 0) return tc;\n return a.migrationHash.localeCompare(b.migrationHash);\n}\n\nfunction sortedNeighbors(edges: readonly MigrationEdge[]): readonly MigrationEdge[] {\n return [...edges].sort(compareTieBreak);\n}\n\n/**\n * Find the shortest path from `fromHash` to `toHash` using BFS over the\n * contract-hash graph. Returns the ordered list of edges, or null if no path\n * exists. Returns an empty array when `fromHash === toHash` (no-op).\n *\n * Neighbor ordering is deterministic via the tie-break sort key:\n * label priority → createdAt → to → migrationHash.\n */\nexport function findPath(\n graph: MigrationGraph,\n fromHash: string,\n toHash: string,\n): readonly MigrationEdge[] | null {\n if (fromHash === toHash) return [];\n\n const parents = new Map<string, { parent: string; edge: MigrationEdge }>();\n for (const step of bfs([fromHash], (n) => sortedForwardNeighbours(graph, n))) {\n if (step.parent !== null && step.incomingEdge !== null) {\n parents.set(step.state, { parent: step.parent, edge: step.incomingEdge });\n }\n if (step.state === toHash) {\n const path: MigrationEdge[] = [];\n let cur = toHash;\n let p = parents.get(cur);\n while (p) {\n path.push(p.edge);\n cur = p.parent;\n p = parents.get(cur);\n }\n path.reverse();\n return path;\n }\n }\n\n return null;\n}\n\n/**\n * Find the shortest path from `fromHash` to `toHash` whose edges collectively\n * cover every invariant in `required`. Returns `null` when no such path exists\n * (either `fromHash`→`toHash` is structurally unreachable, or every reachable\n * path leaves at least one required invariant uncovered). When `required` is\n * empty, delegates to `findPath` so the result is byte-identical for that case.\n *\n * Algorithm: BFS over `(node, coveredSubset)` states with state-level dedup.\n * The covered subset is a `Set<string>` of invariant ids; the state's dedup\n * key is `${node}\\0${[...covered].sort().join('\\0')}`. State keys distinguish\n * distinct `(node, covered)` tuples regardless of node-name length because\n * `\\0` cannot appear in any invariant id (validation rejects whitespace and\n * control chars at authoring time).\n *\n * Neighbour ordering when `required ≠ ∅`: edges covering ≥1 still-needed\n * invariant come first, with `labelPriority → createdAt → to → migrationHash`\n * as the secondary key. The heuristic steers BFS toward the satisfying path;\n * correctness (shortest, deterministic) does not depend on it.\n */\nexport function findPathWithInvariants(\n graph: MigrationGraph,\n fromHash: string,\n toHash: string,\n required: ReadonlySet<string>,\n): readonly MigrationEdge[] | null {\n if (required.size === 0) {\n return findPath(graph, fromHash, toHash);\n }\n\n interface InvState {\n readonly node: string;\n readonly covered: ReadonlySet<string>;\n }\n // `\\0` is a safe segment separator: `validateInvariantId` rejects any id\n // containing whitespace or control characters (NUL is U+0000), and node\n // hashes are hex strings. Distinct `(node, covered)` tuples therefore\n // map to distinct strings. If `validateInvariantId` is ever relaxed,\n // re-confirm dedup correctness here.\n const stateKey = (s: InvState): string => {\n if (s.covered.size === 0) return `${s.node}\\0`;\n return `${s.node}\\0${[...s.covered].sort().join('\\0')}`;\n };\n\n const neighbours = (s: InvState): Iterable<{ next: InvState; edge: MigrationEdge }> => {\n const outgoing = graph.forwardChain.get(s.node) ?? [];\n if (outgoing.length === 0) return [];\n return [...outgoing]\n .map((edge) => {\n let useful = false;\n let next: Set<string> | null = null;\n for (const inv of edge.invariants) {\n if (required.has(inv) && !s.covered.has(inv)) {\n if (next === null) next = new Set(s.covered);\n next.add(inv);\n useful = true;\n }\n }\n return { edge, useful, nextCovered: next ?? s.covered };\n })\n .sort((a, b) => {\n if (a.useful !== b.useful) return a.useful ? -1 : 1;\n return compareTieBreak(a.edge, b.edge);\n })\n .map(({ edge, nextCovered }) => ({\n next: { node: edge.to, covered: nextCovered },\n edge,\n }));\n };\n\n // Path reconstruction is consumer-side, keyed on stateKey, same shape as\n // findPath's parents map.\n const parents = new Map<string, { parentKey: string; edge: MigrationEdge }>();\n for (const step of bfs<InvState, MigrationEdge>(\n [{ node: fromHash, covered: new Set() }],\n neighbours,\n stateKey,\n )) {\n const curKey = stateKey(step.state);\n if (step.parent !== null && step.incomingEdge !== null) {\n parents.set(curKey, { parentKey: stateKey(step.parent), edge: step.incomingEdge });\n }\n if (step.state.node === toHash && step.state.covered.size === required.size) {\n const path: MigrationEdge[] = [];\n let cur: string | undefined = curKey;\n while (cur !== undefined) {\n const p = parents.get(cur);\n if (!p) break;\n path.push(p.edge);\n cur = p.parentKey;\n }\n path.reverse();\n return path;\n }\n }\n\n return null;\n}\n\n/**\n * Reverse-BFS from `toHash` over `reverseChain` to collect every node from\n * which `toHash` is reachable (inclusive of `toHash` itself).\n */\nfunction collectNodesReachingTarget(graph: MigrationGraph, toHash: string): Set<string> {\n const reached = new Set<string>();\n for (const step of bfs([toHash], (n) => reverseNeighbours(graph, n))) {\n reached.add(step.state);\n }\n return reached;\n}\n\nexport interface PathDecision {\n readonly selectedPath: readonly MigrationEdge[];\n readonly fromHash: string;\n readonly toHash: string;\n readonly alternativeCount: number;\n readonly tieBreakReasons: readonly string[];\n readonly refName?: string;\n /** The caller-supplied required invariant set, sorted ascending. */\n readonly requiredInvariants: readonly string[];\n /**\n * The subset of `requiredInvariants` actually covered by edges on\n * `selectedPath`. Always a subset of `requiredInvariants` (when the path\n * is satisfying, equal to it); always derived from `selectedPath`.\n */\n readonly satisfiedInvariants: readonly string[];\n}\n\n/**\n * Outcome of {@link findPathWithDecision}. The pathfinder distinguishes\n * three cases up front so callers don't re-derive structural reachability:\n *\n * - `ok` — a path covering `required` exists; `decision` carries the\n * selection metadata and per-edge invariants.\n * - `unreachable` — `from`→`to` has no structural path. Mapped by callers\n * to the existing no-path / `NO_TARGET` diagnostic.\n * - `unsatisfiable` — `from`→`to` is structurally reachable but no path\n * covers every required invariant. `structuralPath` is the\n * `findPath(graph, from, to)` result, included so callers don't have to\n * recompute it when raising `MIGRATION.NO_INVARIANT_PATH`. `missing` is\n * the subset of `required` that the structural path does *not* cover —\n * correctly accounts for partial coverage when some required invariants\n * are met by the fallback path. Only emitted when `required` is\n * non-empty.\n */\nexport type FindPathOutcome =\n | { readonly kind: 'ok'; readonly decision: PathDecision }\n | { readonly kind: 'unreachable' }\n | {\n readonly kind: 'unsatisfiable';\n readonly structuralPath: readonly MigrationEdge[];\n readonly missing: readonly string[];\n };\n\n/**\n * Routing context for {@link findPathWithDecision}. Both fields are optional;\n * `refName` is only used to decorate the resulting `PathDecision` for the\n * JSON envelope, and `required` defaults to an empty set (purely structural\n * routing). They are passed via a single options object so the call sites\n * cannot silently swap two adjacent string parameters.\n */\nexport interface FindPathWithDecisionOptions {\n readonly refName?: string;\n readonly required?: ReadonlySet<string>;\n}\n\n/**\n * Find the shortest path from `fromHash` to `toHash` and return structured\n * path-decision metadata for machine-readable output. When `required` is\n * non-empty, the returned path is the shortest one whose edges collectively\n * cover every required invariant.\n *\n * The discriminated return type tells the caller *why* a path could not be\n * found, so the CLI can pick the right structured error without re-running\n * a structural BFS.\n */\nexport function findPathWithDecision(\n graph: MigrationGraph,\n fromHash: string,\n toHash: string,\n options: FindPathWithDecisionOptions = {},\n): FindPathOutcome {\n const { refName, required = new Set<string>() } = options;\n const requiredInvariants = [...required].sort();\n\n if (fromHash === toHash && required.size === 0) {\n return {\n kind: 'ok',\n decision: {\n selectedPath: [],\n fromHash,\n toHash,\n alternativeCount: 0,\n tieBreakReasons: [],\n requiredInvariants,\n satisfiedInvariants: [],\n ...ifDefined('refName', refName),\n },\n };\n }\n\n const path = findPathWithInvariants(graph, fromHash, toHash, required);\n if (!path) {\n if (required.size === 0) {\n return { kind: 'unreachable' };\n }\n const structural = findPath(graph, fromHash, toHash);\n if (structural === null) {\n return { kind: 'unreachable' };\n }\n const coveredByStructural = new Set<string>();\n for (const edge of structural) {\n for (const inv of edge.invariants) {\n if (required.has(inv)) coveredByStructural.add(inv);\n }\n }\n const missing = requiredInvariants.filter((id) => !coveredByStructural.has(id));\n return { kind: 'unsatisfiable', structuralPath: structural, missing };\n }\n\n const satisfiedInvariants = computeSatisfiedInvariants(required, path);\n\n // Single reverse BFS marks every node from which `toHash` is reachable.\n // Replaces a per-edge `findPath(e.to, toHash)` call inside the loop below,\n // which made the whole function O(|path| · (V + E)) instead of O(V + E).\n const reachesTarget = collectNodesReachingTarget(graph, toHash);\n const coveragePrefixes = requiredCoveragePrefixes(required, path);\n\n const tieBreakReasons: string[] = [];\n let alternativeCount = 0;\n\n for (const [i, edge] of path.entries()) {\n const outgoing = graph.forwardChain.get(edge.from);\n if (!outgoing || outgoing.length <= 1) continue;\n const reachable = outgoing.filter((e) => reachesTarget.has(e.to));\n if (reachable.length <= 1) continue;\n\n let comparisonPool: readonly MigrationEdge[] = reachable;\n if (required.size > 0) {\n // coveragePrefixes is built one-per-edge from path, so the index is\n // always in range here; the explicit guard keeps the type narrowed\n // without a non-null assertion.\n const prefixSet = coveragePrefixes[i];\n if (prefixSet === undefined) continue;\n comparisonPool = invariantViableAlternativesAtStep(required, prefixSet, reachable);\n }\n\n alternativeCount += reachable.length - 1;\n const sorted = sortedNeighbors(reachable);\n if (sorted[0]?.migrationHash !== edge.migrationHash) continue;\n if (!reachable.some((e) => e.migrationHash !== edge.migrationHash)) continue;\n\n const sortedViable = sortedNeighbors(comparisonPool);\n if (\n sortedViable.length > 1 &&\n sortedViable[0]?.migrationHash === edge.migrationHash &&\n sortedViable.some((e) => e.migrationHash !== edge.migrationHash)\n ) {\n tieBreakReasons.push(\n `at ${edge.from}: ${comparisonPool.length} candidates, selected by tie-break`,\n );\n }\n }\n\n return {\n kind: 'ok',\n decision: {\n selectedPath: path,\n fromHash,\n toHash,\n alternativeCount,\n tieBreakReasons,\n requiredInvariants,\n satisfiedInvariants,\n ...ifDefined('refName', refName),\n },\n };\n}\n\nfunction computeSatisfiedInvariants(\n required: ReadonlySet<string>,\n path: readonly MigrationEdge[],\n): readonly string[] {\n if (required.size === 0) return [];\n const covered = new Set<string>();\n for (const edge of path) {\n for (const inv of edge.invariants) {\n if (required.has(inv)) covered.add(inv);\n }\n }\n return [...covered].sort();\n}\n\n/**\n * For each edge on path, invariant coverage accumulated from earlier edges only —\n * `(required ∩ ∪_{j<i} path[j].invariants)` represented as cumulative set along `required`,\n * keyed as \"full set of required ids satisfied before taking path[i]\".\n */\nfunction requiredCoveragePrefixes(\n required: ReadonlySet<string>,\n path: readonly MigrationEdge[],\n): readonly ReadonlySet<string>[] {\n const prefixes: ReadonlySet<string>[] = [];\n const acc = new Set<string>();\n for (const edge of path) {\n prefixes.push(new Set(acc));\n for (const inv of edge.invariants) {\n if (required.has(inv)) acc.add(inv);\n }\n }\n return prefixes;\n}\n\nfunction invariantViableAlternativesAtStep(\n required: ReadonlySet<string>,\n coverageBeforeTakingEdge: ReadonlySet<string>,\n outgoing: readonly MigrationEdge[],\n): readonly MigrationEdge[] {\n if (required.size === 0) return [...outgoing];\n return outgoing.filter((e) =>\n [...required].every((id) => coverageBeforeTakingEdge.has(id) || e.invariants.includes(id)),\n );\n}\n\n/**\n * Walk ancestors of each branch tip back to find the last node\n * that appears on all paths. Returns `fromHash` if no shared ancestor is found.\n */\nfunction findDivergencePoint(\n graph: MigrationGraph,\n fromHash: string,\n leaves: readonly string[],\n): string {\n const ancestorSets = leaves.map((leaf) => {\n const ancestors = new Set<string>();\n for (const step of bfs([leaf], (n) => reverseNeighbours(graph, n))) {\n ancestors.add(step.state);\n }\n return ancestors;\n });\n\n const commonAncestors = [...(ancestorSets[0] ?? [])].filter((node) =>\n ancestorSets.every((s) => s.has(node)),\n );\n\n let deepest = fromHash;\n let deepestDepth = -1;\n for (const ancestor of commonAncestors) {\n const path = findPath(graph, fromHash, ancestor);\n const depth = path ? path.length : 0;\n if (depth > deepestDepth) {\n deepestDepth = depth;\n deepest = ancestor;\n }\n }\n return deepest;\n}\n\n/**\n * Find all branch tips (nodes with no outgoing edges) reachable from\n * `fromHash` via forward edges.\n */\nexport function findReachableLeaves(graph: MigrationGraph, fromHash: string): readonly string[] {\n const leaves: string[] = [];\n for (const step of bfs([fromHash], (n) => forwardNeighbours(graph, n))) {\n if (!graph.forwardChain.get(step.state)?.length) {\n leaves.push(step.state);\n }\n }\n return leaves;\n}\n\n/**\n * Find the target contract hash of the migration graph reachable from\n * EMPTY_CONTRACT_HASH. Returns `null` for a graph that has no target\n * state (either empty, or containing only the root with no outgoing\n * edges). Throws NO_INITIAL_MIGRATION if the graph has nodes but none\n * originate from the empty hash, and AMBIGUOUS_TARGET if multiple\n * branch tips exist.\n */\nexport function findLeaf(graph: MigrationGraph): string | null {\n if (graph.nodes.size === 0) {\n return null;\n }\n\n if (!graph.nodes.has(EMPTY_CONTRACT_HASH)) {\n throw errorNoInitialMigration([...graph.nodes]);\n }\n\n const leaves = findReachableLeaves(graph, EMPTY_CONTRACT_HASH);\n\n if (leaves.length === 0) {\n const reachable = [...graph.nodes].filter((n) => n !== EMPTY_CONTRACT_HASH);\n if (reachable.length > 0) {\n throw errorNoTarget(reachable);\n }\n return null;\n }\n\n if (leaves.length > 1) {\n const divergencePoint = findDivergencePoint(graph, EMPTY_CONTRACT_HASH, leaves);\n const branches = leaves.map((tip) => {\n const path = findPath(graph, divergencePoint, tip);\n return {\n tip,\n edges: (path ?? []).map((e) => ({ dirName: e.dirName, from: e.from, to: e.to })),\n };\n });\n throw errorAmbiguousTarget(leaves, { divergencePoint, branches });\n }\n\n // biome-ignore lint/style/noNonNullAssertion: leaves.length is neither 0 nor >1 per the branches above, so exactly one leaf remains\n return leaves[0]!;\n}\n\n/**\n * Find the latest migration entry by traversing from EMPTY_CONTRACT_HASH\n * to the single target. Returns null for an empty graph.\n * Throws AMBIGUOUS_TARGET if the graph has multiple branch tips.\n */\nexport function findLatestMigration(graph: MigrationGraph): MigrationEdge | null {\n const leafHash = findLeaf(graph);\n if (leafHash === null) return null;\n\n const path = findPath(graph, EMPTY_CONTRACT_HASH, leafHash);\n return path?.at(-1) ?? null;\n}\n\nexport function detectCycles(graph: MigrationGraph): readonly string[][] {\n const WHITE = 0;\n const GRAY = 1;\n const BLACK = 2;\n\n const color = new Map<string, number>();\n const parentMap = new Map<string, string | null>();\n const cycles: string[][] = [];\n\n for (const node of graph.nodes) {\n color.set(node, WHITE);\n }\n\n // Iterative three-color DFS. A frame is (node, outgoing edges, next-index).\n interface Frame {\n node: string;\n outgoing: readonly MigrationEdge[];\n index: number;\n }\n const stack: Frame[] = [];\n\n function pushFrame(u: string): void {\n color.set(u, GRAY);\n stack.push({ node: u, outgoing: graph.forwardChain.get(u) ?? [], index: 0 });\n }\n\n for (const root of graph.nodes) {\n if (color.get(root) !== WHITE) continue;\n parentMap.set(root, null);\n pushFrame(root);\n\n while (stack.length > 0) {\n // biome-ignore lint/style/noNonNullAssertion: stack.length > 0 should guarantee that this cannot be undefined\n const frame = stack[stack.length - 1]!;\n if (frame.index >= frame.outgoing.length) {\n color.set(frame.node, BLACK);\n stack.pop();\n continue;\n }\n // biome-ignore lint/style/noNonNullAssertion: the early-continue above guarantees frame.index < frame.outgoing.length here, so this is defined\n const edge = frame.outgoing[frame.index++]!;\n const v = edge.to;\n const vColor = color.get(v);\n if (vColor === GRAY) {\n const cycle: string[] = [v];\n let cur = frame.node;\n while (cur !== v) {\n cycle.push(cur);\n cur = parentMap.get(cur) ?? v;\n }\n cycle.reverse();\n cycles.push(cycle);\n } else if (vColor === WHITE) {\n parentMap.set(v, frame.node);\n pushFrame(v);\n }\n }\n }\n\n return cycles;\n}\n\nexport function detectOrphans(graph: MigrationGraph): readonly MigrationEdge[] {\n if (graph.nodes.size === 0) return [];\n\n const reachable = new Set<string>();\n const startNodes: string[] = [];\n\n if (graph.forwardChain.has(EMPTY_CONTRACT_HASH)) {\n startNodes.push(EMPTY_CONTRACT_HASH);\n } else {\n const allTargets = new Set<string>();\n for (const edges of graph.forwardChain.values()) {\n for (const edge of edges) {\n allTargets.add(edge.to);\n }\n }\n for (const node of graph.nodes) {\n if (!allTargets.has(node)) {\n startNodes.push(node);\n }\n }\n }\n\n for (const step of bfs(startNodes, (n) => forwardNeighbours(graph, n))) {\n reachable.add(step.state);\n }\n\n const orphans: MigrationEdge[] = [];\n for (const [from, migrations] of graph.forwardChain) {\n if (!reachable.has(from)) {\n orphans.push(...migrations);\n }\n }\n\n return orphans;\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"graph-Czaj8O2q.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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"hash-G0bAfIGh.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, OnDiskMigrationPackage } from './package';\n\nexport interface VerifyResult {\n readonly ok: boolean;\n readonly reason?: 'mismatch';\n readonly storedHash: string;\n readonly computedHash: string;\n}\n\nfunction sha256Hex(input: string): string {\n return createHash('sha256').update(input).digest('hex');\n}\n\n/**\n * Content-addressed migration hash over (metadata envelope sans\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: OnDiskMigrationPackage): VerifyResult {\n const computed = computeMigrationHash(pkg.metadata, pkg.ops);\n\n if (pkg.metadata.migrationHash === computed) {\n return {\n ok: true,\n storedHash: pkg.metadata.migrationHash,\n computedHash: computed,\n };\n }\n\n return {\n ok: false,\n reason: 'mismatch',\n storedHash: pkg.metadata.migrationHash,\n computedHash: computed,\n };\n}\n"],"mappings":";;;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,KAA2C;CAC7E,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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"invariants-4Avb_Yhy.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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"io-CDJaWGbt.mjs","names":["manifestRaw: string","opsRaw: string","metadata: MigrationMetadata","ops: MigrationOps","pkg: OnDiskMigrationPackage","entries: string[]","packages: OnDiskMigrationPackage[]"],"sources":["../src/io.ts"],"sourcesContent":["import { copyFile, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';\nimport type {\n MigrationMetadata,\n MigrationPackage,\n} from '@prisma-next/framework-components/control';\nimport { type } from 'arktype';\nimport { basename, dirname, join, resolve } from 'pathe';\nimport { canonicalizeJson } from './canonicalize-json';\nimport {\n errorDirectoryExists,\n errorInvalidDestName,\n errorInvalidJson,\n errorInvalidManifest,\n errorInvalidSlug,\n errorMigrationHashMismatch,\n errorMissingFile,\n errorProvidedInvariantsMismatch,\n} from './errors';\nimport { verifyMigrationHash } from './hash';\nimport { deriveProvidedInvariants } from './invariants';\nimport { MigrationOpsSchema } from './op-schema';\nimport type { MigrationOps, OnDiskMigrationPackage } from './package';\n\nexport const MANIFEST_FILE = 'migration.json';\nconst OPS_FILE = 'ops.json';\nconst MAX_SLUG_LENGTH = 64;\n\nfunction hasErrnoCode(error: unknown, code: string): boolean {\n return error instanceof Error && (error as { code?: string }).code === code;\n}\n\nconst MigrationHintsSchema = type({\n used: 'string[]',\n applied: 'string[]',\n plannerVersion: 'string',\n});\n\nconst MigrationMetadataSchema = type({\n '+': 'reject',\n from: 'string > 0 | null',\n to: 'string',\n migrationHash: 'string',\n fromContract: 'object | null',\n toContract: 'object',\n hints: MigrationHintsSchema,\n labels: 'string[]',\n providedInvariants: '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\nexport async function writeMigrationPackage(\n dir: string,\n metadata: MigrationMetadata,\n ops: MigrationOps,\n): Promise<void> {\n await mkdir(dirname(dir), { recursive: true });\n\n try {\n await mkdir(dir);\n } catch (error) {\n if (hasErrnoCode(error, 'EEXIST')) {\n throw errorDirectoryExists(dir);\n }\n throw error;\n }\n\n await writeFile(join(dir, MANIFEST_FILE), JSON.stringify(metadata, null, 2), {\n flag: 'wx',\n });\n await writeFile(join(dir, OPS_FILE), JSON.stringify(ops, null, 2), { flag: 'wx' });\n}\n\n/**\n * Materialise an in-memory {@link MigrationPackage} to a per-space\n * directory on disk.\n *\n * Writes three files under `<targetDir>/<pkg.dirName>/`:\n *\n * - `migration.json` — the manifest (pretty-printed, matches\n * {@link writeMigrationPackage}'s output for byte-for-byte parity with\n * app-space migrations).\n * - `ops.json` — the operation list (pretty-printed).\n * - `contract.json` — the canonical-JSON serialisation of\n * `metadata.toContract`. This is the per-package post-state contract\n * snapshot; the canonicalisation pass guarantees byte-determinism so\n * re-emitting the same package across machines / runs produces an\n * identical file.\n *\n * Distinct verb from the lower-level {@link writeMigrationPackage}\n * (which takes constituent `(metadata, ops)`): callers reading\n * `materialise…` know they are persisting a struct-typed package\n * including its contract-snapshot side car.\n *\n * Overwrite-idempotent: the per-package directory is cleared before\n * each emit, so re-running against the same `targetDir` produces\n * byte-identical contents and never leaves stale files behind. The\n * spec's \"re-emitting the same package across runs / machines produces\n * byte-identical files\" guarantee (§ 3) covers both same-dir and\n * fresh-dir re-emits. The lower-level {@link writeMigrationPackage}\n * stays strict because the CLI authoring path (`migration plan` /\n * `migration new`) deliberately refuses to clobber an existing\n * authored migration; this helper is the re-emit path that is\n * supposed to converge on a single canonical on-disk shape.\n *\n * @see specs/framework-mechanism.spec.md § 3 — Emission helper (T1.7).\n */\nexport async function materialiseMigrationPackage(\n targetDir: string,\n pkg: MigrationPackage,\n): Promise<void> {\n const dir = join(targetDir, pkg.dirName);\n await rm(dir, { recursive: true, force: true });\n await writeMigrationPackage(dir, pkg.metadata, pkg.ops);\n await writeFile(join(dir, 'contract.json'), `${canonicalizeJson(pkg.metadata.toContract)}\\n`, {\n flag: 'wx',\n });\n}\n\n/**\n * Copy a list of files into `destDir`, optionally renaming each one.\n *\n * The destination directory is created (with `recursive: true`) if it\n * does not already exist. Each source path is copied byte-for-byte into\n * `destDir/<destName>`; missing sources throw `ENOENT`. The helper is\n * intentionally generic: callers own the list of files (e.g. a contract\n * emitter's emitted output) and the naming convention (e.g. renaming\n * the destination contract to `end-contract.*` and the source contract\n * to `start-contract.*`).\n */\nexport async function copyFilesWithRename(\n destDir: string,\n files: readonly { readonly sourcePath: string; readonly destName: string }[],\n): Promise<void> {\n await mkdir(destDir, { recursive: true });\n for (const file of files) {\n if (basename(file.destName) !== file.destName) {\n throw errorInvalidDestName(file.destName);\n }\n await copyFile(file.sourcePath, join(destDir, file.destName));\n }\n}\n\nexport async function writeMigrationMetadata(\n dir: string,\n metadata: MigrationMetadata,\n): Promise<void> {\n await writeFile(join(dir, MANIFEST_FILE), `${JSON.stringify(metadata, null, 2)}\\n`);\n}\n\nexport async function writeMigrationOps(dir: string, ops: MigrationOps): Promise<void> {\n await writeFile(join(dir, OPS_FILE), `${JSON.stringify(ops, null, 2)}\\n`);\n}\n\nexport async function readMigrationPackage(dir: string): Promise<OnDiskMigrationPackage> {\n const absoluteDir = resolve(dir);\n const manifestPath = join(absoluteDir, MANIFEST_FILE);\n const opsPath = join(absoluteDir, OPS_FILE);\n\n let manifestRaw: string;\n try {\n manifestRaw = await readFile(manifestPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorMissingFile(MANIFEST_FILE, absoluteDir);\n }\n throw error;\n }\n\n let opsRaw: string;\n try {\n opsRaw = await readFile(opsPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorMissingFile(OPS_FILE, absoluteDir);\n }\n throw error;\n }\n\n let metadata: MigrationMetadata;\n try {\n metadata = JSON.parse(manifestRaw);\n } catch (e) {\n throw errorInvalidJson(manifestPath, e instanceof Error ? e.message : String(e));\n }\n\n let ops: MigrationOps;\n try {\n ops = JSON.parse(opsRaw);\n } catch (e) {\n throw errorInvalidJson(opsPath, e instanceof Error ? e.message : String(e));\n }\n\n validateMetadata(metadata, manifestPath);\n validateOps(ops, opsPath);\n\n // Re-derive before the hash check so format/duplicate diagnostics\n // fire with their dedicated codes rather than as a generic hash mismatch.\n const derivedInvariants = deriveProvidedInvariants(ops);\n if (!arraysEqual(metadata.providedInvariants, derivedInvariants)) {\n throw errorProvidedInvariantsMismatch(\n manifestPath,\n metadata.providedInvariants,\n derivedInvariants,\n );\n }\n\n const pkg: OnDiskMigrationPackage = {\n dirName: basename(absoluteDir),\n dirPath: absoluteDir,\n metadata,\n ops,\n };\n\n const verification = verifyMigrationHash(pkg);\n if (!verification.ok) {\n throw errorMigrationHashMismatch(\n absoluteDir,\n verification.storedHash,\n verification.computedHash,\n );\n }\n\n return pkg;\n}\n\nfunction arraysEqual(a: readonly string[], b: readonly string[]): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\nfunction validateMetadata(\n metadata: unknown,\n filePath: string,\n): asserts metadata is MigrationMetadata {\n const result = MigrationMetadataSchema(metadata);\n if (result instanceof type.errors) {\n throw errorInvalidManifest(filePath, result.summary);\n }\n}\n\nfunction validateOps(ops: unknown, filePath: string): asserts ops is MigrationOps {\n const result = MigrationOpsSchema(ops);\n if (result instanceof type.errors) {\n throw errorInvalidManifest(filePath, result.summary);\n }\n}\n\nexport async function readMigrationsDir(\n migrationsRoot: string,\n): Promise<readonly OnDiskMigrationPackage[]> {\n let entries: string[];\n try {\n entries = await readdir(migrationsRoot);\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n return [];\n }\n throw error;\n }\n\n const packages: OnDiskMigrationPackage[] = [];\n\n for (const entry of entries.sort()) {\n const entryPath = join(migrationsRoot, entry);\n const entryStat = await stat(entryPath);\n if (!entryStat.isDirectory()) continue;\n\n const manifestPath = join(entryPath, MANIFEST_FILE);\n try {\n await stat(manifestPath);\n } catch {\n continue; // skip non-migration directories\n }\n\n packages.push(await readMigrationPackage(entryPath));\n }\n\n return packages;\n}\n\nexport function formatMigrationDirName(timestamp: Date, slug: string): string {\n const sanitized = slug\n .toLowerCase()\n .replace(/[^a-z0-9]/g, '_')\n .replace(/_+/g, '_')\n .replace(/^_|_$/g, '');\n\n if (sanitized.length === 0) {\n throw errorInvalidSlug(slug);\n }\n\n const truncated = sanitized.slice(0, MAX_SLUG_LENGTH);\n\n const y = timestamp.getUTCFullYear();\n const mo = String(timestamp.getUTCMonth() + 1).padStart(2, '0');\n const d = String(timestamp.getUTCDate()).padStart(2, '0');\n const h = String(timestamp.getUTCHours()).padStart(2, '0');\n const mi = String(timestamp.getUTCMinutes()).padStart(2, '0');\n\n return `${y}${mo}${d}T${h}${mi}_${truncated}`;\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"package-B3Yl6DTr.d.mts","names":[],"sources":["../src/package.ts"],"sourcesContent":[],"mappings":";;;KAKY,YAAA,YAAwB;;AAApC;AAaA;;;;;;;;;UAAiB,sBAAA,SAA+B"}
@@ -1,77 +0,0 @@
1
- import { readFile } from 'node:fs/promises';
2
- import { join } from 'pathe';
3
- import { errorInvalidJson, errorInvalidRefFile, errorPinnedArtefactsAppSpace } from './errors';
4
- import { APP_SPACE_ID, assertValidSpaceId } from './space-layout';
5
-
6
- function hasErrnoCode(error: unknown, code: string): boolean {
7
- return error instanceof Error && (error as { code?: string }).code === code;
8
- }
9
-
10
- /**
11
- * Read the pinned head hash for an extension space.
12
- *
13
- * Returns the `hash` field of `<projectMigrationsDir>/<spaceId>/refs/head.json`
14
- * — i.e. the canonical contract hash the framework wrote on the last
15
- * `migrate` for this space. Returns `null` when the file does not exist
16
- * (or the migrations directory is missing entirely), which is the
17
- * "first emit" signal {@link import('./detect-space-contract-drift').detectSpaceContractDrift}
18
- * uses to distinguish a brand-new extension from drift.
19
- *
20
- * Pure I/O (read + parse). The "comparison hash" is stored on disk by
21
- * {@link import('./emit-pinned-space-artefacts').emitPinnedSpaceArtefacts}
22
- * via the descriptor's `headRef.hash`, so reading it back here matches
23
- * the descriptor's hashing pipeline by construction — neither side
24
- * recomputes anything.
25
- *
26
- * Validation:
27
- *
28
- * - Rejects the app space — pinned head refs are an extension-space
29
- * concept; the app space's contract-of-record lives at the project
30
- * root, not under `migrations/`.
31
- * - Validates the space id against the same `[a-z][a-z0-9_-]{0,63}`
32
- * pattern as the rest of the per-space helpers.
33
- * - Surfaces `MIGRATION.INVALID_JSON` / `MIGRATION.INVALID_REF_FILE`
34
- * on a corrupt `refs/head.json` so callers can distinguish "no
35
- * pinned file" (returns `null`) from "pinned file but unreadable"
36
- * (throws).
37
- *
38
- * @see specs/framework-mechanism.spec.md § 3 — Drift detection (T1.9).
39
- */
40
- export async function readPinnedContractHash(
41
- projectMigrationsDir: string,
42
- spaceId: string,
43
- ): Promise<string | null> {
44
- if (spaceId === APP_SPACE_ID) {
45
- throw errorPinnedArtefactsAppSpace();
46
- }
47
- assertValidSpaceId(spaceId);
48
-
49
- const filePath = join(projectMigrationsDir, spaceId, 'refs', 'head.json');
50
-
51
- let raw: string;
52
- try {
53
- raw = await readFile(filePath, 'utf-8');
54
- } catch (error) {
55
- if (hasErrnoCode(error, 'ENOENT')) {
56
- return null;
57
- }
58
- throw error;
59
- }
60
-
61
- let parsed: unknown;
62
- try {
63
- parsed = JSON.parse(raw);
64
- } catch (e) {
65
- throw errorInvalidJson(filePath, e instanceof Error ? e.message : String(e));
66
- }
67
-
68
- if (
69
- typeof parsed !== 'object' ||
70
- parsed === null ||
71
- typeof (parsed as { hash?: unknown }).hash !== 'string'
72
- ) {
73
- throw errorInvalidRefFile(filePath, 'expected an object with a string `hash` field');
74
- }
75
-
76
- return (parsed as { hash: string }).hash;
77
- }