@prisma-next/migration-tools 0.8.0-dev.1 → 0.8.0-dev.10

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 (74) hide show
  1. package/README.md +6 -6
  2. package/dist/{errors-EPL_9p9f.mjs → errors-DGYwcwXs.mjs} +3 -15
  3. package/dist/errors-DGYwcwXs.mjs.map +1 -0
  4. package/dist/exports/aggregate.d.mts +7 -7
  5. package/dist/exports/aggregate.mjs +3 -3
  6. package/dist/exports/aggregate.mjs.map +1 -1
  7. package/dist/exports/errors.d.mts.map +1 -1
  8. package/dist/exports/errors.mjs +1 -1
  9. package/dist/exports/graph.d.mts +1 -1
  10. package/dist/exports/hash.d.mts +9 -7
  11. package/dist/exports/hash.d.mts.map +1 -1
  12. package/dist/exports/hash.mjs +1 -1
  13. package/dist/exports/invariants.d.mts +1 -1
  14. package/dist/exports/invariants.mjs +1 -1
  15. package/dist/exports/io.d.mts +13 -17
  16. package/dist/exports/io.d.mts.map +1 -1
  17. package/dist/exports/io.mjs +1 -1
  18. package/dist/exports/metadata.d.mts +1 -1
  19. package/dist/exports/migration-graph.d.mts +1 -1
  20. package/dist/exports/migration-graph.mjs +1 -1
  21. package/dist/exports/migration.d.mts +1 -1
  22. package/dist/exports/migration.d.mts.map +1 -1
  23. package/dist/exports/migration.mjs +3 -41
  24. package/dist/exports/migration.mjs.map +1 -1
  25. package/dist/exports/package.d.mts +1 -1
  26. package/dist/exports/ref-resolution.d.mts +100 -0
  27. package/dist/exports/ref-resolution.d.mts.map +1 -0
  28. package/dist/exports/ref-resolution.mjs +239 -0
  29. package/dist/exports/ref-resolution.mjs.map +1 -0
  30. package/dist/exports/refs.d.mts +2 -16
  31. package/dist/exports/refs.mjs +1 -147
  32. package/dist/exports/spaces.d.mts +1 -1
  33. package/dist/exports/spaces.mjs +4 -4
  34. package/dist/exports/spaces.mjs.map +1 -1
  35. package/dist/{graph-HMWAldoR.d.mts → graph-BrLXqoUc.d.mts} +1 -1
  36. package/dist/{graph-HMWAldoR.d.mts.map → graph-BrLXqoUc.d.mts.map} +1 -1
  37. package/dist/{hash-C6bpZczT.mjs → hash-Cr4WIr4Z.mjs} +10 -8
  38. package/dist/hash-Cr4WIr4Z.mjs.map +1 -0
  39. package/dist/{invariants-qgQGlsrV.mjs → invariants-0daYEzyo.mjs} +2 -2
  40. package/dist/{invariants-qgQGlsrV.mjs.map → invariants-0daYEzyo.mjs.map} +1 -1
  41. package/dist/{io-Dw620b51.mjs → io-BPLfzvZe.mjs} +16 -24
  42. package/dist/io-BPLfzvZe.mjs.map +1 -0
  43. package/dist/{migration-graph-DulOITvG.d.mts → migration-graph-De0dUZoC.d.mts} +3 -3
  44. package/dist/{migration-graph-DulOITvG.d.mts.map → migration-graph-De0dUZoC.d.mts.map} +1 -1
  45. package/dist/{migration-graph-DGNnKDY5.mjs → migration-graph-nlS4TRpn.mjs} +2 -2
  46. package/dist/{migration-graph-DGNnKDY5.mjs.map → migration-graph-nlS4TRpn.mjs.map} +1 -1
  47. package/dist/{package-BjiZ7KDy.d.mts → package-DZj8YvD0.d.mts} +1 -1
  48. package/dist/package-DZj8YvD0.d.mts.map +1 -0
  49. package/dist/{read-contract-space-contract-COyz4tZn.mjs → read-contract-space-contract-C4_goX0c.mjs} +3 -3
  50. package/dist/{read-contract-space-contract-COyz4tZn.mjs.map → read-contract-space-contract-C4_goX0c.mjs.map} +1 -1
  51. package/dist/refs-BDHo5l_g.mjs +148 -0
  52. package/dist/refs-BDHo5l_g.mjs.map +1 -0
  53. package/dist/refs-CDaNerhT.d.mts +16 -0
  54. package/dist/refs-CDaNerhT.d.mts.map +1 -0
  55. package/package.json +10 -6
  56. package/src/aggregate/planner-types.ts +2 -2
  57. package/src/aggregate/strategies/graph-walk.ts +1 -1
  58. package/src/compute-extension-space-apply-path.ts +1 -1
  59. package/src/errors.ts +1 -22
  60. package/src/exports/ref-resolution.ts +15 -0
  61. package/src/hash.ts +8 -12
  62. package/src/io.ts +12 -22
  63. package/src/migration-base.ts +1 -54
  64. package/src/refs/contract-ref.ts +103 -0
  65. package/src/refs/migration-ref.ts +121 -0
  66. package/src/refs/types.ts +93 -0
  67. package/src/refs.ts +3 -3
  68. package/dist/errors-EPL_9p9f.mjs.map +0 -1
  69. package/dist/exports/refs.d.mts.map +0 -1
  70. package/dist/exports/refs.mjs.map +0 -1
  71. package/dist/hash-C6bpZczT.mjs.map +0 -1
  72. package/dist/io-Dw620b51.mjs.map +0 -1
  73. package/dist/package-BjiZ7KDy.d.mts.map +0 -1
  74. /package/dist/{metadata-CFvm3ayn.d.mts → metadata-BFX0xdz8.d.mts} +0 -0
package/src/errors.ts CHANGED
@@ -111,27 +111,6 @@ export function errorInvalidOperationEntry(index: number, reason: string): Migra
111
111
  );
112
112
  }
113
113
 
114
- export function errorStaleContractBookends(args: {
115
- readonly side: 'from' | 'to';
116
- readonly metaHash: string | null;
117
- readonly contractHash: string;
118
- }): MigrationToolsError {
119
- const { side, metaHash, contractHash } = args;
120
- // `meta.from` is `string | null` (null = baseline). Render `null` as a
121
- // human-readable token in the diagnostic so the message stays clear when
122
- // the mismatch is a baseline-vs-non-baseline disagreement.
123
- const renderedMetaHash = metaHash === null ? 'null (baseline)' : `"${metaHash}"`;
124
- return new MigrationToolsError(
125
- 'MIGRATION.STALE_CONTRACT_BOOKENDS',
126
- 'Migration manifest contract bookends disagree with describe()',
127
- {
128
- 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\`.`,
129
- fix: 'Re-run `migration plan` to regenerate the package with fresh contract bookends, or restore the directory from version control.',
130
- details: { side, metaHash, contractHash },
131
- },
132
- );
133
- }
134
-
135
114
  export function errorInvalidSlug(slug: string): MigrationToolsError {
136
115
  return new MigrationToolsError('MIGRATION.INVALID_NAME', 'Invalid migration name', {
137
116
  why: `The slug "${slug}" contains no valid characters after sanitization (only a-z, 0-9 are kept).`,
@@ -220,7 +199,7 @@ export function errorAmbiguousTarget(
220
199
  : '';
221
200
  return new MigrationToolsError('MIGRATION.AMBIGUOUS_TARGET', 'Ambiguous migration target', {
222
201
  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}`,
223
- 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.',
202
+ fix: 'Use `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.',
224
203
  details: {
225
204
  branchTips,
226
205
  ...(context ? { divergencePoint: context.divergencePoint, branches: context.branches } : {}),
@@ -0,0 +1,15 @@
1
+ export { parseContractRef } from '../refs/contract-ref';
2
+ export { parseMigrationRef } from '../refs/migration-ref';
3
+ export type {
4
+ ContractRef,
5
+ ContractRefProvenance,
6
+ MigrationRef,
7
+ MigrationRefProvenance,
8
+ RefResolutionAmbiguous,
9
+ RefResolutionContext,
10
+ RefResolutionError,
11
+ RefResolutionInvalidFormat,
12
+ RefResolutionNotFound,
13
+ RefResolutionWrongGrammar,
14
+ } from '../refs/types';
15
+ export { findEdgeByDirName } from '../refs/types';
package/src/hash.ts CHANGED
@@ -15,11 +15,13 @@ function sha256Hex(input: string): string {
15
15
  }
16
16
 
17
17
  /**
18
- * Content-addressed migration hash over (metadata envelope sans
19
- * contracts/hints, ops). See ADR 199 — Storage-only migration identity
20
- * for the rationale: contracts are anchored separately by the
21
- * storage-hash bookends inside the envelope; planner hints are advisory
22
- * and must not affect identity.
18
+ * Content-addressed migration hash over (metadata envelope sans hints,
19
+ * ops). See ADR 199 — Storage-only migration identity for the
20
+ * rationale: the storage-hash bookends (`from`, `to`) inside the
21
+ * envelope anchor the contract identity by hash, and planner hints are
22
+ * advisory and must not affect identity. The full contract IRs are not
23
+ * part of the manifest — they live in sibling `*-contract.json` files
24
+ * authored alongside the migration, never inlined here.
23
25
  *
24
26
  * The integrity check is purely structural, not semantic. The function
25
27
  * canonicalizes its inputs via `sortKeys` (recursive) + `JSON.stringify`
@@ -44,13 +46,7 @@ export function computeMigrationHash(
44
46
  metadata: Omit<MigrationMetadata, 'migrationHash'> & { readonly migrationHash?: string },
45
47
  ops: MigrationOps,
46
48
  ): string {
47
- const {
48
- migrationHash: _migrationHash,
49
- fromContract: _fromContract,
50
- toContract: _toContract,
51
- hints: _hints,
52
- ...strippedMeta
53
- } = metadata;
49
+ const { migrationHash: _migrationHash, hints: _hints, ...strippedMeta } = metadata;
54
50
 
55
51
  const canonicalMetadata = canonicalizeJson(strippedMeta);
56
52
  const canonicalOps = canonicalizeJson(ops);
package/src/io.ts CHANGED
@@ -3,7 +3,6 @@ import type {
3
3
  MigrationMetadata,
4
4
  MigrationPackage,
5
5
  } from '@prisma-next/framework-components/control';
6
- import { canonicalizeJson } from '@prisma-next/framework-components/utils';
7
6
  import { type } from 'arktype';
8
7
  import { basename, dirname, join, resolve } from 'pathe';
9
8
  import {
@@ -40,8 +39,6 @@ const MigrationMetadataSchema = type({
40
39
  from: 'string > 0 | null',
41
40
  to: 'string',
42
41
  migrationHash: 'string',
43
- fromContract: 'object | null',
44
- toContract: 'object',
45
42
  hints: MigrationHintsSchema,
46
43
  labels: 'string[]',
47
44
  providedInvariants: 'string[]',
@@ -74,35 +71,31 @@ export async function writeMigrationPackage(
74
71
  * Materialise an in-memory {@link MigrationPackage} to a per-space
75
72
  * directory on disk.
76
73
  *
77
- * Writes three files under `<targetDir>/<pkg.dirName>/`:
74
+ * Writes two files under `<targetDir>/<pkg.dirName>/`:
78
75
  *
79
76
  * - `migration.json` — the manifest (pretty-printed, matches
80
77
  * {@link writeMigrationPackage}'s output for byte-for-byte parity with
81
78
  * app-space migrations).
82
79
  * - `ops.json` — the operation list (pretty-printed).
83
- * - `contract.json` — the canonical-JSON serialisation of
84
- * `metadata.toContract`. This is the per-package post-state contract
85
- * snapshot; the canonicalisation pass guarantees byte-determinism so
86
- * re-emitting the same package across machines / runs produces an
87
- * identical file.
88
80
  *
89
81
  * Distinct verb from the lower-level {@link writeMigrationPackage}
90
82
  * (which takes constituent `(metadata, ops)`): callers reading
91
- * `materialise…` know they are persisting a struct-typed package
92
- * including its contract-snapshot side car.
83
+ * `materialise…` know they are persisting a struct-typed package.
93
84
  *
94
85
  * Overwrite-idempotent: the per-package directory is cleared before
95
86
  * each emit, so re-running against the same `targetDir` produces
96
87
  * byte-identical contents and never leaves stale files behind. The
97
- * spec's "re-emitting the same package across runs / machines produces
98
- * byte-identical files" guarantee (§ 3) covers both same-dir and
99
- * fresh-dir re-emits. The lower-level {@link writeMigrationPackage}
100
- * stays strict because the CLI authoring path (`migration plan` /
101
- * `migration new`) deliberately refuses to clobber an existing
102
- * authored migration; this helper is the re-emit path that is
103
- * supposed to converge on a single canonical on-disk shape.
88
+ * lower-level {@link writeMigrationPackage} stays strict because the
89
+ * CLI authoring path (`migration plan` / `migration new`) deliberately
90
+ * refuses to clobber an existing authored migration; this helper is
91
+ * the re-emit path that is supposed to converge on a single canonical
92
+ * on-disk shape.
104
93
  *
105
- * @see specs/framework-mechanism.spec.md § 3 Emission helper (T1.7).
94
+ * The per-space head contract lives at
95
+ * `<projectMigrationsDir>/<spaceId>/contract.json` (written by
96
+ * {@link import('./emit-contract-space-artefacts').emitContractSpaceArtefacts}),
97
+ * not inside the per-package directory. The runner reads only
98
+ * `migration.json` + `ops.json` from each package.
106
99
  */
107
100
  export async function materialiseMigrationPackage(
108
101
  targetDir: string,
@@ -111,9 +104,6 @@ export async function materialiseMigrationPackage(
111
104
  const dir = join(targetDir, pkg.dirName);
112
105
  await rm(dir, { recursive: true, force: true });
113
106
  await writeMigrationPackage(dir, pkg.metadata, pkg.ops);
114
- await writeFile(join(dir, 'contract.json'), `${canonicalizeJson(pkg.metadata.toContract)}\n`, {
115
- flag: 'wx',
116
- });
117
107
  }
118
108
 
119
109
  /**
@@ -1,13 +1,12 @@
1
1
  import { realpathSync } from 'node:fs';
2
2
  import { fileURLToPath } from 'node:url';
3
- import type { Contract } from '@prisma-next/contract/types';
4
3
  import type {
5
4
  ControlStack,
6
5
  MigrationPlan,
7
6
  MigrationPlanOperation,
8
7
  } from '@prisma-next/framework-components/control';
9
8
  import { type } from 'arktype';
10
- import { errorInvalidOperationEntry, errorStaleContractBookends } from './errors';
9
+ import { errorInvalidOperationEntry } from './errors';
11
10
  import { computeMigrationHash } from './hash';
12
11
  import { deriveProvidedInvariants } from './invariants';
13
12
  import type { MigrationHints, MigrationMetadata } from './metadata';
@@ -145,21 +144,12 @@ function buildAttestedMetadata(
145
144
  ops: MigrationOps,
146
145
  existing: Partial<MigrationMetadata> | null,
147
146
  ): MigrationMetadata {
148
- assertBookendsMatchMeta(meta, existing);
149
-
150
147
  const baseMetadata: Omit<MigrationMetadata, 'migrationHash'> = {
151
148
  from: meta.from,
152
149
  to: meta.to,
153
150
  labels: meta.labels ?? existing?.labels ?? [],
154
151
  providedInvariants: deriveProvidedInvariants(ops),
155
152
  createdAt: existing?.createdAt ?? new Date().toISOString(),
156
- fromContract: existing?.fromContract ?? null,
157
- // When no scaffolded metadata exists we synthesize a minimal contract
158
- // stub so the package is still readable end-to-end. The cast is
159
- // intentional: only the storage bookend matters for hash computation
160
- // (everything else is stripped by `computeMigrationHash`), and a real
161
- // contract bookend would only be available after `migration plan`.
162
- toContract: existing?.toContract ?? ({ storage: { storageHash: meta.to } } as Contract),
163
153
  hints: normalizeHints(existing?.hints),
164
154
  };
165
155
 
@@ -167,49 +157,6 @@ function buildAttestedMetadata(
167
157
  return { ...baseMetadata, migrationHash };
168
158
  }
169
159
 
170
- /**
171
- * Verify each preserved contract bookend in `existing` agrees with the
172
- * corresponding side of `describe()`'s output. A mismatch indicates the
173
- * migration's `describe()` was edited after `migration plan` scaffolded
174
- * the package, leaving a self-inconsistent manifest. Failing fast at
175
- * write-time turns a silent foot-gun into an actionable diagnostic.
176
- *
177
- * Skipped when a side's `existing.<side>Contract` is null/absent (the
178
- * synthesis path stays open for origin-less initial migrations and for
179
- * bare `migration.ts` runs from scratch). When a bookend is *present*
180
- * but its `storage.storageHash` is missing, that's treated as a
181
- * mismatch — a malformed bookend is not equivalent to "no bookend".
182
- *
183
- * This check is paired with TML-2274, which removes `fromContract` /
184
- * `toContract` from the manifest entirely; once that lands, this
185
- * function and its error code are deleted.
186
- */
187
- function assertBookendsMatchMeta(
188
- meta: MigrationMeta,
189
- existing: Partial<MigrationMetadata> | null,
190
- ): void {
191
- if (existing?.fromContract != null) {
192
- const contractHash = existing.fromContract.storage?.storageHash ?? '';
193
- if (contractHash !== meta.from) {
194
- throw errorStaleContractBookends({
195
- side: 'from',
196
- metaHash: meta.from,
197
- contractHash,
198
- });
199
- }
200
- }
201
- if (existing?.toContract != null) {
202
- const contractHash = existing.toContract.storage?.storageHash ?? '';
203
- if (contractHash !== meta.to) {
204
- throw errorStaleContractBookends({
205
- side: 'to',
206
- metaHash: meta.to,
207
- contractHash,
208
- });
209
- }
210
- }
211
- }
212
-
213
160
  /**
214
161
  * Project `existing.hints` down to the known `MigrationHints` shape, dropping
215
162
  * any legacy keys that may linger in metadata scaffolded by older CLI
@@ -0,0 +1,103 @@
1
+ import type { Result } from '@prisma-next/utils/result';
2
+ import { notOk, ok } from '@prisma-next/utils/result';
3
+ import { validateRefName } from '../refs';
4
+ import type {
5
+ ContractRef,
6
+ ContractRefProvenance,
7
+ RefResolutionContext,
8
+ RefResolutionError,
9
+ } from './types';
10
+ import { findEdgeByDirName, isFullHash, isHexPrefix, normalizeHashPrefix } from './types';
11
+
12
+ /**
13
+ * Resolve a user-supplied string to a contract hash using the unified
14
+ * contract-reference grammar.
15
+ *
16
+ * Accepted forms:
17
+ * - Full storage hash (`sha256:<64 hex>` or `sha256:empty`)
18
+ * - Hex prefix (6+ hex chars, must uniquely identify one contract)
19
+ * - Ref name (looked up in the refs index)
20
+ * - Migration directory name (resolves to the migration's `to`-contract)
21
+ * - `<dir>^` (resolves to the migration's `from`-contract)
22
+ */
23
+ export function parseContractRef(
24
+ input: string,
25
+ ctx: RefResolutionContext,
26
+ ): Result<ContractRef, RefResolutionError> {
27
+ if (!input) {
28
+ return notOk({ kind: 'invalid-format', input, reason: 'Reference cannot be empty' });
29
+ }
30
+
31
+ if (isFullHash(input)) {
32
+ if (ctx.graph.nodes.has(input)) {
33
+ return ok({ hash: input, provenance: { kind: 'hash', input } });
34
+ }
35
+ return notOk({ kind: 'not-found', input, grammar: 'contract' });
36
+ }
37
+
38
+ if (input.endsWith('^')) {
39
+ const dirName = input.slice(0, -1);
40
+ if (!dirName) {
41
+ return notOk({ kind: 'invalid-format', input, reason: 'Missing directory name before ^' });
42
+ }
43
+ const edge = findEdgeByDirName(ctx.graph, dirName);
44
+ if (edge) {
45
+ return ok({ hash: edge.from, provenance: { kind: 'migration-from', dirName } });
46
+ }
47
+ return notOk({ kind: 'not-found', input, grammar: 'contract' });
48
+ }
49
+
50
+ type Candidate = { hash: string; provenance: ContractRefProvenance; label: string };
51
+ const candidates: Candidate[] = [];
52
+
53
+ if (validateRefName(input) && Object.hasOwn(ctx.refs, input)) {
54
+ const ref = ctx.refs[input];
55
+ if (ref) {
56
+ candidates.push({
57
+ hash: ref.hash,
58
+ provenance: { kind: 'ref', refName: input },
59
+ label: `ref "${input}"`,
60
+ });
61
+ }
62
+ }
63
+
64
+ const edge = findEdgeByDirName(ctx.graph, input);
65
+ if (edge) {
66
+ candidates.push({
67
+ hash: edge.to,
68
+ provenance: { kind: 'migration-to', dirName: input },
69
+ label: `migration directory "${input}"`,
70
+ });
71
+ }
72
+
73
+ if (isHexPrefix(input)) {
74
+ const prefix = normalizeHashPrefix(input);
75
+ const matches = [...ctx.graph.nodes].filter((n) => n.startsWith(prefix));
76
+ const [firstMatch] = matches;
77
+ if (matches.length === 1 && firstMatch !== undefined) {
78
+ candidates.push({
79
+ hash: firstMatch,
80
+ provenance: { kind: 'hash', input },
81
+ label: `hash prefix "${input}"`,
82
+ });
83
+ } else if (matches.length > 1) {
84
+ return notOk({ kind: 'ambiguous', input, candidates: matches, grammar: 'contract' });
85
+ }
86
+ }
87
+
88
+ const [firstCandidate] = candidates;
89
+ if (candidates.length === 1 && firstCandidate !== undefined) {
90
+ return ok({ hash: firstCandidate.hash, provenance: firstCandidate.provenance });
91
+ }
92
+
93
+ if (candidates.length > 1) {
94
+ return notOk({
95
+ kind: 'ambiguous',
96
+ input,
97
+ candidates: candidates.map((c) => c.label),
98
+ grammar: 'contract',
99
+ });
100
+ }
101
+
102
+ return notOk({ kind: 'not-found', input, grammar: 'contract' });
103
+ }
@@ -0,0 +1,121 @@
1
+ import type { Result } from '@prisma-next/utils/result';
2
+ import { notOk, ok } from '@prisma-next/utils/result';
3
+ import { validateRefName } from '../refs';
4
+ import type { MigrationRef, RefResolutionContext, RefResolutionError } from './types';
5
+ import { findEdgeByDirName, isFullHash, isHexPrefix, normalizeHashPrefix } from './types';
6
+
7
+ /**
8
+ * Resolve a user-supplied string to a migration using the migration-reference
9
+ * grammar.
10
+ *
11
+ * Accepted forms:
12
+ * - Migration directory name (e.g. `20260101-add-users`)
13
+ * - Migration hash (full or 6+ hex prefix)
14
+ *
15
+ * Wrong-grammar diagnostics are produced when the input matches a
16
+ * contract-grammar form (ref name, `<dir>^`, contract-only hash) so the
17
+ * user gets a targeted hint rather than a generic "not found".
18
+ */
19
+ export function parseMigrationRef(
20
+ input: string,
21
+ ctx: RefResolutionContext,
22
+ ): Result<MigrationRef, RefResolutionError> {
23
+ if (!input) {
24
+ return notOk({ kind: 'invalid-format', input, reason: 'Reference cannot be empty' });
25
+ }
26
+
27
+ if (input.endsWith('^')) {
28
+ return notOk({
29
+ kind: 'wrong-grammar',
30
+ input,
31
+ expectedGrammar: 'migration',
32
+ message: '`^` syntax addresses contracts, not migrations',
33
+ fix: 'Pass the migration directory name without `^`, or use a contract-accepting flag like `--to` or `--from`.',
34
+ });
35
+ }
36
+
37
+ if (validateRefName(input) && Object.hasOwn(ctx.refs, input)) {
38
+ return notOk({
39
+ kind: 'wrong-grammar',
40
+ input,
41
+ expectedGrammar: 'migration',
42
+ message: `"${input}" is a ref name, not a migration`,
43
+ fix: 'Refs point at contracts, not migrations. Use a migration directory name or migration hash.',
44
+ });
45
+ }
46
+
47
+ const edge = findEdgeByDirName(ctx.graph, input);
48
+ if (edge) {
49
+ return ok({
50
+ dirName: edge.dirName,
51
+ migrationHash: edge.migrationHash,
52
+ from: edge.from,
53
+ to: edge.to,
54
+ provenance: { kind: 'dir-name', dirName: input },
55
+ });
56
+ }
57
+
58
+ if (isFullHash(input)) {
59
+ const migEdge = ctx.graph.migrationByHash.get(input);
60
+ if (migEdge) {
61
+ return ok({
62
+ dirName: migEdge.dirName,
63
+ migrationHash: migEdge.migrationHash,
64
+ from: migEdge.from,
65
+ to: migEdge.to,
66
+ provenance: { kind: 'hash', input },
67
+ });
68
+ }
69
+ if (ctx.graph.nodes.has(input)) {
70
+ return notOk({
71
+ kind: 'wrong-grammar',
72
+ input,
73
+ expectedGrammar: 'migration',
74
+ message: 'Hash matched a contract but not a migration',
75
+ fix: 'Use a contract-accepting flag like `--to` or `--from` to reference contracts by hash. Pass `migration show <dir>` for a specific migration.',
76
+ });
77
+ }
78
+ return notOk({ kind: 'not-found', input, grammar: 'migration' });
79
+ }
80
+
81
+ if (isHexPrefix(input)) {
82
+ const prefix = normalizeHashPrefix(input);
83
+ const migMatches = [...ctx.graph.migrationByHash.entries()].filter(([hash]) =>
84
+ hash.startsWith(prefix),
85
+ );
86
+
87
+ const [firstMigMatch] = migMatches;
88
+ if (migMatches.length === 1 && firstMigMatch !== undefined) {
89
+ const [, matchedEdge] = firstMigMatch;
90
+ return ok({
91
+ dirName: matchedEdge.dirName,
92
+ migrationHash: matchedEdge.migrationHash,
93
+ from: matchedEdge.from,
94
+ to: matchedEdge.to,
95
+ provenance: { kind: 'hash', input },
96
+ });
97
+ }
98
+
99
+ if (migMatches.length > 1) {
100
+ return notOk({
101
+ kind: 'ambiguous',
102
+ input,
103
+ candidates: migMatches.map(([hash]) => hash),
104
+ grammar: 'migration',
105
+ });
106
+ }
107
+
108
+ const contractMatches = [...ctx.graph.nodes].filter((n) => n.startsWith(prefix));
109
+ if (contractMatches.length > 0) {
110
+ return notOk({
111
+ kind: 'wrong-grammar',
112
+ input,
113
+ expectedGrammar: 'migration',
114
+ message: 'Hash matched a contract but not a migration',
115
+ fix: 'Use a contract-accepting flag like `--to` or `--from` to reference contracts by hash. Pass `migration show <dir>` for a specific migration.',
116
+ });
117
+ }
118
+ }
119
+
120
+ return notOk({ kind: 'not-found', input, grammar: 'migration' });
121
+ }
@@ -0,0 +1,93 @@
1
+ import type { MigrationEdge, MigrationGraph } from '../graph';
2
+ import type { Refs } from '../refs';
3
+
4
+ /** Context required to resolve a contract or migration reference. */
5
+ export interface RefResolutionContext {
6
+ readonly graph: MigrationGraph;
7
+ readonly refs: Refs;
8
+ }
9
+
10
+ export type ContractRefProvenance =
11
+ | { readonly kind: 'hash'; readonly input: string }
12
+ | { readonly kind: 'ref'; readonly refName: string }
13
+ | { readonly kind: 'migration-to'; readonly dirName: string }
14
+ | { readonly kind: 'migration-from'; readonly dirName: string };
15
+
16
+ /** A resolved contract reference: the target hash and how it was derived. */
17
+ export interface ContractRef {
18
+ readonly hash: string;
19
+ readonly provenance: ContractRefProvenance;
20
+ }
21
+
22
+ export type MigrationRefProvenance =
23
+ | { readonly kind: 'dir-name'; readonly dirName: string }
24
+ | { readonly kind: 'hash'; readonly input: string };
25
+
26
+ /** A resolved migration reference. */
27
+ export interface MigrationRef {
28
+ readonly dirName: string;
29
+ readonly migrationHash: string;
30
+ readonly from: string;
31
+ readonly to: string;
32
+ readonly provenance: MigrationRefProvenance;
33
+ }
34
+
35
+ export interface RefResolutionNotFound {
36
+ readonly kind: 'not-found';
37
+ readonly input: string;
38
+ readonly grammar: 'contract' | 'migration';
39
+ }
40
+
41
+ export interface RefResolutionAmbiguous {
42
+ readonly kind: 'ambiguous';
43
+ readonly input: string;
44
+ readonly candidates: readonly string[];
45
+ readonly grammar: 'contract' | 'migration';
46
+ }
47
+
48
+ export interface RefResolutionWrongGrammar {
49
+ readonly kind: 'wrong-grammar';
50
+ readonly input: string;
51
+ readonly expectedGrammar: 'contract' | 'migration';
52
+ readonly message: string;
53
+ readonly fix: string;
54
+ }
55
+
56
+ export interface RefResolutionInvalidFormat {
57
+ readonly kind: 'invalid-format';
58
+ readonly input: string;
59
+ readonly reason: string;
60
+ }
61
+
62
+ export type RefResolutionError =
63
+ | RefResolutionNotFound
64
+ | RefResolutionAmbiguous
65
+ | RefResolutionWrongGrammar
66
+ | RefResolutionInvalidFormat;
67
+
68
+ const FULL_HASH_PATTERN = /^sha256:([0-9a-f]{64}|empty)$/;
69
+ const HEX_PREFIX_PATTERN = /^(sha256:)?[0-9a-f]{6,}$/;
70
+
71
+ export function isFullHash(input: string): boolean {
72
+ return FULL_HASH_PATTERN.test(input);
73
+ }
74
+
75
+ export function isHexPrefix(input: string): boolean {
76
+ return HEX_PREFIX_PATTERN.test(input);
77
+ }
78
+
79
+ export function normalizeHashPrefix(input: string): string {
80
+ return input.startsWith('sha256:') ? input : `sha256:${input}`;
81
+ }
82
+
83
+ export function findEdgeByDirName(
84
+ graph: MigrationGraph,
85
+ dirName: string,
86
+ ): MigrationEdge | undefined {
87
+ for (const edges of graph.forwardChain.values()) {
88
+ for (const edge of edges) {
89
+ if (edge.dirName === dirName) return edge;
90
+ }
91
+ }
92
+ return undefined;
93
+ }
package/src/refs.ts CHANGED
@@ -61,7 +61,7 @@ export async function readRef(refsDir: string, name: string): Promise<RefEntry>
61
61
  if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
62
62
  throw new MigrationToolsError('MIGRATION.UNKNOWN_REF', `Unknown ref "${name}"`, {
63
63
  why: `No ref file found at "${filePath}".`,
64
- fix: `Create the ref with: prisma-next migration ref set ${name} <hash>`,
64
+ fix: `Create the ref with: prisma-next ref set ${name} <hash>`,
65
65
  details: { refName: name, filePath },
66
66
  });
67
67
  }
@@ -166,7 +166,7 @@ export async function deleteRef(refsDir: string, name: string): Promise<void> {
166
166
  if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
167
167
  throw new MigrationToolsError('MIGRATION.UNKNOWN_REF', `Unknown ref "${name}"`, {
168
168
  why: `No ref file found at "${filePath}".`,
169
- fix: 'Run `prisma-next migration ref list` to see available refs.',
169
+ fix: 'Run `prisma-next ref list` to see available refs.',
170
170
  details: { refName: name, filePath },
171
171
  });
172
172
  }
@@ -203,7 +203,7 @@ export function resolveRef(refs: Refs, name: string): RefEntry {
203
203
  if (!Object.hasOwn(refs, name)) {
204
204
  throw new MigrationToolsError('MIGRATION.UNKNOWN_REF', `Unknown ref "${name}"`, {
205
205
  why: `No ref named "${name}" exists.`,
206
- fix: `Available refs: ${Object.keys(refs).join(', ') || '(none)'}. Create a ref with: prisma-next migration ref set ${name} <hash>`,
206
+ fix: `Available refs: ${Object.keys(refs).join(', ') || '(none)'}. Create a ref with: prisma-next ref set ${name} <hash>`,
207
207
  details: { refName: name, availableRefs: Object.keys(refs) },
208
208
  });
209
209
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"errors-EPL_9p9f.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 errorDescriptorHeadHashMismatch(args: {\n readonly extensionId: string;\n readonly recomputedHash: string;\n readonly headRefHash: string;\n}): MigrationToolsError {\n const { extensionId, recomputedHash, headRefHash } = args;\n return new MigrationToolsError(\n 'MIGRATION.DESCRIPTOR_HEAD_HASH_MISMATCH',\n \"Extension descriptor's headRef.hash does not match its contractJson\",\n {\n why: `Extension \"${extensionId}\" publishes a \\`contractSpace\\` whose \\`headRef.hash\\` (${headRefHash}) does not match the canonical hash recomputed from \\`contractSpace.contractJson\\` (${recomputedHash}). This means the extension descriptor was published with stale \\`headRef.hash\\` — typically because the contract was bumped without rerunning the extension's emit pipeline.`,\n fix: 'Re-run the extension authoring pipeline so `contractJson.storage.storageHash` and `headRef.hash` agree, then republish the extension. If you are the extension author and you intentionally bumped `contractJson`, recompute and update `headRef.hash` (and refresh any on-disk migration metadata that derives from it).',\n details: { extensionId, recomputedHash, headRefHash },\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":";;;;;;;;;;;;;AAaA,SAAS,WAAW,KAAa,UAA2B;CAE1D,MAAM,SAAS,0CADK,SAAS,QAAQ,KAAK,EAAE,IACwB,CAAC;CACrE,OAAO,WAAW,GAAG,OAAO,IAAI,aAAa,GAAG,OAAO;;;;;;;;;;;;;;;;;;AAmBzD,IAAa,sBAAb,cAAyC,MAAM;CAC7C;CACA,WAAoB;CACpB;CACA;CACA;CAEA,YACE,MACA,SACA,SAKA;EACA,MAAM,QAAQ;EACd,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK,MAAM,QAAQ;EACnB,KAAK,MAAM,QAAQ;EACnB,KAAK,UAAU,QAAQ;;CAGzB,OAAO,GAAG,OAA8C;EACtD,IAAI,EAAE,iBAAiB,QAAQ,OAAO;EACtC,MAAM,YAAY;EAClB,OAAO,UAAU,SAAS,yBAAyB,OAAO,UAAU,SAAS;;;AAIjF,SAAgB,qBAAqB,KAAkC;CACrE,OAAO,IAAI,oBAAoB,wBAAwB,sCAAsC;EAC3F,KAAK,kBAAkB,IAAI;EAC3B,KAAK;EACL,SAAS,EAAE,KAAK;EACjB,CAAC;;AAGJ,SAAgB,iBAAiB,MAAc,KAAkC;CAC/E,OAAO,IAAI,oBAAoB,0BAA0B,WAAW,QAAQ;EAC1E,KAAK,aAAa,KAAK,QAAQ,IAAI;EACnC,KAAK,WACH,KACA,0FACD;EACD,SAAS;GAAE;GAAM;GAAK;EACvB,CAAC;;AAGJ,SAAgB,iBAAiB,UAAkB,YAAyC;CAC1F,OAAO,IAAI,oBAAoB,0BAA0B,kCAAkC;EACzF,KAAK,oBAAoB,SAAS,KAAK;EACvC,KAAK,WAAW,QAAQ,SAAS,EAAE,iDAAiD;EACpF,SAAS;GAAE;GAAU;GAAY;EAClC,CAAC;;AAGJ,SAAgB,qBAAqB,UAAkB,QAAqC;CAC1F,OAAO,IAAI,oBAAoB,8BAA8B,8BAA8B;EACzF,KAAK,0BAA0B,SAAS,gBAAgB;EACxD,KAAK,WAAW,QAAQ,SAAS,EAAE,iDAAiD;EACpF,SAAS;GAAE;GAAU;GAAQ;EAC9B,CAAC;;AAGJ,SAAgB,2BAA2B,OAAe,QAAqC;CAC7F,OAAO,IAAI,oBACT,qCACA,0CACA;EACE,KAAK,sBAAsB,MAAM,6DAA6D,OAAO;EACrG,KAAK;EACL,SAAS;GAAE;GAAO;GAAQ;EAC3B,CACF;;AAGH,SAAgB,2BAA2B,MAInB;CACtB,MAAM,EAAE,MAAM,UAAU,iBAAiB;CAKzC,OAAO,IAAI,oBACT,qCACA,iEACA;EACE,KAAK,yBAAyB,KAAK,gCAAgC,aAAa,kCAAkC,KAAK,KALlG,aAAa,OAAO,oBAAoB,IAAI,SAAS,GAKmE;EAC7I,KAAK;EACL,SAAS;GAAE;GAAM;GAAU;GAAc;EAC1C,CACF;;AAGH,SAAgB,iBAAiB,MAAmC;CAClE,OAAO,IAAI,oBAAoB,0BAA0B,0BAA0B;EACjF,KAAK,aAAa,KAAK;EACvB,KAAK;EACL,SAAS,EAAE,MAAM;EAClB,CAAC;;AAGJ,SAAgB,qBAAqB,UAAuC;CAC1E,OAAO,IAAI,oBAAoB,+BAA+B,iCAAiC;EAC7F,KAAK,yBAAyB,SAAS;EACvC,KAAK;EACL,SAAS,EAAE,UAAU;EACtB,CAAC;;AAGJ,SAAgB,oBAAoB,SAAsC;CACxE,OAAO,IAAI,oBACT,8BACA,qCACA;EACE,KAAK,iBAAiB,QAAQ;EAC9B,KAAK;EACL,SAAS,EAAE,SAAS;EACrB,CACF;;AAGH,SAAgB,gCAAgC,MAIxB;CACtB,MAAM,EAAE,aAAa,gBAAgB,gBAAgB;CACrD,OAAO,IAAI,oBACT,2CACA,uEACA;EACE,KAAK,cAAc,YAAY,0DAA0D,YAAY,sFAAsF,eAAe;EAC1M,KAAK;EACL,SAAS;GAAE;GAAa;GAAgB;GAAa;EACtD,CACF;;AAGH,SAAgB,sBAAsB,SAAsC;CAC1E,OAAO,IAAI,oBACT,gCACA,uCACA;EACE,KAAK,iBAAiB,QAAQ;EAC9B,KAAK;EACL,SAAS,EAAE,SAAS;EACrB,CACF;;AAGH,SAAgB,yBAAyB,KAAa,MAAmC;CACvF,MAAM,UAAU,SAAS,IAAI;CAC7B,OAAO,IAAI,oBACT,oCACA,0EACA;EACE,KAAK,cAAc,QAAQ,yBAAyB,KAAK;EACzD,KAAK,WACH,KACA,6HACD;EACD,SAAS;GAAE;GAAS;GAAM;EAC3B,CACF;;AAGH,SAAgB,qBACd,YACA,SAOqB;CACrB,MAAM,iBAAiB,UACnB,uBAAuB,QAAQ,gBAAgB,eAAe,QAAQ,SAAS,KAAK,MAAM,OAAO,EAAE,IAAI,IAAI,EAAE,MAAM,OAAO,YAAY,EAAE,MAAM,KAAK,MAAM,EAAE,QAAQ,CAAC,KAAK,MAAM,IAAI,SAAS,GAAG,CAAC,KAAK,KAAK,KAC1M;CACJ,OAAO,IAAI,oBAAoB,8BAA8B,8BAA8B;EACzF,KAAK,8DAA8D,WAAW,KAAK,KAAK,CAAC,4FAA4F;EACrL,KAAK;EACL,SAAS;GACP;GACA,GAAI,UAAU;IAAE,iBAAiB,QAAQ;IAAiB,UAAU,QAAQ;IAAU,GAAG,EAAE;GAC5F;EACF,CAAC;;AAGJ,SAAgB,wBAAwB,OAA+C;CACrF,OAAO,IAAI,oBAAoB,kCAAkC,8BAA8B;EAC7F,KAAK,oEAAoE,MAAM,KAAK,KAAK,CAAC;EAC1F,KAAK;EACL,SAAS,EAAE,OAAO;EACnB,CAAC;;AAWJ,SAAgB,oBAAoB,UAAkB,QAAqC;CACzF,OAAO,IAAI,oBAAoB,8BAA8B,oBAAoB;EAC/E,KAAK,gBAAgB,SAAS,gBAAgB;EAC9C,KAAK;EACL,SAAS;GAAE,MAAM;GAAU;GAAQ;EACpC,CAAC;;AAGJ,SAAgB,oBAAoB,SAAsC;CACxE,OAAO,IAAI,oBAAoB,8BAA8B,oBAAoB;EAC/E,KAAK,aAAa,QAAQ;EAC1B,KAAK;EACL,SAAS,EAAE,SAAS;EACrB,CAAC;;AAGJ,SAAgB,cAAc,iBAAyD;CACrF,OAAO,IAAI,oBAAoB,uBAAuB,yCAAyC;EAC7F,KAAK,wGAAwG,gBAAgB,KAAK,KAAK,CAAC;EACxI,KAAK;EACL,SAAS,EAAE,iBAAiB;EAC7B,CAAC;;AAGJ,SAAgB,qBAAqB,OAAoC;CACvE,OAAO,IAAI,oBAAoB,+BAA+B,qBAAqB;EACjF,KAAK,cAAc,MAAM;EACzB,KAAK;EACL,SAAS,EAAE,OAAO;EACnB,CAAC;;AAGJ,SAAgB,4BAA4B,eAA4C;CACtF,OAAO,IAAI,oBACT,sCACA,8CACA;EACE,KAAK,4CAA4C,cAAc;EAC/D,KAAK;EACL,SAAS,EAAE,eAAe;EAC3B,CACF;;AAGH,SAAgB,wBAAwB,aAA0C;CAChF,OAAO,IAAI,oBAAoB,kCAAkC,uBAAuB;EACtF,KAAK,eAAe,KAAK,UAAU,YAAY,CAAC;EAChD,KAAK;EACL,SAAS,EAAE,aAAa;EACzB,CAAC;;AAGJ,SAAgB,8BAA8B,aAA0C;CACtF,OAAO,IAAI,oBACT,yCACA,+CACA;EACE,KAAK,gBAAgB,YAAY;EACjC,KAAK;EACL,SAAS,EAAE,aAAa;EACzB,CACF;;AAGH,SAAgB,gCACd,UACA,QACA,SACqB;CACrB,MAAM,YAAY,IAAI,IAAI,OAAO;CACjC,MAAM,aAAa,IAAI,IAAI,QAAQ;CACnC,MAAM,UAAU,CAAC,GAAG,WAAW,CAAC,QAAQ,OAAO,CAAC,UAAU,IAAI,GAAG,CAAC;CAClE,MAAM,QAAQ,CAAC,GAAG,UAAU,CAAC,QAAQ,OAAO,CAAC,WAAW,IAAI,GAAG,CAAC;CAUhE,OAAO,IAAI,oBACT,0CACA,gEACA;EACE,KARiB,QAAQ,WAAW,KAAK,MAAM,WAAW,IAE1D,sBAAsB,SAAS,8BAA8B,KAAK,UAAU,OAAO,CAAC,qDAAqD,KAAK,UAAU,QAAQ,CAAC,mFACjK,sBAAsB,SAAS,8BAA8B,KAAK,UAAU,OAAO,CAAC,2CAA2C,KAAK,UAAU,QAAQ,CAAC;EAMvJ,KAAK,WAAW,QAAQ,SAAS,EAAE,iDAAiD;EACpF,SAAS;GAAE;GAAU;GAAQ;GAAS,YAAY;IAAE;IAAS;IAAO;GAAE;EACvE,CACF;;AAsBH,SAAgB,qBAAqB,MAKb;CACtB,MAAM,EAAE,SAAS,UAAU,SAAS,mBAAmB;CACvD,MAAM,YAAY,UAAU,QAAQ,QAAQ,KAAK;CACjD,MAAM,cAAc,QAAQ,KAAK,OAAO,KAAK,UAAU,GAAG,CAAC,CAAC,KAAK,KAAK;CAEtE,OAAO,IAAI,oBACT,+BACA,0CACA;EACE,KAAK,GAAG,UAAU,mEALD,SAAS,KAAK,OAAO,KAAK,UAAU,GAAG,CAAC,CAAC,KAAK,KAKkC,CAAC,cAAc,YAAY;EAC5H,KAAK;EACL,SAAS;GACP;GACA;GACA;GACA,GAAG,UAAU,WAAW,QAAQ;GACjC;EACF,CACF;;AAGH,SAAgB,sBAAsB,MAId;CACtB,MAAM,EAAE,SAAS,SAAS,aAAa;CAGvC,OAAO,IAAI,oBACT,+BACA,8DACA;EACE,KAAK,GANS,UAAU,QAAQ,QAAQ,cAAc,WAMpC,2DALF,QAAQ,KAAK,OAAO,KAAK,UAAU,GAAG,CAAC,CAAC,KAAK,KAK2B,CAAC;EACzF,KAAK;EACL,SAAS;GACP;GACA;GACA,GAAG,UAAU,WAAW,QAAQ;GACjC;EACF,CACF;;AAGH,SAAgB,2BACd,KACA,YACA,cACqB;CAKrB,OAAO,IAAI,oBAAoB,2BAA2B,gCAAgC;EACxF,KAAK,yBAAyB,WAAW,wCAAwC,aAAa,SAF5E,SAAS,QAAQ,KAAK,EAAE,IAEwE,CAAC;EACnH,KAAK,WAAW,KAAK,iDAAiD;EACtE,SAAS;GAAE;GAAK;GAAY;GAAc;EAC3C,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"refs.d.mts","names":[],"sources":["../../src/refs.ts"],"mappings":";UAUiB,QAAA;EAAA,SACN,IAAA;EAAA,SACA,UAAA;AAAA;AAAA,KAGC,IAAA,GAAO,QAAA,CAAS,MAAA,SAAe,QAAA;AAAA,iBAK3B,eAAA,CAAgB,IAAA;AAAA,iBAQhB,gBAAA,CAAiB,KAAA;AAAA,iBAsBX,OAAA,CAAQ,OAAA,UAAiB,IAAA,WAAe,OAAA,CAAQ,QAAA;AAAA,iBAmChD,QAAA,CAAS,OAAA,WAAkB,OAAA,CAAQ,IAAA;AAAA,iBAmDnC,QAAA,CAAS,OAAA,UAAiB,IAAA,UAAc,KAAA,EAAO,QAAA,GAAW,OAAA;AAAA,iBAoB1D,SAAA,CAAU,OAAA,UAAiB,IAAA,WAAe,OAAA;AAAA,iBAsChD,UAAA,CAAW,IAAA,EAAM,IAAA,EAAM,IAAA,WAAe,QAAA"}