@prisma-next/migration-tools 0.11.0-dev.55 → 0.11.0-dev.57

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 (68) hide show
  1. package/dist/{errors-4YabujxZ.mjs → errors-vFROOhCR.mjs} +33 -2
  2. package/dist/{errors-4YabujxZ.mjs.map → errors-vFROOhCR.mjs.map} +1 -1
  3. package/dist/exports/aggregate.d.mts +37 -9
  4. package/dist/exports/aggregate.d.mts.map +1 -1
  5. package/dist/exports/aggregate.mjs +133 -17
  6. package/dist/exports/aggregate.mjs.map +1 -1
  7. package/dist/exports/enumerate-migration-spaces.d.mts +1 -1
  8. package/dist/exports/enumerate-migration-spaces.mjs +3 -3
  9. package/dist/exports/errors.mjs +1 -1
  10. package/dist/exports/graph.d.mts +1 -1
  11. package/dist/exports/hash.d.mts +2 -2
  12. package/dist/exports/invariants.d.mts +1 -1
  13. package/dist/exports/invariants.mjs +1 -1
  14. package/dist/exports/io.d.mts +1 -1
  15. package/dist/exports/io.mjs +1 -1
  16. package/dist/exports/metadata.d.mts +1 -1
  17. package/dist/exports/migration-graph.d.mts +2 -2
  18. package/dist/exports/migration-graph.mjs +2 -15
  19. package/dist/exports/migration-list-graph-topology.d.mts +1 -1
  20. package/dist/exports/migration-list-types.d.mts +1 -1
  21. package/dist/exports/migration.d.mts +1 -1
  22. package/dist/exports/migration.mjs +2 -2
  23. package/dist/exports/package.d.mts +1 -1
  24. package/dist/exports/ref-resolution.d.mts +2 -2
  25. package/dist/exports/ref-resolution.mjs +1 -1
  26. package/dist/exports/refs.d.mts +1 -1
  27. package/dist/exports/refs.mjs +2 -136
  28. package/dist/exports/spaces.d.mts +2 -2
  29. package/dist/exports/spaces.mjs +5 -5
  30. package/dist/{graph-BUZuUeBC.d.mts → graph-BwjwIZmb.d.mts} +1 -1
  31. package/dist/{graph-BUZuUeBC.d.mts.map → graph-BwjwIZmb.d.mts.map} +1 -1
  32. package/dist/graph-membership-BV23F1IV.mjs +15 -0
  33. package/dist/graph-membership-BV23F1IV.mjs.map +1 -0
  34. package/dist/{invariants-CCOAyg6c.mjs → invariants-C23nXy1c.mjs} +2 -2
  35. package/dist/{invariants-CCOAyg6c.mjs.map → invariants-C23nXy1c.mjs.map} +1 -1
  36. package/dist/{io-BHl0amF0.mjs → io-BGlPOt9b.mjs} +3 -3
  37. package/dist/{io-BHl0amF0.mjs.map → io-BGlPOt9b.mjs.map} +1 -1
  38. package/dist/{io-nqFXSSTN.d.mts → io-g7fQCYNJ.d.mts} +2 -2
  39. package/dist/{io-nqFXSSTN.d.mts.map → io-g7fQCYNJ.d.mts.map} +1 -1
  40. package/dist/{migration-graph-kGBkIZDa.mjs → migration-graph-BMAqSfv9.mjs} +2 -2
  41. package/dist/{migration-graph-kGBkIZDa.mjs.map → migration-graph-BMAqSfv9.mjs.map} +1 -1
  42. package/dist/{migration-graph-DtNT-cqc.d.mts → migration-graph-CC7PSXw0.d.mts} +3 -3
  43. package/dist/{migration-graph-DtNT-cqc.d.mts.map → migration-graph-CC7PSXw0.d.mts.map} +1 -1
  44. package/dist/{migration-list-types-BRTuXR8i.d.mts → migration-list-types-B8NY0jQ1.d.mts} +1 -1
  45. package/dist/{migration-list-types-BRTuXR8i.d.mts.map → migration-list-types-B8NY0jQ1.d.mts.map} +1 -1
  46. package/dist/{package-DIttKL7X.d.mts → package-C31VGBCK.d.mts} +1 -1
  47. package/dist/{package-DIttKL7X.d.mts.map → package-C31VGBCK.d.mts.map} +1 -1
  48. package/dist/{read-contract-space-contract-BS5Oxbgw.mjs → read-contract-space-contract-BbcST3Lm.mjs} +3 -3
  49. package/dist/{read-contract-space-contract-BS5Oxbgw.mjs.map → read-contract-space-contract-BbcST3Lm.mjs.map} +1 -1
  50. package/dist/{refs-C8f2IGM8.d.mts → refs-BPYU-r14.d.mts} +1 -1
  51. package/dist/{refs-C8f2IGM8.d.mts.map → refs-BPYU-r14.d.mts.map} +1 -1
  52. package/dist/{refs-BBKNL45K.mjs → refs-Ditzcs06.mjs} +2 -2
  53. package/dist/{refs-BBKNL45K.mjs.map → refs-Ditzcs06.mjs.map} +1 -1
  54. package/dist/snapshot-CVg78oBp.mjs +137 -0
  55. package/dist/snapshot-CVg78oBp.mjs.map +1 -0
  56. package/dist/{verify-contract-spaces-BJX5gqtD.mjs → verify-contract-spaces-3u7gzhqT.mjs} +3 -3
  57. package/dist/{verify-contract-spaces-BJX5gqtD.mjs.map → verify-contract-spaces-3u7gzhqT.mjs.map} +1 -1
  58. package/dist/{verify-contract-spaces-T0aiJlBS.d.mts → verify-contract-spaces-C7EZktZP.d.mts} +1 -1
  59. package/dist/{verify-contract-spaces-T0aiJlBS.d.mts.map → verify-contract-spaces-C7EZktZP.d.mts.map} +1 -1
  60. package/package.json +6 -6
  61. package/src/aggregate/aggregate.ts +184 -8
  62. package/src/aggregate/loader.ts +6 -1
  63. package/src/aggregate/types.ts +27 -0
  64. package/src/errors.ts +41 -0
  65. package/src/exports/aggregate.ts +6 -1
  66. package/dist/exports/migration-graph.mjs.map +0 -1
  67. package/dist/exports/refs.mjs.map +0 -1
  68. /package/dist/{metadata-Bp9X04gM.d.mts → metadata-CH3tNNkp.d.mts} +0 -0
@@ -1,11 +1,160 @@
1
+ import { readFile } from 'node:fs/promises';
1
2
  import type { Contract } from '@prisma-next/contract/types';
3
+ import { join } from 'pathe';
4
+ import {
5
+ errorBundleNotFoundForGraphNode,
6
+ errorContractDeserializationFailed,
7
+ errorHashNotInGraph,
8
+ errorInvalidJson,
9
+ errorMissingFile,
10
+ errorSnapshotMissing,
11
+ MigrationToolsError,
12
+ } from '../errors';
2
13
  import type { MigrationGraph } from '../graph';
14
+ import { isGraphNode } from '../graph-membership';
3
15
  import type { IntegrityQueryOptions, IntegrityViolation } from '../integrity-violation';
4
16
  import { reconstructGraph } from '../migration-graph';
5
17
  import type { OnDiskMigrationPackage } from '../package';
6
18
  import type { Refs } from '../refs';
19
+ import { readRefSnapshot } from '../refs/snapshot';
7
20
  import type { ContractSpaceHeadRecord } from '../verify-contract-spaces';
8
- import type { ContractSpaceAggregate, ContractSpaceMember } from './types';
21
+ import type {
22
+ ContractAtOptions,
23
+ ContractAtResult,
24
+ ContractSpaceAggregate,
25
+ ContractSpaceMember,
26
+ } from './types';
27
+
28
+ function hasErrnoCode(error: unknown, code: string): boolean {
29
+ return error instanceof Error && (error as { code?: string }).code === code;
30
+ }
31
+
32
+ function contractAtMemoKey(hash: string, refName: string | undefined): string {
33
+ return `${hash}\0${refName ?? ''}`;
34
+ }
35
+
36
+ function deserializeContractAtPath(
37
+ filePath: string,
38
+ contractJson: unknown,
39
+ deserializeContract: (raw: unknown) => Contract,
40
+ ): Contract {
41
+ try {
42
+ return deserializeContract(contractJson);
43
+ } catch (error) {
44
+ if (MigrationToolsError.is(error)) {
45
+ throw error;
46
+ }
47
+ const message = error instanceof Error ? error.message : String(error);
48
+ throw errorContractDeserializationFailed(filePath, message);
49
+ }
50
+ }
51
+
52
+ async function readGraphNodeEndContract(
53
+ packageDir: string,
54
+ deserializeContract: (raw: unknown) => Contract,
55
+ ): Promise<{ contractJson: unknown; contractDts: string; contract: Contract }> {
56
+ const jsonPath = join(packageDir, 'end-contract.json');
57
+ const dtsPath = join(packageDir, 'end-contract.d.ts');
58
+
59
+ let rawJson: string;
60
+ try {
61
+ rawJson = await readFile(jsonPath, 'utf-8');
62
+ } catch (error) {
63
+ if (hasErrnoCode(error, 'ENOENT')) {
64
+ throw errorMissingFile('end-contract.json', packageDir);
65
+ }
66
+ throw error;
67
+ }
68
+
69
+ let contractJson: unknown;
70
+ try {
71
+ contractJson = JSON.parse(rawJson);
72
+ } catch (error) {
73
+ throw errorInvalidJson(jsonPath, error instanceof Error ? error.message : String(error));
74
+ }
75
+
76
+ let contractDts: string;
77
+ try {
78
+ contractDts = await readFile(dtsPath, 'utf-8');
79
+ } catch (error) {
80
+ if (hasErrnoCode(error, 'ENOENT')) {
81
+ throw errorMissingFile('end-contract.d.ts', packageDir);
82
+ }
83
+ throw error;
84
+ }
85
+
86
+ const contract = deserializeContractAtPath(jsonPath, contractJson, deserializeContract);
87
+ return { contractJson, contractDts, contract };
88
+ }
89
+
90
+ async function resolveContractAt(args: {
91
+ readonly hash: string;
92
+ readonly opts: ContractAtOptions | undefined;
93
+ readonly refsDir: string;
94
+ readonly packages: readonly OnDiskMigrationPackage[];
95
+ readonly graph: MigrationGraph;
96
+ readonly deserializeContract: (raw: unknown) => Contract;
97
+ }): Promise<ContractAtResult> {
98
+ const { hash, opts, refsDir, packages, graph, deserializeContract } = args;
99
+ const refName = opts?.refName;
100
+
101
+ if (refName !== undefined) {
102
+ const snapshot = await readRefSnapshot(refsDir, refName);
103
+ if (snapshot) {
104
+ const jsonPath = join(refsDir, `${refName}.contract.json`);
105
+ return {
106
+ hash,
107
+ contractJson: snapshot.contract,
108
+ contractDts: snapshot.contractDts,
109
+ contract: deserializeContractAtPath(jsonPath, snapshot.contract, deserializeContract),
110
+ provenance: 'snapshot',
111
+ };
112
+ }
113
+
114
+ if (isGraphNode(hash, graph)) {
115
+ return resolveGraphNodeContractAt({
116
+ hash,
117
+ packages,
118
+ deserializeContract,
119
+ explicitLabel: refName,
120
+ });
121
+ }
122
+
123
+ throw errorSnapshotMissing(refName);
124
+ }
125
+
126
+ if (isGraphNode(hash, graph)) {
127
+ return resolveGraphNodeContractAt({ hash, packages, deserializeContract });
128
+ }
129
+
130
+ throw errorHashNotInGraph(hash, graph);
131
+ }
132
+
133
+ async function resolveGraphNodeContractAt(args: {
134
+ readonly hash: string;
135
+ readonly packages: readonly OnDiskMigrationPackage[];
136
+ readonly deserializeContract: (raw: unknown) => Contract;
137
+ readonly explicitLabel?: string;
138
+ }): Promise<ContractAtResult> {
139
+ const { hash, packages, deserializeContract, explicitLabel } = args;
140
+ const matchingBundle = packages.find((pkg) => pkg.metadata.to === hash);
141
+ if (!matchingBundle) {
142
+ throw errorBundleNotFoundForGraphNode(hash, explicitLabel);
143
+ }
144
+
145
+ const { contractJson, contractDts, contract } = await readGraphNodeEndContract(
146
+ matchingBundle.dirPath,
147
+ deserializeContract,
148
+ );
149
+ return {
150
+ hash,
151
+ contractJson,
152
+ contractDts,
153
+ contract,
154
+ provenance: 'graph-node',
155
+ sourceDir: matchingBundle.dirPath,
156
+ };
157
+ }
9
158
 
10
159
  /**
11
160
  * Resolve a member's head ref, asserting it is present. The apply/verify
@@ -25,38 +174,65 @@ export function requireHeadRef(member: ContractSpaceMember): ContractSpaceHeadRe
25
174
  }
26
175
 
27
176
  /**
28
- * Build a {@link ContractSpaceMember} with lazily-memoised `graph()` and
29
- * `contract()` facets.
177
+ * Build a {@link ContractSpaceMember} with lazily-memoised `graph()`,
178
+ * `contract()`, and `contractAt()` facets.
30
179
  *
31
180
  * `graph()` reconstructs the migration graph from `packages` on first
32
181
  * call and caches it. `contract()` calls `resolveContract` on first call
33
182
  * and caches the result; a throwing `resolveContract` (e.g. a missing or
34
183
  * undeserializable on-disk contract) re-throws on each call rather than
35
184
  * caching a value — `checkIntegrity` surfaces that as `contractUnreadable`.
185
+ * `contractAt()` materializes the contract at an arbitrary graph node with
186
+ * the same resolution order as plan-time ref resolution: ref snapshot first
187
+ * (when `opts.refName` is set), else the matching package's `end-contract.*`.
36
188
  */
37
189
  export function createContractSpaceMember(args: {
38
190
  readonly spaceId: string;
39
191
  readonly packages: readonly OnDiskMigrationPackage[];
40
192
  readonly refs: Refs;
41
193
  readonly headRef: ContractSpaceHeadRecord | null;
194
+ readonly refsDir: string;
42
195
  readonly resolveContract: () => Contract;
196
+ readonly deserializeContract: (raw: unknown) => Contract;
43
197
  }): ContractSpaceMember {
44
- const { spaceId, packages, refs, headRef, resolveContract } = args;
198
+ const { spaceId, packages, refs, headRef, refsDir, resolveContract, deserializeContract } = args;
45
199
  let graphMemo: MigrationGraph | undefined;
46
200
  let contractMemo: Contract | undefined;
201
+ const contractAtMemo = new Map<string, ContractAtResult>();
202
+
203
+ function memberGraph(): MigrationGraph {
204
+ graphMemo ??= reconstructGraph(packages);
205
+ return graphMemo;
206
+ }
207
+
47
208
  return {
48
209
  spaceId,
49
210
  packages,
50
211
  refs,
51
212
  headRef,
52
- graph() {
53
- graphMemo ??= reconstructGraph(packages);
54
- return graphMemo;
55
- },
213
+ graph: memberGraph,
56
214
  contract() {
57
215
  contractMemo ??= resolveContract();
58
216
  return contractMemo;
59
217
  },
218
+ async contractAt(hash, opts) {
219
+ const key = contractAtMemoKey(hash, opts?.refName);
220
+ const cached = contractAtMemo.get(key);
221
+ if (cached) {
222
+ return cached;
223
+ }
224
+
225
+ const result = await resolveContractAt({
226
+ hash,
227
+ opts,
228
+ refsDir,
229
+ packages,
230
+ graph: memberGraph(),
231
+ deserializeContract,
232
+ });
233
+ contractAtMemo.set(key, result);
234
+ return result;
235
+ },
60
236
  };
61
237
  }
62
238
 
@@ -58,7 +58,7 @@ export async function loadContractSpaceAggregate(
58
58
  const { migrationsDir, deserializeContract, appContract } = input;
59
59
  const targetId = appContract.target;
60
60
 
61
- const appState = await loadAppSpace(migrationsDir, appContract);
61
+ const appState = await loadAppSpace(migrationsDir, appContract, deserializeContract);
62
62
  const extensionStates = await loadExtensionSpaces(migrationsDir, deserializeContract);
63
63
 
64
64
  const spaces: readonly IntegritySpaceState[] = [appState, ...extensionStates];
@@ -74,6 +74,7 @@ export async function loadContractSpaceAggregate(
74
74
  async function loadAppSpace(
75
75
  migrationsDir: string,
76
76
  appContract: Contract,
77
+ deserializeContract: (raw: unknown) => Contract,
77
78
  ): Promise<IntegritySpaceState> {
78
79
  const spaceDir = spaceMigrationDirectory(migrationsDir, APP_SPACE_ID);
79
80
  const { packages, problems } = await readMigrationsDir(spaceDir);
@@ -84,7 +85,9 @@ async function loadAppSpace(
84
85
  packages,
85
86
  refs,
86
87
  headRef: { hash: appContract.storage.storageHash, invariants: [] },
88
+ refsDir: spaceRefsDirectory(spaceDir),
87
89
  resolveContract: () => appContract,
90
+ deserializeContract,
88
91
  });
89
92
 
90
93
  // The app head ref is synthesised from the live contract, so there is
@@ -126,7 +129,9 @@ async function loadExtensionSpace(
126
129
  packages,
127
130
  refs,
128
131
  headRef,
132
+ refsDir: spaceRefsDirectory(spaceDir),
129
133
  resolveContract: () => deserializeContract(rawContract()),
134
+ deserializeContract,
130
135
  });
131
136
 
132
137
  return { member, problems, refProblems, headRefProblem, isApp: false };
@@ -5,6 +5,27 @@ import type { OnDiskMigrationPackage } from '../package';
5
5
  import type { Refs } from '../refs';
6
6
  import type { ContractSpaceHeadRecord } from '../verify-contract-spaces';
7
7
 
8
+ export interface ContractAtOptions {
9
+ readonly refName?: string;
10
+ }
11
+
12
+ export type ContractAtResult =
13
+ | {
14
+ readonly provenance: 'snapshot';
15
+ readonly hash: string;
16
+ readonly contractJson: unknown;
17
+ readonly contractDts: string;
18
+ readonly contract: Contract;
19
+ }
20
+ | {
21
+ readonly provenance: 'graph-node';
22
+ readonly sourceDir: string;
23
+ readonly hash: string;
24
+ readonly contractJson: unknown;
25
+ readonly contractDts: string;
26
+ readonly contract: Contract;
27
+ };
28
+
8
29
  /**
9
30
  * One contract space — app or extension — as a member of a
10
31
  * {@link ContractSpaceAggregate}. Every member has the same shape.
@@ -35,6 +56,11 @@ import type { ContractSpaceHeadRecord } from '../verify-contract-spaces';
35
56
  * `deserializeContract`. Throws if the on-disk contract is missing or
36
57
  * undeserializable (surfaced as `contractUnreadable` by `checkIntegrity`
37
58
  * under `checkContracts`); callers gate before querying it.
59
+ * - `contractAt(hash, opts?)`: materializes the contract at an arbitrary
60
+ * graph node — when `opts.refName` is set, prefer the ref's paired
61
+ * snapshot; else find the package whose `metadata.to === hash` and read
62
+ * its `end-contract.*`. Lazy per `(hash, refName?)` memoisation; throws
63
+ * typed {@link MigrationToolsError} values compatible with CLI mappers.
38
64
  */
39
65
  export interface ContractSpaceMember {
40
66
  readonly spaceId: string;
@@ -43,6 +69,7 @@ export interface ContractSpaceMember {
43
69
  readonly headRef: ContractSpaceHeadRecord | null;
44
70
  graph(): MigrationGraph;
45
71
  contract(): Contract;
72
+ contractAt(hash: string, opts?: ContractAtOptions): Promise<ContractAtResult>;
46
73
  }
47
74
 
48
75
  /**
package/src/errors.ts CHANGED
@@ -401,6 +401,47 @@ export function errorMigrationHashMismatch(
401
401
  });
402
402
  }
403
403
 
404
+ export function errorSnapshotMissing(refName: string): MigrationToolsError {
405
+ return new MigrationToolsError(
406
+ 'MIGRATION.SNAPSHOT_MISSING',
407
+ `Ref "${refName}" has no paired contract snapshot`,
408
+ {
409
+ why: `Ref "${refName}" exists but its paired snapshot files are missing.`,
410
+ fix: `Run "prisma-next db update --advance-ref ${refName}" to repopulate the snapshot, or "prisma-next ref delete ${refName}" to clear the orphan pointer.`,
411
+ details: { refName, identifier: refName, viaRef: true },
412
+ },
413
+ );
414
+ }
415
+
416
+ export function errorBundleNotFoundForGraphNode(
417
+ hash: string,
418
+ explicitLabel?: string,
419
+ ): MigrationToolsError {
420
+ const summary = explicitLabel
421
+ ? `No migration bundle found for reference "${explicitLabel}" (resolved hash: ${hash})`
422
+ : `No migration bundle found for graph node ${hash}`;
423
+ return new MigrationToolsError('MIGRATION.BUNDLE_NOT_FOUND_FOR_GRAPH_NODE', summary, {
424
+ why: `The hash ${hash} is a graph node but no on-disk migration package has an end-contract hash matching it.`,
425
+ fix: 'Provide a ref or hash that corresponds to an existing migration package, or run `migration list` to see available migrations.',
426
+ details: { hash, ...(explicitLabel ? { explicitLabel } : {}) },
427
+ });
428
+ }
429
+
430
+ export function errorContractDeserializationFailed(
431
+ filePath: string,
432
+ message: string,
433
+ ): MigrationToolsError {
434
+ return new MigrationToolsError(
435
+ 'MIGRATION.CONTRACT_DESERIALIZATION_FAILED',
436
+ 'Contract failed to deserialize',
437
+ {
438
+ why: `Contract at "${filePath}" failed to deserialize: ${message}`,
439
+ fix: reemitHint(dirname(filePath), 'or restore the directory from version control.'),
440
+ details: { filePath, message },
441
+ },
442
+ );
443
+ }
444
+
404
445
  export function errorHashNotInGraph(hash: string, graph: MigrationGraph): MigrationToolsError {
405
446
  const reachableHashes = [...graph.nodes].sort();
406
447
  const reachableList = reachableHashes.length > 0 ? reachableHashes.join(', ') : '(none)';
@@ -28,7 +28,12 @@ export {
28
28
  type GraphWalkStrategyInputs,
29
29
  graphWalkStrategy,
30
30
  } from '../aggregate/strategies/graph-walk';
31
- export type { ContractSpaceAggregate, ContractSpaceMember } from '../aggregate/types';
31
+ export type {
32
+ ContractAtOptions,
33
+ ContractAtResult,
34
+ ContractSpaceAggregate,
35
+ ContractSpaceMember,
36
+ } from '../aggregate/types';
32
37
  export {
33
38
  type AggregateVerifierError,
34
39
  type AggregateVerifierInput,
@@ -1 +0,0 @@
1
- {"version":3,"file":"migration-graph.mjs","names":[],"sources":["../../src/graph-membership.ts"],"sourcesContent":["import { EMPTY_CONTRACT_HASH } from './constants';\nimport { errorHashNotInGraph } from './errors';\nimport type { MigrationGraph } from './graph';\n\nexport function isGraphNode(hash: string, graph: MigrationGraph): boolean {\n if (hash === EMPTY_CONTRACT_HASH) {\n return true;\n }\n return graph.nodes.has(hash);\n}\n\nexport function assertHashIsGraphNode(hash: string, graph: MigrationGraph): asserts hash is string {\n if (isGraphNode(hash, graph)) {\n return;\n }\n throw errorHashNotInGraph(hash, graph);\n}\n"],"mappings":";;;;AAIA,SAAgB,YAAY,MAAc,OAAgC;CACxE,IAAI,SAAA,gBACF,OAAO;CAET,OAAO,MAAM,MAAM,IAAI,IAAI;AAC7B;AAEA,SAAgB,sBAAsB,MAAc,OAA+C;CACjG,IAAI,YAAY,MAAM,KAAK,GACzB;CAEF,MAAM,oBAAoB,MAAM,KAAK;AACvC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"refs.mjs","names":[],"sources":["../../src/refs/snapshot.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { access, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';\nimport { canonicalizeJson } from '@prisma-next/framework-components/utils';\nimport { type } from 'arktype';\nimport { basename, dirname, join } from 'pathe';\nimport { errorInvalidRefFile, errorInvalidRefName, MigrationToolsError } from '../errors';\nimport { deleteRef, type RefEntry, validateRefName, writeRef } from '../refs';\n\nexport interface ContractIR {\n readonly contract: unknown;\n readonly contractDts: string;\n}\n\nconst ContractIrSchema = type({\n targetFamily: 'string',\n target: 'string',\n profileHash: 'string',\n storage: type({\n storageHash: 'string',\n }),\n models: 'object',\n});\n\nfunction hasErrnoCode(error: unknown, code: string): boolean {\n return error instanceof Error && (error as { code?: string }).code === code;\n}\n\nfunction snapshotJsonPath(refsDir: string, name: string): string {\n return join(refsDir, `${name}.contract.json`);\n}\n\nfunction snapshotDtsPath(refsDir: string, name: string): string {\n return join(refsDir, `${name}.contract.d.ts`);\n}\n\nfunction tmpPathFor(finalPath: string): string {\n const dir = dirname(finalPath);\n const fileName = basename(finalPath);\n return join(dir, `.${fileName}.${Date.now()}-${randomBytes(4).toString('hex')}.tmp`);\n}\n\nasync function atomicWriteFile(finalPath: string, content: string): Promise<void> {\n const dir = dirname(finalPath);\n await mkdir(dir, { recursive: true });\n const tmpPath = tmpPathFor(finalPath);\n await writeFile(tmpPath, content);\n await rename(tmpPath, finalPath);\n}\n\nasync function unlinkIfExists(filePath: string): Promise<void> {\n try {\n await unlink(filePath);\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) return;\n throw error;\n }\n}\n\nfunction parseContractSnapshotJson(filePath: string, raw: string): unknown {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw errorInvalidRefFile(filePath, 'Failed to parse as JSON');\n }\n\n const result = ContractIrSchema(parsed);\n if (result instanceof type.errors) {\n throw errorInvalidRefFile(filePath, result.summary);\n }\n\n return result;\n}\n\nexport async function writeRefSnapshot(\n refsDir: string,\n name: string,\n snapshot: ContractIR,\n): Promise<void> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n const jsonPath = snapshotJsonPath(refsDir, name);\n const dtsPath = snapshotDtsPath(refsDir, name);\n const jsonContent = `${canonicalizeJson(snapshot.contract)}\\n`;\n const dtsContent = snapshot.contractDts.endsWith('\\n')\n ? snapshot.contractDts\n : `${snapshot.contractDts}\\n`;\n\n try {\n await atomicWriteFile(jsonPath, jsonContent);\n } catch (error) {\n await unlinkIfExists(jsonPath);\n throw error;\n }\n\n try {\n await atomicWriteFile(dtsPath, dtsContent);\n } catch (error) {\n await unlinkIfExists(jsonPath);\n await unlinkIfExists(dtsPath);\n throw error;\n }\n}\n\nexport async function readRefSnapshot(refsDir: string, name: string): Promise<ContractIR | null> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n const jsonPath = snapshotJsonPath(refsDir, name);\n const dtsPath = snapshotDtsPath(refsDir, name);\n\n let raw: string;\n try {\n raw = await readFile(jsonPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n return null;\n }\n throw error;\n }\n\n const contract = parseContractSnapshotJson(jsonPath, raw);\n\n let contractDts: string;\n try {\n contractDts = await readFile(dtsPath, 'utf-8');\n } catch (error) {\n if (hasErrnoCode(error, 'ENOENT')) {\n throw errorInvalidRefFile(dtsPath, 'Missing paired contract.d.ts snapshot file');\n }\n throw error;\n }\n\n return { contract, contractDts };\n}\n\nexport async function deleteRefSnapshot(refsDir: string, name: string): Promise<void> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n await unlinkIfExists(snapshotJsonPath(refsDir, name));\n await unlinkIfExists(snapshotDtsPath(refsDir, name));\n}\n\nexport async function writeRefPaired(\n refsDir: string,\n name: string,\n entry: RefEntry,\n snapshot: ContractIR,\n): Promise<void> {\n await writeRefSnapshot(refsDir, name, snapshot);\n try {\n await writeRef(refsDir, name, entry);\n } catch (writeError) {\n try {\n await deleteRefSnapshot(refsDir, name);\n } catch {\n // Rollback failure is secondary; preserve the original writeRef error.\n }\n throw writeError;\n }\n}\n\nfunction isUnknownRefError(error: unknown): boolean {\n return MigrationToolsError.is(error) && error.code === 'MIGRATION.UNKNOWN_REF';\n}\n\nasync function snapshotFilesExist(refsDir: string, name: string): Promise<boolean> {\n if (!validateRefName(name)) {\n throw errorInvalidRefName(name);\n }\n\n const paths = [snapshotJsonPath(refsDir, name), snapshotDtsPath(refsDir, name)];\n const checks = await Promise.allSettled(paths.map((filePath) => access(filePath)));\n return checks.some((result) => result.status === 'fulfilled');\n}\n\nexport async function deleteRefPaired(refsDir: string, name: string): Promise<void> {\n if (await snapshotFilesExist(refsDir, name)) {\n try {\n await deleteRef(refsDir, name);\n } catch (error) {\n if (!isUnknownRefError(error)) {\n throw error;\n }\n }\n await deleteRefSnapshot(refsDir, name);\n return;\n }\n\n await deleteRef(refsDir, name);\n await deleteRefSnapshot(refsDir, name);\n}\n"],"mappings":"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"}