@prisma-next/migration-tools 0.11.0 → 0.12.0

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 (118) hide show
  1. package/README.md +4 -4
  2. package/dist/{errors-DGYwcwXs.mjs → errors-vFROOhCR.mjs} +46 -21
  3. package/dist/errors-vFROOhCR.mjs.map +1 -0
  4. package/dist/exports/aggregate.d.mts +328 -204
  5. package/dist/exports/aggregate.d.mts.map +1 -1
  6. package/dist/exports/aggregate.mjs +480 -243
  7. package/dist/exports/aggregate.mjs.map +1 -1
  8. package/dist/exports/errors.d.mts +2 -2
  9. package/dist/exports/errors.d.mts.map +1 -1
  10. package/dist/exports/errors.mjs +1 -1
  11. package/dist/exports/graph.d.mts +1 -1
  12. package/dist/exports/hash.d.mts +8 -9
  13. package/dist/exports/hash.d.mts.map +1 -1
  14. package/dist/exports/hash.mjs +1 -1
  15. package/dist/exports/invariants.d.mts +1 -1
  16. package/dist/exports/invariants.d.mts.map +1 -1
  17. package/dist/exports/invariants.mjs +1 -1
  18. package/dist/exports/io.d.mts +2 -83
  19. package/dist/exports/io.mjs +1 -1
  20. package/dist/exports/metadata.d.mts +2 -2
  21. package/dist/exports/migration-graph.d.mts +9 -2
  22. package/dist/exports/migration-graph.d.mts.map +1 -0
  23. package/dist/exports/migration-graph.mjs +3 -2
  24. package/dist/exports/migration-ts.d.mts.map +1 -1
  25. package/dist/exports/migration-ts.mjs.map +1 -1
  26. package/dist/exports/migration.d.mts +5 -6
  27. package/dist/exports/migration.d.mts.map +1 -1
  28. package/dist/exports/migration.mjs +14 -32
  29. package/dist/exports/migration.mjs.map +1 -1
  30. package/dist/exports/package.d.mts +1 -1
  31. package/dist/exports/ref-resolution.d.mts +2 -2
  32. package/dist/exports/ref-resolution.d.mts.map +1 -1
  33. package/dist/exports/ref-resolution.mjs +1 -1
  34. package/dist/exports/ref-resolution.mjs.map +1 -1
  35. package/dist/exports/refs.d.mts +15 -2
  36. package/dist/exports/refs.d.mts.map +1 -0
  37. package/dist/exports/refs.mjs +3 -2
  38. package/dist/exports/spaces.d.mts +31 -132
  39. package/dist/exports/spaces.d.mts.map +1 -1
  40. package/dist/exports/spaces.mjs +13 -9
  41. package/dist/exports/spaces.mjs.map +1 -1
  42. package/dist/{graph-BrLXqoUc.d.mts → graph-3dLMZp5l.d.mts} +1 -2
  43. package/dist/graph-3dLMZp5l.d.mts.map +1 -0
  44. package/dist/graph-membership-BV23F1IV.mjs +15 -0
  45. package/dist/graph-membership-BV23F1IV.mjs.map +1 -0
  46. package/dist/{hash-Cr4WIr4Z.mjs → hash--Y7vCpN3.mjs} +8 -9
  47. package/dist/hash--Y7vCpN3.mjs.map +1 -0
  48. package/dist/{invariants-0daYEzyo.mjs → invariants-C23nXy1c.mjs} +2 -2
  49. package/dist/{invariants-0daYEzyo.mjs.map → invariants-C23nXy1c.mjs.map} +1 -1
  50. package/dist/{io-BPLfzvZe.mjs → io-BGlPOt9b.mjs} +100 -13
  51. package/dist/io-BGlPOt9b.mjs.map +1 -0
  52. package/dist/io-BH4G3F-i.d.mts +124 -0
  53. package/dist/io-BH4G3F-i.d.mts.map +1 -0
  54. package/dist/metadata-Bp9X04gM.d.mts +2 -0
  55. package/dist/{migration-graph-nlS4TRpn.mjs → migration-graph-BMAqSfv9.mjs} +6 -26
  56. package/dist/migration-graph-BMAqSfv9.mjs.map +1 -0
  57. package/dist/{migration-graph-De0dUZoC.d.mts → migration-graph-CWEM2SLR.d.mts} +6 -6
  58. package/dist/migration-graph-CWEM2SLR.d.mts.map +1 -0
  59. package/dist/op-schema-D5qkXfEf.mjs.map +1 -1
  60. package/dist/{package-DZj8YvD0.d.mts → package-Ca-J_z_0.d.mts} +1 -1
  61. package/dist/package-Ca-J_z_0.d.mts.map +1 -0
  62. package/dist/{read-contract-space-contract-DRueB4Aa.mjs → read-contract-space-contract-TbeXuJXL.mjs} +32 -5
  63. package/dist/read-contract-space-contract-TbeXuJXL.mjs.map +1 -0
  64. package/dist/{refs-BDHo5l_g.mjs → refs-C-_WUrPw.mjs} +97 -4
  65. package/dist/refs-C-_WUrPw.mjs.map +1 -0
  66. package/dist/refs-C7wuYFqZ.d.mts +42 -0
  67. package/dist/refs-C7wuYFqZ.d.mts.map +1 -0
  68. package/dist/snapshot-Bazwo13S.mjs +137 -0
  69. package/dist/snapshot-Bazwo13S.mjs.map +1 -0
  70. package/dist/verify-contract-spaces-BdysZdQk.d.mts +132 -0
  71. package/dist/verify-contract-spaces-BdysZdQk.d.mts.map +1 -0
  72. package/package.json +18 -9
  73. package/src/aggregate/aggregate.ts +266 -0
  74. package/src/aggregate/check-integrity.ts +243 -0
  75. package/src/aggregate/loader.ts +161 -334
  76. package/src/aggregate/planner-types.ts +14 -14
  77. package/src/aggregate/planner.ts +20 -23
  78. package/src/aggregate/project-schema-to-space.ts +3 -8
  79. package/src/aggregate/strategies/graph-walk.ts +15 -10
  80. package/src/aggregate/strategies/synth.ts +4 -4
  81. package/src/aggregate/types.ts +81 -62
  82. package/src/aggregate/verifier.ts +23 -23
  83. package/src/assert-descriptor-self-consistency.ts +6 -0
  84. package/src/compute-extension-space-apply-path.ts +1 -1
  85. package/src/emit-contract-space-artefacts.ts +4 -3
  86. package/src/errors.ts +58 -2
  87. package/src/exports/aggregate.ts +29 -19
  88. package/src/exports/io.ts +2 -0
  89. package/src/exports/metadata.ts +1 -1
  90. package/src/exports/migration-graph.ts +1 -0
  91. package/src/exports/refs.ts +11 -0
  92. package/src/exports/spaces.ts +3 -0
  93. package/src/graph-membership.ts +17 -0
  94. package/src/graph.ts +0 -1
  95. package/src/hash.ts +7 -8
  96. package/src/integrity-violation.ts +114 -0
  97. package/src/io.ts +139 -14
  98. package/src/metadata.ts +1 -1
  99. package/src/migration-base.ts +10 -30
  100. package/src/migration-graph.ts +7 -35
  101. package/src/read-contract-space-head-ref.ts +5 -2
  102. package/src/refs/snapshot.ts +199 -0
  103. package/src/refs.ts +124 -1
  104. package/src/space-layout.ts +30 -0
  105. package/dist/errors-DGYwcwXs.mjs.map +0 -1
  106. package/dist/exports/io.d.mts.map +0 -1
  107. package/dist/graph-BrLXqoUc.d.mts.map +0 -1
  108. package/dist/hash-Cr4WIr4Z.mjs.map +0 -1
  109. package/dist/io-BPLfzvZe.mjs.map +0 -1
  110. package/dist/metadata-BFX0xdz8.d.mts +0 -2
  111. package/dist/migration-graph-De0dUZoC.d.mts.map +0 -1
  112. package/dist/migration-graph-nlS4TRpn.mjs.map +0 -1
  113. package/dist/package-DZj8YvD0.d.mts.map +0 -1
  114. package/dist/read-contract-space-contract-DRueB4Aa.mjs.map +0 -1
  115. package/dist/refs-BDHo5l_g.mjs.map +0 -1
  116. package/dist/refs-CDaNerhT.d.mts +0 -16
  117. package/dist/refs-CDaNerhT.d.mts.map +0 -1
  118. package/src/aggregate/extract-storage-element-names.ts +0 -75
package/src/errors.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ifDefined } from '@prisma-next/utils/defined';
2
2
  import { basename, dirname, relative } from 'pathe';
3
+ import type { MigrationGraph } from './graph';
3
4
 
4
5
  /**
5
6
  * Build the canonical "re-emit this package" remediation hint.
@@ -319,8 +320,8 @@ export function errorProvidedInvariantsMismatch(
319
320
  /**
320
321
  * Wire-shape edge surfaced through the JSON envelope's
321
322
  * `meta.structuralPath` of `MIGRATION.NO_INVARIANT_PATH`. Slim by design —
322
- * authoring metadata (`createdAt`, `labels`) lives on `MigrationEdge` but
323
- * is intentionally dropped here so the envelope stays stable across
323
+ * authoring metadata (`createdAt`) lives on `MigrationEdge` but is
324
+ * intentionally dropped here so the envelope stays stable across
324
325
  * graph-internal refactors.
325
326
  *
326
327
  * Stability: any field added here is part of the public CLI JSON contract.
@@ -399,3 +400,58 @@ export function errorMigrationHashMismatch(
399
400
  details: { dir, storedHash, computedHash },
400
401
  });
401
402
  }
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
+
445
+ export function errorHashNotInGraph(hash: string, graph: MigrationGraph): MigrationToolsError {
446
+ const reachableHashes = [...graph.nodes].sort();
447
+ const reachableList = reachableHashes.length > 0 ? reachableHashes.join(', ') : '(none)';
448
+ return new MigrationToolsError(
449
+ 'MIGRATION.HASH_NOT_IN_GRAPH',
450
+ `Hash "${hash}" is not a node in the migration graph`,
451
+ {
452
+ why: `The migration graph contains nodes ${reachableList}; "${hash}" isn't one of them.`,
453
+ fix: `Pass a hash that's the from-or-to of an on-disk migration bundle, use --from with a graph-node hash, or run "prisma-next migration plan" to introduce it.`,
454
+ details: { hash, reachableHashes },
455
+ },
456
+ );
457
+ }
@@ -1,22 +1,26 @@
1
1
  export {
2
- type DeclaredExtensionEntry,
3
- type LayoutViolation,
4
- type LoadAggregateError,
5
- type LoadAggregateInput,
6
- type LoadAggregateOutput,
7
- loadContractSpaceAggregate,
8
- } from '../aggregate/loader';
2
+ createContractSpaceAggregate,
3
+ createContractSpaceMember,
4
+ requireHeadRef,
5
+ } from '../aggregate/aggregate';
6
+ export {
7
+ computeIntegrityViolations,
8
+ type IntegrityComputationInput,
9
+ type IntegritySpaceState,
10
+ loadProblemToViolation,
11
+ } from '../aggregate/check-integrity';
12
+ export { type LoadAggregateInput, loadContractSpaceAggregate } from '../aggregate/loader';
9
13
  export type { ContractMarkerRecordLike } from '../aggregate/marker-types';
10
14
  export {
11
15
  type AggregateCurrentDBState,
12
16
  type AggregateMigrationEdgeRef,
13
- type AggregatePerSpacePlan,
14
- type AggregatePlannerError,
15
- type AggregatePlannerInput,
16
- type AggregatePlannerOutput,
17
- type AggregatePlannerSuccess,
18
17
  type CallerPolicy,
19
- planAggregate,
18
+ type PerSpacePlan,
19
+ type PlannerError,
20
+ type PlannerInput,
21
+ type PlannerOutput,
22
+ type PlannerSuccess,
23
+ planMigration,
20
24
  } from '../aggregate/planner';
21
25
  export { projectSchemaToSpace } from '../aggregate/project-schema-to-space';
22
26
  export {
@@ -25,18 +29,24 @@ export {
25
29
  graphWalkStrategy,
26
30
  } from '../aggregate/strategies/graph-walk';
27
31
  export type {
32
+ ContractAtOptions,
33
+ ContractAtResult,
28
34
  ContractSpaceAggregate,
29
35
  ContractSpaceMember,
30
- HydratedMigrationGraph,
31
36
  } from '../aggregate/types';
32
37
  export {
33
- type AggregateVerifierError,
34
- type AggregateVerifierInput,
35
- type AggregateVerifierOutput,
36
- type AggregateVerifierSuccess,
37
38
  type MarkerCheckResult,
38
39
  type MarkerCheckSection,
39
40
  type OrphanElement,
40
41
  type SchemaCheckSection,
41
- verifyAggregate,
42
+ type VerifierError,
43
+ type VerifierInput,
44
+ type VerifierOutput,
45
+ type VerifierSuccess,
46
+ verifyMigration,
42
47
  } from '../aggregate/verifier';
48
+ export type {
49
+ DeclaredExtensionEntry,
50
+ IntegrityQueryOptions,
51
+ IntegrityViolation,
52
+ } from '../integrity-violation';
package/src/exports/io.ts CHANGED
@@ -3,6 +3,8 @@ export {
3
3
  formatMigrationDirName,
4
4
  materialiseExtensionMigrationPackageIfMissing,
5
5
  materialiseMigrationPackage,
6
+ type PackageLoadProblem,
7
+ type ReadMigrationsDirResult,
6
8
  readMigrationPackage,
7
9
  readMigrationsDir,
8
10
  writeMigrationMetadata,
@@ -1 +1 @@
1
- export type { MigrationHints, MigrationMetadata } from '../metadata';
1
+ export type { MigrationMetadata } from '../metadata';
@@ -1,3 +1,4 @@
1
+ export { assertHashIsGraphNode, isGraphNode } from '../graph-membership';
1
2
  export type { PathDecision } from '../migration-graph';
2
3
  export {
3
4
  detectCycles,
@@ -1,10 +1,21 @@
1
1
  export type { RefEntry, Refs } from '../refs';
2
2
  export {
3
3
  deleteRef,
4
+ HEAD_REF_NAME,
4
5
  readRef,
5
6
  readRefs,
7
+ refsByContractHash,
6
8
  resolveRef,
9
+ resolveRefsByContractHash,
7
10
  validateRefName,
8
11
  validateRefValue,
9
12
  writeRef,
10
13
  } from '../refs';
14
+ export type { ContractIR } from '../refs/snapshot';
15
+ export {
16
+ deleteRefPaired,
17
+ deleteRefSnapshot,
18
+ readRefSnapshot,
19
+ writeRefPaired,
20
+ writeRefSnapshot,
21
+ } from '../refs/snapshot';
@@ -31,7 +31,10 @@ export {
31
31
  APP_SPACE_ID,
32
32
  assertValidSpaceId,
33
33
  isValidSpaceId,
34
+ RESERVED_SPACE_SUBDIR_NAMES,
35
+ SPACE_REFS_DIRNAME,
34
36
  spaceMigrationDirectory,
37
+ spaceRefsDirectory,
35
38
  type ValidSpaceId,
36
39
  } from '../space-layout';
37
40
  export {
@@ -0,0 +1,17 @@
1
+ import { EMPTY_CONTRACT_HASH } from './constants';
2
+ import { errorHashNotInGraph } from './errors';
3
+ import type { MigrationGraph } from './graph';
4
+
5
+ export function isGraphNode(hash: string, graph: MigrationGraph): boolean {
6
+ if (hash === EMPTY_CONTRACT_HASH) {
7
+ return true;
8
+ }
9
+ return graph.nodes.has(hash);
10
+ }
11
+
12
+ export function assertHashIsGraphNode(hash: string, graph: MigrationGraph): asserts hash is string {
13
+ if (isGraphNode(hash, graph)) {
14
+ return;
15
+ }
16
+ throw errorHashNotInGraph(hash, graph);
17
+ }
package/src/graph.ts CHANGED
@@ -8,7 +8,6 @@ export interface MigrationEdge {
8
8
  readonly migrationHash: string;
9
9
  readonly dirName: string;
10
10
  readonly createdAt: string;
11
- readonly labels: readonly string[];
12
11
  /**
13
12
  * Sorted, deduplicated list of `invariantId`s this edge provides.
14
13
  * An empty array means the migration declares no routing-visible
package/src/hash.ts CHANGED
@@ -15,13 +15,12 @@ function sha256Hex(input: string): string {
15
15
  }
16
16
 
17
17
  /**
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.
18
+ * Content-addressed migration hash over (metadata envelope, ops). See
19
+ * ADR 199 — Storage-only migration identity for the rationale: the
20
+ * storage-hash bookends (`from`, `to`) inside the envelope anchor the
21
+ * contract identity by hash. The full contract IRs are not part of the
22
+ * manifest they live in sibling `*-contract.json` files authored
23
+ * alongside the migration, never inlined here.
25
24
  *
26
25
  * The integrity check is purely structural, not semantic. The function
27
26
  * canonicalizes its inputs via `sortKeys` (recursive) + `JSON.stringify`
@@ -46,7 +45,7 @@ export function computeMigrationHash(
46
45
  metadata: Omit<MigrationMetadata, 'migrationHash'> & { readonly migrationHash?: string },
47
46
  ops: MigrationOps,
48
47
  ): string {
49
- const { migrationHash: _migrationHash, hints: _hints, ...strippedMeta } = metadata;
48
+ const { migrationHash: _migrationHash, ...strippedMeta } = metadata;
50
49
 
51
50
  const canonicalMetadata = canonicalizeJson(strippedMeta);
52
51
  const canonicalOps = canonicalizeJson(ops);
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Every structural problem the migration model can carry.
3
+ *
4
+ * Violations come in three groups:
5
+ *
6
+ * - **Recoverable**: the package or space is retained in the model;
7
+ * the violation is surfaced for policy (report, refuse, or ignore
8
+ * depending on the command).
9
+ * - **Config/contract-dependent**: produced only when the matching
10
+ * `IntegrityQueryOptions` opt is set (declaredExtensions /
11
+ * checkContracts). The model is built without them; they surface
12
+ * when the caller explicitly asks for the broader integrity view.
13
+ * - **Unloadable**: the package is omitted from the model entirely
14
+ * (its on-disk content cannot be parsed into an `OnDiskMigrationPackage`).
15
+ *
16
+ * `checkIntegrity()` on `ContractSpaceAggregate` returns the full set —
17
+ * all violations across all spaces — never bailing at the first hit.
18
+ */
19
+ export type IntegrityViolation =
20
+ // recoverable — package/space retained, surfaced for policy
21
+ | {
22
+ readonly kind: 'sameSourceAndTarget';
23
+ readonly spaceId: string;
24
+ readonly dirName: string;
25
+ readonly hash: string;
26
+ }
27
+ | {
28
+ readonly kind: 'hashMismatch';
29
+ readonly spaceId: string;
30
+ readonly dirName: string;
31
+ readonly stored: string;
32
+ readonly computed: string;
33
+ }
34
+ | {
35
+ readonly kind: 'providedInvariantsMismatch';
36
+ readonly spaceId: string;
37
+ readonly dirName: string;
38
+ }
39
+ | { readonly kind: 'headRefMissing'; readonly spaceId: string }
40
+ | { readonly kind: 'headRefNotInGraph'; readonly spaceId: string; readonly hash: string }
41
+ | {
42
+ readonly kind: 'duplicateMigrationHash';
43
+ readonly spaceId: string;
44
+ readonly migrationHash: string;
45
+ readonly dirNames: readonly string[];
46
+ }
47
+ | {
48
+ readonly kind: 'refUnreadable';
49
+ readonly spaceId: string;
50
+ readonly refName: string;
51
+ readonly detail: string;
52
+ }
53
+ // config/contract-dependent — produced only when the matching opt is set
54
+ | { readonly kind: 'orphanSpaceDir'; readonly spaceId: string }
55
+ | { readonly kind: 'declaredButUnmigrated'; readonly spaceId: string }
56
+ | {
57
+ readonly kind: 'targetMismatch';
58
+ readonly spaceId: string;
59
+ readonly expected: string;
60
+ readonly actual: string;
61
+ }
62
+ | {
63
+ readonly kind: 'disjointness';
64
+ readonly element: string;
65
+ readonly claimedBy: readonly string[];
66
+ }
67
+ | { readonly kind: 'contractUnreadable'; readonly spaceId: string; readonly detail: string }
68
+ // genuinely unloadable — package omitted from member.packages
69
+ | {
70
+ readonly kind: 'packageUnloadable';
71
+ readonly spaceId: string;
72
+ readonly dirName: string;
73
+ readonly detail: string;
74
+ };
75
+
76
+ /**
77
+ * One declared extension entry, drawn from `Config.extensionPacks`.
78
+ *
79
+ * The integrity layer needs only:
80
+ *
81
+ * - `id` — the space id (also the directory name under `migrations/`),
82
+ * used for the layout-drift checks (`orphanSpaceDir` /
83
+ * `declaredButUnmigrated`).
84
+ * - `targetId` — the target the declaring extension was configured for.
85
+ *
86
+ * Typed structurally so the migration-tools layer stays framework-neutral.
87
+ */
88
+ export interface DeclaredExtensionEntry {
89
+ readonly id: string;
90
+ readonly targetId: string;
91
+ }
92
+
93
+ /**
94
+ * Options controlling which config/contract-dependent violation checks
95
+ * `checkIntegrity()` runs.
96
+ *
97
+ * Both opts default to disabled: a caller without the app contract or
98
+ * declared extensions still gets the structurally-derivable violations
99
+ * (hashMismatch, providedInvariantsMismatch, headRefMissing,
100
+ * headRefNotInGraph, refUnreadable, sameSourceAndTarget, packageUnloadable).
101
+ */
102
+ export interface IntegrityQueryOptions {
103
+ /**
104
+ * When provided, enables layout-drift checks: `orphanSpaceDir`
105
+ * (a directory exists on disk for an extension not in the list) and
106
+ * `declaredButUnmigrated` (an extension in the list has no on-disk dir).
107
+ */
108
+ readonly declaredExtensions?: readonly DeclaredExtensionEntry[];
109
+ /**
110
+ * When true, enables contract/disjointness/target checks:
111
+ * `contractUnreadable`, `targetMismatch`, `disjointness`.
112
+ */
113
+ readonly checkContracts?: boolean;
114
+ }
package/src/io.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  errorMigrationHashMismatch,
15
15
  errorMissingFile,
16
16
  errorProvidedInvariantsMismatch,
17
+ MigrationToolsError,
17
18
  } from './errors';
18
19
  import { verifyMigrationHash } from './hash';
19
20
  import { deriveProvidedInvariants } from './invariants';
@@ -28,19 +29,11 @@ function hasErrnoCode(error: unknown, code: string): boolean {
28
29
  return error instanceof Error && (error as { code?: string }).code === code;
29
30
  }
30
31
 
31
- const MigrationHintsSchema = type({
32
- used: 'string[]',
33
- applied: 'string[]',
34
- plannerVersion: 'string',
35
- });
36
-
37
32
  const MigrationMetadataSchema = type({
38
33
  '+': 'reject',
39
34
  from: 'string > 0 | null',
40
35
  to: 'string',
41
36
  migrationHash: 'string',
42
- hints: MigrationHintsSchema,
43
- labels: 'string[]',
44
37
  providedInvariants: 'string[]',
45
38
  createdAt: 'string',
46
39
  });
@@ -255,6 +248,60 @@ export async function readMigrationPackage(dir: string): Promise<OnDiskMigration
255
248
  return pkg;
256
249
  }
257
250
 
251
+ /**
252
+ * Reads a migration package's manifest and ops without running hash or
253
+ * invariants verification. Returns `null` when the files cannot be read or
254
+ * parsed (i.e. when the package is genuinely unloadable).
255
+ *
256
+ * Used by {@link readMigrationsDir} to retain a package whose hash or
257
+ * invariants diverge from what is stored on disk — the raw content is still
258
+ * useful for display / querying; only integrity is in question.
259
+ */
260
+ async function readMigrationPackageRaw(dir: string): Promise<OnDiskMigrationPackage | null> {
261
+ const absoluteDir = resolve(dir);
262
+ const manifestPath = join(absoluteDir, MANIFEST_FILE);
263
+ const opsPath = join(absoluteDir, OPS_FILE);
264
+
265
+ let manifestRaw: string;
266
+ try {
267
+ manifestRaw = await readFile(manifestPath, 'utf-8');
268
+ } catch {
269
+ return null;
270
+ }
271
+ let opsRaw: string;
272
+ try {
273
+ opsRaw = await readFile(opsPath, 'utf-8');
274
+ } catch {
275
+ return null;
276
+ }
277
+
278
+ let metadata: MigrationMetadata;
279
+ try {
280
+ metadata = JSON.parse(manifestRaw);
281
+ } catch {
282
+ return null;
283
+ }
284
+ let ops: MigrationOps;
285
+ try {
286
+ ops = JSON.parse(opsRaw);
287
+ } catch {
288
+ return null;
289
+ }
290
+
291
+ const result = MigrationMetadataSchema(metadata);
292
+ if (result instanceof type.errors) return null;
293
+
294
+ const opsResult = MigrationOpsSchema(ops);
295
+ if (opsResult instanceof type.errors) return null;
296
+
297
+ return {
298
+ dirName: basename(absoluteDir),
299
+ dirPath: absoluteDir,
300
+ metadata,
301
+ ops,
302
+ };
303
+ }
304
+
258
305
  function arraysEqual(a: readonly string[], b: readonly string[]): boolean {
259
306
  if (a.length !== b.length) return false;
260
307
  for (let i = 0; i < a.length; i++) {
@@ -280,20 +327,64 @@ function validateOps(ops: unknown, filePath: string): asserts ops is MigrationOp
280
327
  }
281
328
  }
282
329
 
283
- export async function readMigrationsDir(
284
- migrationsRoot: string,
285
- ): Promise<readonly OnDiskMigrationPackage[]> {
330
+ /**
331
+ * A per-package load-time problem returned by {@link readMigrationsDir}.
332
+ *
333
+ * Three variants, matching the relocated throws from the load path:
334
+ *
335
+ * - `hashMismatch` — stored `migrationHash` differs from the recomputed value.
336
+ * The package is **retained** in the returned `packages` array.
337
+ * - `providedInvariantsMismatch` — `migration.json` declares different
338
+ * `providedInvariants` than `ops.json` implies. The package is **retained**.
339
+ * - `packageUnloadable` — the manifest is missing, unparseable, or schema-
340
+ * invalid. The package is **omitted** from `packages`.
341
+ *
342
+ * Callers that need the `spaceId` context (e.g. the aggregate loader) attach
343
+ * it when converting to {@link import('./integrity-violation').IntegrityViolation}.
344
+ */
345
+ export type PackageLoadProblem =
346
+ | {
347
+ readonly kind: 'hashMismatch';
348
+ readonly dirName: string;
349
+ readonly stored: string;
350
+ readonly computed: string;
351
+ }
352
+ | { readonly kind: 'providedInvariantsMismatch'; readonly dirName: string }
353
+ | { readonly kind: 'packageUnloadable'; readonly dirName: string; readonly detail: string };
354
+
355
+ /**
356
+ * Result returned by {@link readMigrationsDir}.
357
+ *
358
+ * - `packages` — every package that could be read; hash-mismatched and
359
+ * invariants-mismatched packages are included here (the problem is
360
+ * represented rather than fatal).
361
+ * - `problems` — one entry per package that had a load-time issue.
362
+ * `packageUnloadable` entries are **not** in `packages`.
363
+ */
364
+ export interface ReadMigrationsDirResult {
365
+ readonly packages: readonly OnDiskMigrationPackage[];
366
+ readonly problems: readonly PackageLoadProblem[];
367
+ }
368
+
369
+ function packageLoadProblemDetailFromError(error: unknown): string {
370
+ if (MigrationToolsError.is(error)) return error.why;
371
+ if (error instanceof Error) return error.message;
372
+ return String(error);
373
+ }
374
+
375
+ export async function readMigrationsDir(migrationsRoot: string): Promise<ReadMigrationsDirResult> {
286
376
  let entries: string[];
287
377
  try {
288
378
  entries = await readdir(migrationsRoot);
289
379
  } catch (error) {
290
380
  if (hasErrnoCode(error, 'ENOENT')) {
291
- return [];
381
+ return { packages: [], problems: [] };
292
382
  }
293
383
  throw error;
294
384
  }
295
385
 
296
386
  const packages: OnDiskMigrationPackage[] = [];
387
+ const problems: PackageLoadProblem[] = [];
297
388
 
298
389
  for (const entry of entries.sort()) {
299
390
  const entryPath = join(migrationsRoot, entry);
@@ -307,10 +398,44 @@ export async function readMigrationsDir(
307
398
  continue; // skip non-migration directories
308
399
  }
309
400
 
310
- packages.push(await readMigrationPackage(entryPath));
401
+ let pkg: OnDiskMigrationPackage;
402
+ try {
403
+ pkg = await readMigrationPackage(entryPath);
404
+ } catch (error) {
405
+ const dirName = entry;
406
+ if (MigrationToolsError.is(error)) {
407
+ if (error.code === 'MIGRATION.HASH_MISMATCH') {
408
+ const details = error.details;
409
+ const rawPkg = await readMigrationPackageRaw(entryPath);
410
+ if (rawPkg !== null) packages.push(rawPkg);
411
+ problems.push({
412
+ kind: 'hashMismatch',
413
+ dirName,
414
+ stored: typeof details?.['storedHash'] === 'string' ? details['storedHash'] : '',
415
+ computed: typeof details?.['computedHash'] === 'string' ? details['computedHash'] : '',
416
+ });
417
+ continue;
418
+ }
419
+ if (error.code === 'MIGRATION.PROVIDED_INVARIANTS_MISMATCH') {
420
+ const rawPkg = await readMigrationPackageRaw(entryPath);
421
+ if (rawPkg !== null) packages.push(rawPkg);
422
+ problems.push({ kind: 'providedInvariantsMismatch', dirName });
423
+ continue;
424
+ }
425
+ }
426
+ // Any other error (missing file, invalid JSON, invalid manifest schema) →
427
+ // package unloadable; omit from packages.
428
+ problems.push({
429
+ kind: 'packageUnloadable',
430
+ dirName,
431
+ detail: packageLoadProblemDetailFromError(error),
432
+ });
433
+ continue;
434
+ }
435
+ packages.push(pkg);
311
436
  }
312
437
 
313
- return packages;
438
+ return { packages, problems };
314
439
  }
315
440
 
316
441
  export function formatMigrationDirName(timestamp: Date, slug: string): string {
package/src/metadata.ts CHANGED
@@ -1 +1 @@
1
- export type { MigrationHints, MigrationMetadata } from '@prisma-next/framework-components/control';
1
+ export type { MigrationMetadata } from '@prisma-next/framework-components/control';
@@ -9,14 +9,13 @@ import { type } from 'arktype';
9
9
  import { errorInvalidOperationEntry } from './errors';
10
10
  import { computeMigrationHash } from './hash';
11
11
  import { deriveProvidedInvariants } from './invariants';
12
- import type { MigrationHints, MigrationMetadata } from './metadata';
12
+ import type { MigrationMetadata } from './metadata';
13
13
  import { MigrationOpSchema } from './op-schema';
14
14
  import type { MigrationOps } from './package';
15
15
 
16
16
  export interface MigrationMeta {
17
17
  readonly from: string | null;
18
18
  readonly to: string;
19
- readonly labels?: readonly string[];
20
19
  }
21
20
 
22
21
  // `from` rejects empty strings to mirror `MigrationMetadataSchema` in
@@ -27,7 +26,6 @@ export interface MigrationMeta {
27
26
  const MigrationMetaSchema = type({
28
27
  from: 'string > 0 | null',
29
28
  to: 'string',
30
- 'labels?': type('string').array(),
31
29
  });
32
30
 
33
31
  /**
@@ -127,12 +125,11 @@ export interface MigrationArtifacts {
127
125
  * operations list, and the previously-scaffolded metadata (if any).
128
126
  *
129
127
  * When a `migration.json` already exists for this package (the common
130
- * case: it was scaffolded by `migration plan`), preserve the contract
131
- * bookends, hints, labels, and `createdAt` set there those fields are
132
- * owned by the CLI scaffolder, not the authored class. Only the
133
- * `describe()`-derived fields (`from`, `to`) and the operations
134
- * change as the author iterates. When no metadata exists yet (a bare
135
- * `migration.ts` run from scratch), synthesize a minimal but
128
+ * case: it was scaffolded by `migration plan`), preserve `createdAt`
129
+ * set there that field is owned by the CLI scaffolder, not the authored
130
+ * class. Only the `describe()`-derived fields (`from`, `to`) and the
131
+ * operations change as the author iterates. When no metadata exists yet
132
+ * (a bare `migration.ts` run from scratch), synthesize a minimal but
136
133
  * schema-conformant record so the resulting package can still be read,
137
134
  * verified, and applied.
138
135
  *
@@ -147,39 +144,22 @@ function buildAttestedMetadata(
147
144
  const baseMetadata: Omit<MigrationMetadata, 'migrationHash'> = {
148
145
  from: meta.from,
149
146
  to: meta.to,
150
- labels: meta.labels ?? existing?.labels ?? [],
151
147
  providedInvariants: deriveProvidedInvariants(ops),
152
148
  createdAt: existing?.createdAt ?? new Date().toISOString(),
153
- hints: normalizeHints(existing?.hints),
154
149
  };
155
150
 
156
151
  const migrationHash = computeMigrationHash(baseMetadata, ops);
157
152
  return { ...baseMetadata, migrationHash };
158
153
  }
159
154
 
160
- /**
161
- * Project `existing.hints` down to the known `MigrationHints` shape, dropping
162
- * any legacy keys that may linger in metadata scaffolded by older CLI
163
- * versions (e.g. `planningStrategy`). Picking fields explicitly instead of
164
- * spreading keeps refreshed `migration.json` files schema-clean regardless
165
- * of what was on disk before.
166
- */
167
- function normalizeHints(existing: MigrationHints | undefined): MigrationHints {
168
- return {
169
- used: existing?.used ?? [],
170
- applied: existing?.applied ?? [],
171
- plannerVersion: existing?.plannerVersion ?? '2.0.0',
172
- };
173
- }
174
-
175
155
  /**
176
156
  * Pure conversion from a `Migration` instance (plus the previously
177
157
  * scaffolded metadata, when one exists on disk) to the in-memory
178
158
  * artifacts that downstream tooling persists. Owns metadata validation,
179
- * metadata synthesis/preservation, hint normalization, and the
180
- * content-addressed `migrationHash` computation, but performs no file I/O
181
- * — callers handle reads (to source `existing`) and writes (to persist
182
- * `opsJson` / `metadataJson`).
159
+ * metadata synthesis/preservation, and the content-addressed
160
+ * `migrationHash` computation, but performs no file I/O — callers handle
161
+ * reads (to source `existing`) and writes (to persist `opsJson` /
162
+ * `metadataJson`).
183
163
  */
184
164
  export function buildMigrationArtifacts(
185
165
  instance: Migration,