@prisma-next/cli 0.16.0-dev.4 → 0.16.0-dev.6

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 (114) hide show
  1. package/README.md +3 -4
  2. package/dist/cli.mjs +13 -13
  3. package/dist/{client-KuBQftxz.mjs → client-2kwLMBSf.mjs} +4 -4
  4. package/dist/{client-KuBQftxz.mjs.map → client-2kwLMBSf.mjs.map} +1 -1
  5. package/dist/{command-helpers-Cpq44aVa.mjs → command-helpers-CVn3U9Uz.mjs} +12 -4
  6. package/dist/command-helpers-CVn3U9Uz.mjs.map +1 -0
  7. package/dist/commands/contract-emit.mjs +1 -1
  8. package/dist/commands/contract-infer.mjs +1 -1
  9. package/dist/commands/db-init.d.mts.map +1 -1
  10. package/dist/commands/db-init.mjs +5 -4
  11. package/dist/commands/db-init.mjs.map +1 -1
  12. package/dist/commands/db-schema.mjs +3 -3
  13. package/dist/commands/db-sign.d.mts.map +1 -1
  14. package/dist/commands/db-sign.mjs +8 -10
  15. package/dist/commands/db-sign.mjs.map +1 -1
  16. package/dist/commands/db-update.d.mts.map +1 -1
  17. package/dist/commands/db-update.mjs +11 -13
  18. package/dist/commands/db-update.mjs.map +1 -1
  19. package/dist/commands/db-verify.mjs +1 -1
  20. package/dist/commands/migrate.d.mts.map +1 -1
  21. package/dist/commands/migrate.mjs +7 -7
  22. package/dist/commands/migrate.mjs.map +1 -1
  23. package/dist/commands/migration-check.d.mts +3 -1
  24. package/dist/commands/migration-check.d.mts.map +1 -1
  25. package/dist/commands/migration-check.mjs +1 -1
  26. package/dist/commands/migration-graph.mjs +3 -3
  27. package/dist/commands/migration-list.d.mts.map +1 -1
  28. package/dist/commands/migration-list.mjs +1 -1
  29. package/dist/commands/migration-log.mjs +1 -1
  30. package/dist/commands/migration-new.d.mts.map +1 -1
  31. package/dist/commands/migration-new.mjs +14 -38
  32. package/dist/commands/migration-new.mjs.map +1 -1
  33. package/dist/commands/migration-plan.d.mts.map +1 -1
  34. package/dist/commands/migration-plan.mjs +1 -1
  35. package/dist/commands/migration-show.mjs +3 -3
  36. package/dist/commands/migration-status.mjs +1 -1
  37. package/dist/commands/ref.d.mts.map +1 -1
  38. package/dist/commands/ref.mjs +13 -18
  39. package/dist/commands/ref.mjs.map +1 -1
  40. package/dist/commands/telemetry/index.mjs +1 -1
  41. package/dist/contract-at-errors-C-l4-JEY.mjs +36 -0
  42. package/dist/contract-at-errors-C-l4-JEY.mjs.map +1 -0
  43. package/dist/{contract-emit-CgnXSANN.mjs → contract-emit-DzOkTs11.mjs} +3 -3
  44. package/dist/{contract-emit-CgnXSANN.mjs.map → contract-emit-DzOkTs11.mjs.map} +1 -1
  45. package/dist/{contract-emit-cJQBlvGb.mjs → contract-emit-iSMJVs26.mjs} +3 -3
  46. package/dist/{contract-emit-cJQBlvGb.mjs.map → contract-emit-iSMJVs26.mjs.map} +1 -1
  47. package/dist/{contract-infer-Cg8evbeg.mjs → contract-infer-BVqworBB.mjs} +3 -3
  48. package/dist/{contract-infer-Cg8evbeg.mjs.map → contract-infer-BVqworBB.mjs.map} +1 -1
  49. package/dist/{contract-space-aggregate-loader-D_3VeHVb.mjs → contract-space-aggregate-loader-hPVymNLw.mjs} +2 -2
  50. package/dist/{contract-space-aggregate-loader-D_3VeHVb.mjs.map → contract-space-aggregate-loader-hPVymNLw.mjs.map} +1 -1
  51. package/dist/{db-verify-BEclUzki.mjs → db-verify-BpwCMyWJ.mjs} +4 -4
  52. package/dist/{db-verify-BEclUzki.mjs.map → db-verify-BpwCMyWJ.mjs.map} +1 -1
  53. package/dist/exports/control-api.mjs +2 -2
  54. package/dist/exports/index.mjs +2 -2
  55. package/dist/exports/init-output.d.mts.map +1 -1
  56. package/dist/exports/init-output.mjs +1 -1
  57. package/dist/{format-DAUAZqPt.mjs → format-CefCDzkw.mjs} +2 -2
  58. package/dist/{format-DAUAZqPt.mjs.map → format-CefCDzkw.mjs.map} +1 -1
  59. package/dist/{framework-components-CnbUbiJw.mjs → framework-components-VMQJbAwl.mjs} +2 -2
  60. package/dist/{framework-components-CnbUbiJw.mjs.map → framework-components-VMQJbAwl.mjs.map} +1 -1
  61. package/dist/{init-C3JhkHg-.mjs → init-BSgPxVXA.mjs} +55 -47
  62. package/dist/init-BSgPxVXA.mjs.map +1 -0
  63. package/dist/{inspect-live-schema-CX7EA5C3.mjs → inspect-live-schema-DxsD_a4d.mjs} +3 -3
  64. package/dist/{inspect-live-schema-CX7EA5C3.mjs.map → inspect-live-schema-DxsD_a4d.mjs.map} +1 -1
  65. package/dist/{migration-check-MHT3w7_v.mjs → migration-check-Biu1YZXu.mjs} +52 -25
  66. package/dist/migration-check-Biu1YZXu.mjs.map +1 -0
  67. package/dist/{migration-command-scaffold-CRyYrHZ9.mjs → migration-command-scaffold-DB_i9nBC.mjs} +3 -3
  68. package/dist/{migration-command-scaffold-CRyYrHZ9.mjs.map → migration-command-scaffold-DB_i9nBC.mjs.map} +1 -1
  69. package/dist/{migration-list-BAKujUjh.mjs → migration-list-cnSKilYH.mjs} +5 -5
  70. package/dist/migration-list-cnSKilYH.mjs.map +1 -0
  71. package/dist/{migration-log-C_xam2HZ.mjs → migration-log-CEqFiez9.mjs} +3 -3
  72. package/dist/{migration-log-C_xam2HZ.mjs.map → migration-log-CEqFiez9.mjs.map} +1 -1
  73. package/dist/{migration-path-target-DpPTsRCg.mjs → migration-path-target-C0TQuV5n.mjs} +2 -2
  74. package/dist/{migration-path-target-DpPTsRCg.mjs.map → migration-path-target-C0TQuV5n.mjs.map} +1 -1
  75. package/dist/{migration-plan-CreSVtWZ.mjs → migration-plan-BJHulFZC.mjs} +53 -61
  76. package/dist/migration-plan-BJHulFZC.mjs.map +1 -0
  77. package/dist/{migration-status-YgEvXf-u.mjs → migration-status-Df4zK5lO.mjs} +6 -5
  78. package/dist/{migration-status-YgEvXf-u.mjs.map → migration-status-Df4zK5lO.mjs.map} +1 -1
  79. package/dist/{output-mEQ74_nd.mjs → output-DWNYqZGc.mjs} +5 -6
  80. package/dist/output-DWNYqZGc.mjs.map +1 -0
  81. package/dist/{ref-advancement-BkXlikCA.mjs → ref-advancement-BCz7X7qJ.mjs} +13 -6
  82. package/dist/ref-advancement-BCz7X7qJ.mjs.map +1 -0
  83. package/dist/{telemetry-DVr6RFnq.mjs → telemetry-BEyNMfco.mjs} +2 -2
  84. package/dist/{telemetry-DVr6RFnq.mjs.map → telemetry-BEyNMfco.mjs.map} +1 -1
  85. package/dist/{verify-CgDVZjhc.mjs → verify-C4RazDE1.mjs} +2 -2
  86. package/dist/{verify-CgDVZjhc.mjs.map → verify-C4RazDE1.mjs.map} +1 -1
  87. package/package.json +21 -21
  88. package/src/commands/db-init.ts +1 -0
  89. package/src/commands/db-sign.ts +6 -4
  90. package/src/commands/db-update.ts +14 -6
  91. package/src/commands/init/hygiene-gitattributes.ts +37 -21
  92. package/src/commands/init/init.ts +6 -6
  93. package/src/commands/init/output.ts +4 -5
  94. package/src/commands/init/reinit-cleanup.ts +16 -17
  95. package/src/commands/migrate.ts +1 -0
  96. package/src/commands/migration-check.ts +69 -32
  97. package/src/commands/migration-list.ts +1 -5
  98. package/src/commands/migration-new.ts +15 -39
  99. package/src/commands/migration-plan.ts +55 -77
  100. package/src/commands/ref.ts +19 -14
  101. package/src/utils/cli-errors.ts +13 -5
  102. package/src/utils/contract-at-errors.ts +13 -22
  103. package/src/utils/contract-space-seed-phase.ts +8 -6
  104. package/src/utils/plan-resolution.ts +6 -9
  105. package/src/utils/ref-advancement.ts +22 -3
  106. package/dist/command-helpers-Cpq44aVa.mjs.map +0 -1
  107. package/dist/contract-at-errors-D9gEjYtg.mjs +0 -42
  108. package/dist/contract-at-errors-D9gEjYtg.mjs.map +0 -1
  109. package/dist/init-C3JhkHg-.mjs.map +0 -1
  110. package/dist/migration-check-MHT3w7_v.mjs.map +0 -1
  111. package/dist/migration-list-BAKujUjh.mjs.map +0 -1
  112. package/dist/migration-plan-CreSVtWZ.mjs.map +0 -1
  113. package/dist/output-mEQ74_nd.mjs.map +0 -1
  114. package/dist/ref-advancement-BkXlikCA.mjs.map +0 -1
@@ -1,4 +1,7 @@
1
- import { readFile } from 'node:fs/promises';
1
+ import {
2
+ contractSnapshotDir,
3
+ readContractSnapshotJson,
4
+ } from '@prisma-next/migration-tools/contract-snapshot-store';
2
5
  import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
3
6
  import { parseContractRef } from '@prisma-next/migration-tools/ref-resolution';
4
7
  import { ifDefined } from '@prisma-next/utils/defined';
@@ -135,16 +138,20 @@ async function executeDbUpdateCommand(
135
138
  errorUnexpected(
136
139
  `No migration bundle found for --to "${options.to}" (resolved hash: ${targetHash})`,
137
140
  {
138
- why: `The ref resolved successfully but no on-disk migration package has an end-contract hash matching ${targetHash}.`,
141
+ why: `The ref resolved successfully but no on-disk migration package has a destination (\`to\`) hash matching ${targetHash}.`,
139
142
  fix: 'Provide a ref or hash that corresponds to an existing migration package, or run `migration list` to see available migrations.',
140
143
  },
141
144
  ),
142
145
  );
143
146
  }
144
- const endContractPath = join(matchingBundle.dirPath, 'end-contract.json');
145
- const raw = await readFile(endContractPath, 'utf-8');
146
- contractJson = JSON.parse(raw) as Record<string, unknown>;
147
- contractJsonPathForSnapshot = endContractPath;
147
+ contractJson = (await readContractSnapshotJson(migrationsDir, targetHash)) as Record<
148
+ string,
149
+ unknown
150
+ >;
151
+ contractJsonPathForSnapshot = join(
152
+ contractSnapshotDir(migrationsDir, targetHash),
153
+ 'contract.json',
154
+ );
148
155
  } catch (error) {
149
156
  if (MigrationToolsError.is(error)) {
150
157
  return notOk(mapMigrationToolsError(error));
@@ -193,6 +200,7 @@ async function executeDbUpdateCommand(
193
200
  ...ifDefined('advanceRef', options.advanceRef),
194
201
  ...ifDefined('db', options.db),
195
202
  refsDir,
203
+ migrationsDir,
196
204
  contractIR,
197
205
  mode: result.value.mode,
198
206
  hash: advancementHash,
@@ -1,47 +1,60 @@
1
1
  import type { TargetId } from './templates/code-templates';
2
2
 
3
3
  /**
4
- * The schema-relative `.gitattributes` entries written for a freshly
5
- * initialised project (FR3.4). Mirrors the relevant subset of the
6
- * repo-root [`.gitattributes`](../../../../../../../../.gitattributes):
4
+ * The `.gitattributes` entries written for a freshly initialised project
5
+ * (FR3.4). Mirrors the relevant subset of the repo-root
6
+ * [`.gitattributes`](../../../../../../../../.gitattributes):
7
7
  *
8
8
  * - **Today**: `contract.json`, `contract.d.ts` are emitted on every
9
9
  * `prisma-next contract emit`. Marking them `linguist-generated`
10
10
  * keeps GitHub's diff stats honest and collapses the file in code
11
11
  * review by default.
12
- * - **Forward-looking**: `end-contract.*`, `start-contract.*`, `ops.json`,
13
- * `migration.json` are not yet emitted by `init` flows but will be
14
- * produced by adjacent commands (lower / migration tooling). Adding
15
- * them now matches Decision 5 (forward-looking subset) so the file
16
- * does not need to be amended every time a new artefact lands.
12
+ * - **Forward-looking**: `ops.json`, `migration.json` are not yet emitted
13
+ * by `init` flows but will be produced by adjacent commands (lower /
14
+ * migration tooling). Adding them now matches Decision 5
15
+ * (forward-looking subset) so the file does not need to be amended
16
+ * every time a new artifact lands.
17
17
  *
18
- * Patterns are written relative to the schema directory so a user
19
- * who runs `init --schema-path db/contract.prisma` gets
20
- * `db/contract.json linguist-generated` — not the workspace-glob form
21
- * `<glob>/contract.json` (which would over-match any unrelated
18
+ * `ARTIFACT_FILENAMES` entries are written relative to the schema
19
+ * directory so a user who runs `init --schema-path db/contract.prisma`
20
+ * gets `db/contract.json linguist-generated` — not the workspace-glob
21
+ * form `<glob>/contract.json` (which would over-match any unrelated
22
22
  * `contract.json` the user has elsewhere) and not the absolute
23
- * `DEFAULT_CONTRACT_SOURCE_DIR/contract.json` (which would silently break for a non-default
24
- * schema path).
23
+ * `DEFAULT_CONTRACT_SOURCE_DIR/contract.json` (which would silently
24
+ * break for a non-default schema path).
25
+ *
26
+ * The migration contract snapshot store (`migrations/snapshots/<hex>/`)
27
+ * is anchored to the migrations root instead, not the schema directory:
28
+ * migration package depth under `migrations/` varies (`app/<pkg>`,
29
+ * `<space>/<pkg>`, or a bare `<pkg>` in extension source repos), so no
30
+ * single schema-dir-relative pattern can reach every snapshot. See
31
+ * `STORE_GITATTRIBUTES_LINES` below.
25
32
  */
26
- const ARTEFACT_FILENAMES: readonly string[] = [
33
+ const ARTIFACT_FILENAMES: readonly string[] = [
27
34
  'contract.json',
28
35
  'contract.d.ts',
29
- 'end-contract.json',
30
- 'end-contract.d.ts',
31
- 'start-contract.json',
32
- 'start-contract.d.ts',
33
36
  'ops.json',
34
37
  'migration.json',
35
38
  ];
36
39
 
37
40
  const ATTRIBUTE = 'linguist-generated';
38
41
 
42
+ /**
43
+ * Full `.gitattributes` lines for the migration contract snapshot store,
44
+ * already anchored to the migrations root — unlike `ARTIFACT_FILENAMES`,
45
+ * these are not combined with the schema-relative prefix.
46
+ */
47
+ const STORE_GITATTRIBUTES_LINES: readonly string[] = [
48
+ `migrations/snapshots/**/contract.json ${ATTRIBUTE}`,
49
+ `migrations/snapshots/**/contract.d.ts ${ATTRIBUTE}`,
50
+ ];
51
+
39
52
  /**
40
53
  * Computes the `.gitattributes` lines this scaffold expects to own. Each
41
54
  * line has the shape `<path> linguist-generated`. The `target` parameter
42
55
  * is currently unused but accepted for symmetry with the other hygiene
43
56
  * helpers and to leave room for target-specific entries (e.g. a future
44
- * Mongo-only artefact) without a signature break.
57
+ * family-specific artifact) without a signature break.
45
58
  */
46
59
  export function requiredGitattributesLines(
47
60
  schemaDir: string,
@@ -49,7 +62,10 @@ export function requiredGitattributesLines(
49
62
  ): readonly string[] {
50
63
  const dir = schemaDir === '.' ? '' : schemaDir.replace(/\/+$/, '');
51
64
  const prefix = dir === '' ? '' : `${dir}/`;
52
- return ARTEFACT_FILENAMES.map((file) => `${prefix}${file} ${ATTRIBUTE}`);
65
+ return [
66
+ ...ARTIFACT_FILENAMES.map((file) => `${prefix}${file} ${ATTRIBUTE}`),
67
+ ...STORE_GITATTRIBUTES_LINES,
68
+ ];
53
69
  }
54
70
 
55
71
  /**
@@ -48,7 +48,7 @@ import {
48
48
  renderInitOutro,
49
49
  } from './output';
50
50
  import { type ProbeOutcome, type ProbeOverrides, probeServerVersion } from './probe-db';
51
- import { findStaleArtefacts, removeDependency } from './reinit-cleanup';
51
+ import { findStaleArtifacts, removeDependency } from './reinit-cleanup';
52
52
  import {
53
53
  DEFAULT_SKILL_SOURCES,
54
54
  formatSkillInstallCommand,
@@ -175,14 +175,14 @@ export async function runInit(
175
175
  { path: '.env.example', content: envExampleContent(inputs.target) },
176
176
  ];
177
177
 
178
- // FR9.1 — on re-init, queue the previously-emitted contract artefacts
178
+ // FR9.1 — on re-init, queue the previously-emitted contract artifacts
179
179
  // for deletion so a target switch (or schema-shape change) does not
180
180
  // leave a stale `contract.json` / `contract.d.ts` next to the new
181
181
  // schema source. Detection is filesystem-only (no parsing of the
182
182
  // previous config) so the cleanup is safe to run before the write
183
183
  // phase: each path is checked for existence in the precondition,
184
184
  // and missing-on-disk-at-write-time is tolerated.
185
- const filesToDelete: string[] = inputs.reinit ? [...findStaleArtefacts(baseDir, schemaDir)] : [];
185
+ const filesToDelete: string[] = inputs.reinit ? [...findStaleArtifacts(baseDir, schemaDir)] : [];
186
186
 
187
187
  // `init` delegates the skill to `npx skills add prisma/prisma-next#v<version>`,
188
188
  // so a hand-rolled `.agents/skills/prisma-next/SKILL.md` in the project
@@ -245,7 +245,7 @@ export async function runInit(
245
245
  }
246
246
 
247
247
  // FR3.4: idempotent .gitattributes — linguist-generated entries for
248
- // the emitted artefacts so GitHub diff stats / code review collapse
248
+ // the emitted artifacts so GitHub diff stats / code review collapse
249
249
  // them by default.
250
250
  const gitattributesPath = join(baseDir, '.gitattributes');
251
251
  const existingGitattributes = existsSync(gitattributesPath)
@@ -373,7 +373,7 @@ export async function runInit(
373
373
  }
374
374
  }
375
375
 
376
- // FR9.1 — delete stale artefacts after the new templates are written.
376
+ // FR9.1 — delete stale artifacts after the new templates are written.
377
377
  // Order is intentional: the names do not collide with `filesToWrite`
378
378
  // (we never write `contract.json` from this command — that's `contract
379
379
  // emit`'s job), so deletion *after* the writes guarantees we never
@@ -847,7 +847,7 @@ function formatCatalogWarning(
847
847
  /**
848
848
  * Recognised pnpm error signatures that justify a fallback to npm.
849
849
  *
850
- * These patterns indicate the published artefact itself is at fault
850
+ * These patterns indicate the published artifact itself is at fault
851
851
  * (a leaked `workspace:*` or `catalog:` specifier), not the user's
852
852
  * environment — pnpm is faithfully reporting "I cannot resolve this
853
853
  * registry version", and npm is willing to install it because npm
@@ -24,10 +24,9 @@ export const InitOutputSchema = type({
24
24
  filesWritten: 'string[]',
25
25
  /**
26
26
  * FR9.1 — files removed from disk during this run. Populated only on
27
- * re-init when previously-emitted contract artefacts (`contract.json`,
28
- * `contract.d.ts`, `start-/end-contract.*`, `ops.json`,
29
- * `migration.json`) were left behind by an earlier run. Empty on a
30
- * green-field init.
27
+ * re-init when previously-emitted contract artifacts (`contract.json`,
28
+ * `contract.d.ts`, `ops.json`, `migration.json`) were left behind by an
29
+ * earlier run. Empty on a green-field init.
31
30
  */
32
31
  filesDeleted: 'string[]',
33
32
  packagesInstalled: {
@@ -80,7 +79,7 @@ export function renderInitOutro(ui: TerminalUI, output: InitOutput, flags: Globa
80
79
 
81
80
  if (output.filesDeleted.length > 0) {
82
81
  lines.push('');
83
- lines.push('Files deleted (stale contract artefacts):');
82
+ lines.push('Files deleted (stale contract artifacts):');
84
83
  for (const file of output.filesDeleted) {
85
84
  lines.push(` • ${file}`);
86
85
  }
@@ -4,41 +4,40 @@ import { join } from 'pathe';
4
4
  /**
5
5
  * Filenames the contract pipeline emits next to the user's schema source
6
6
  * (`<schemaDir>/contract.json`, `<schemaDir>/contract.d.ts`, …). Mirrors
7
- * `ARTEFACT_FILENAMES` in `hygiene-gitattributes.ts`; kept as a separate
8
- * constant here because the cleanup contract is target-agnostic and we
9
- * deliberately do not want a stale `start-contract.json` from a previous
10
- * target lingering after a re-init.
7
+ * the schema-dir-relative `ARTIFACT_FILENAMES` in `hygiene-gitattributes.ts`
8
+ * (not that file's migrations-root-anchored store lines, which are outside
9
+ * this command's scope reinit only ever touches `schemaDir`); kept as a
10
+ * separate constant here because the cleanup contract is target-agnostic
11
+ * and we deliberately do not want a stale artifact from a previous target
12
+ * lingering after a re-init.
11
13
  *
12
- * If a future emit pipeline produces an additional artefact, add it here
13
- * **and** to the gitattributes list — the two stay in lockstep so the
14
- * file `init` advertises as `linguist-generated` is exactly the file
15
- * `init` is willing to delete on re-init.
14
+ * If a future emit pipeline produces an additional schema-dir artifact,
15
+ * add it here **and** to `ARTIFACT_FILENAMES` in `hygiene-gitattributes.ts`
16
+ * the two stay in lockstep so the file `init` advertises as
17
+ * `linguist-generated` is exactly the file `init` is willing to delete on
18
+ * re-init.
16
19
  */
17
- const ARTEFACT_FILENAMES: readonly string[] = [
20
+ const ARTIFACT_FILENAMES: readonly string[] = [
18
21
  'contract.json',
19
22
  'contract.d.ts',
20
- 'end-contract.json',
21
- 'end-contract.d.ts',
22
- 'start-contract.json',
23
- 'start-contract.d.ts',
24
23
  'ops.json',
25
24
  'migration.json',
26
25
  ];
27
26
 
28
27
  /**
29
- * Returns the schema-relative paths of stale contract artefacts the
28
+ * Returns the schema-relative paths of stale contract artifacts the
30
29
  * previous `init` run (or a `contract emit`) left behind in `schemaDir`.
31
30
  * Paths are returned relative to `baseDir` so the caller can plumb them
32
31
  * into `filesWritten`-style logging without re-deriving the path.
33
32
  *
34
33
  * Pure function: no filesystem mutation. Used by `runInit`'s precondition
35
34
  * phase (FR6.2 / NFR3 atomicity) so a downstream parse failure leaves
36
- * the artefacts on disk and the project byte-identical to its pre-init
35
+ * the artifacts on disk and the project byte-identical to its pre-init
37
36
  * state.
38
37
  */
39
- export function findStaleArtefacts(baseDir: string, schemaDir: string): readonly string[] {
38
+ export function findStaleArtifacts(baseDir: string, schemaDir: string): readonly string[] {
40
39
  const result: string[] = [];
41
- for (const filename of ARTEFACT_FILENAMES) {
40
+ for (const filename of ARTIFACT_FILENAMES) {
42
41
  const rel = join(schemaDir, filename);
43
42
  if (existsSync(join(baseDir, rel))) {
44
43
  result.push(rel);
@@ -831,6 +831,7 @@ async function executeMigrateCommand(
831
831
  : await readContractIR(snapshotContractJson, contractPathAbsolute);
832
832
  advancedRef = await executeRefAdvancement(
833
833
  refsDir,
834
+ migrationsDir,
834
835
  options.advanceRef,
835
836
  value.markerHash,
836
837
  contractIR,
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
1
+ import { existsSync, readdirSync, statSync } from 'node:fs';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { loadConfig } from '@prisma-next/config-loader';
4
4
  import { createControlStack } from '@prisma-next/framework-components/control';
@@ -7,6 +7,11 @@ import type {
7
7
  IntegrityViolation,
8
8
  } from '@prisma-next/migration-tools/aggregate';
9
9
  import { loadContractSpaceAggregate } from '@prisma-next/migration-tools/aggregate';
10
+ import {
11
+ contractSnapshotDir,
12
+ readContractSnapshotJson,
13
+ } from '@prisma-next/migration-tools/contract-snapshot-store';
14
+ import { MigrationToolsError } from '@prisma-next/migration-tools/errors';
10
15
  import type { MigrationGraph } from '@prisma-next/migration-tools/graph';
11
16
  import { verifyMigrationHash } from '@prisma-next/migration-tools/hash';
12
17
  import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
@@ -18,7 +23,6 @@ import type { Refs } from '@prisma-next/migration-tools/refs';
18
23
  import {
19
24
  isValidSpaceId,
20
25
  listContractSpaceDirectories,
21
- RESERVED_SPACE_SUBDIR_NAMES,
22
26
  spaceMigrationDirectory,
23
27
  spaceRefsDirectory,
24
28
  } from '@prisma-next/migration-tools/spaces';
@@ -92,32 +96,57 @@ function checkFileExists(
92
96
  return null;
93
97
  }
94
98
 
95
- function checkSnapshotConsistency(
99
+ /**
100
+ * Snapshot-store consistency check for one migration package (D6.5):
101
+ * absent store entry is not an issue (runner independence, ADR 199 — a
102
+ * package dir with only `migration.json` + `ops.json` is legitimate); a
103
+ * present entry whose inner `storage.storageHash` disagrees with
104
+ * `pkg.metadata.to` is `MIGRATION.CHECK_SNAPSHOT_HASH_MISMATCH`; an unparseable store
105
+ * entry (or a malformed `to`) is `MIGRATION.CHECK_SNAPSHOT_UNPARSEABLE`.
106
+ */
107
+ async function checkSnapshotConsistency(
96
108
  spaceId: string,
97
109
  pkg: OnDiskMigrationPackage,
98
- ): CheckFailure | null {
99
- const endContractPath = join(pkg.dirPath, 'end-contract.json');
100
- if (!existsSync(endContractPath)) return null;
110
+ migrationsDir: string,
111
+ ): Promise<CheckFailure | null> {
112
+ let snapshotDir: string;
101
113
  try {
102
- const raw = JSON.parse(readFileSync(endContractPath, 'utf-8')) as Record<string, unknown>;
103
- const storage = raw['storage'] as Record<string, unknown> | undefined;
104
- const snapshotHash = storage?.['storageHash'];
105
- if (typeof snapshotHash === 'string' && snapshotHash !== pkg.metadata.to) {
106
- return {
107
- space: spaceId,
108
- code: 'MIGRATION.CHECK_SNAPSHOT_HASH_MISMATCH',
109
- where: migrationPathRelative(pkg.dirPath),
110
- why: `Migration "${pkg.dirName}" declares to=${pkg.metadata.to} but end-contract.json has storageHash=${snapshotHash}`,
111
- fix: 'Re-emit the migration package so migration.json and end-contract.json agree.',
112
- };
113
- }
114
+ snapshotDir = contractSnapshotDir(migrationsDir, pkg.metadata.to);
114
115
  } catch {
115
116
  return {
116
117
  space: spaceId,
117
118
  code: 'MIGRATION.CHECK_SNAPSHOT_UNPARSEABLE',
118
119
  where: migrationPathRelative(pkg.dirPath),
119
- why: `Migration "${pkg.dirName}" has an unparseable end-contract.json.`,
120
- fix: 'Re-emit the migration package to repair the snapshot file.',
120
+ why: `Migration "${pkg.dirName}" declares to="${pkg.metadata.to}", which is not a well-formed contract snapshot hash.`,
121
+ fix: 'Re-emit the migration package so migration.json declares a valid `sha256:<64hex>` to-hash.',
122
+ };
123
+ }
124
+
125
+ let raw: unknown;
126
+ try {
127
+ raw = await readContractSnapshotJson(migrationsDir, pkg.metadata.to);
128
+ } catch (error) {
129
+ if (MigrationToolsError.is(error) && error.code === 'MIGRATION.CONTRACT_SNAPSHOT_MISSING') {
130
+ return null;
131
+ }
132
+ return {
133
+ space: spaceId,
134
+ code: 'MIGRATION.CHECK_SNAPSHOT_UNPARSEABLE',
135
+ where: migrationPathRelative(pkg.dirPath),
136
+ why: `Migration "${pkg.dirName}" has an unparseable contract snapshot at ${snapshotDir}/contract.json.`,
137
+ fix: 'Restore migrations/snapshots/ from version control, or re-run the command that produced this migration to regenerate its snapshot.',
138
+ };
139
+ }
140
+ const record = raw !== null && typeof raw === 'object' ? (raw as Record<string, unknown>) : {};
141
+ const storage = record['storage'] as Record<string, unknown> | undefined;
142
+ const snapshotHash = storage?.['storageHash'];
143
+ if (typeof snapshotHash === 'string' && snapshotHash !== pkg.metadata.to) {
144
+ return {
145
+ space: spaceId,
146
+ code: 'MIGRATION.CHECK_SNAPSHOT_HASH_MISMATCH',
147
+ where: migrationPathRelative(pkg.dirPath),
148
+ why: `Migration "${pkg.dirName}" declares to=${pkg.metadata.to} but its contract snapshot has storageHash=${snapshotHash}`,
149
+ fix: 'Re-emit the migration package so migration.json and its contract snapshot agree.',
121
150
  };
122
151
  }
123
152
  return null;
@@ -137,6 +166,8 @@ export interface CheckSpace {
137
166
  readonly graph: MigrationGraph;
138
167
  readonly migrationsDir: string;
139
168
  readonly refsDir: string;
169
+ /** Migrations root the contract-snapshot store lives under — shared by every space. */
170
+ readonly projectMigrationsDir: string;
140
171
  }
141
172
 
142
173
  /**
@@ -152,9 +183,7 @@ export async function enumerateCheckSpaces(
152
183
  projectMigrationsDir: string,
153
184
  ): Promise<readonly CheckSpace[]> {
154
185
  const candidateDirs = await listContractSpaceDirectories(projectMigrationsDir);
155
- const onDiskSpaceIds = new Set(
156
- candidateDirs.filter((name) => !RESERVED_SPACE_SUBDIR_NAMES.has(name)).filter(isValidSpaceId),
157
- );
186
+ const onDiskSpaceIds = new Set(candidateDirs.filter(isValidSpaceId));
158
187
  const spaces: CheckSpace[] = [];
159
188
  for (const space of aggregate.spaces()) {
160
189
  const spaceId = space.spaceId;
@@ -168,6 +197,7 @@ export async function enumerateCheckSpaces(
168
197
  graph: space.graph(),
169
198
  migrationsDir,
170
199
  refsDir: spaceRefsDirectory(migrationsDir),
200
+ projectMigrationsDir,
171
201
  });
172
202
  }
173
203
  return spaces;
@@ -238,12 +268,15 @@ function checkDanglingRefs(space: CheckSpace): readonly CheckFailure[] {
238
268
  return failures;
239
269
  }
240
270
 
241
- function checkSpace(space: CheckSpace): readonly CheckFailure[] {
271
+ async function checkSpace(space: CheckSpace): Promise<readonly CheckFailure[]> {
272
+ const snapshotFailures = await Promise.all(
273
+ space.packages.map((pkg) =>
274
+ checkSnapshotConsistency(space.spaceId, pkg, space.projectMigrationsDir),
275
+ ),
276
+ );
242
277
  return [
243
278
  ...checkManifestFilesPresent(space),
244
- ...space.packages
245
- .map((pkg) => checkSnapshotConsistency(space.spaceId, pkg))
246
- .filter((f): f is CheckFailure => f !== null),
279
+ ...snapshotFailures.filter((f): f is CheckFailure => f !== null),
247
280
  ...checkReachability(space),
248
281
  ...checkDanglingRefs(space),
249
282
  ];
@@ -272,9 +305,9 @@ export interface RunMigrationCheckInputs {
272
305
  * Aggregate-integrity violations (which already span every space) are folded
273
306
  * in by the caller, not here.
274
307
  */
275
- export function runMigrationCheck(
308
+ export async function runMigrationCheck(
276
309
  inputs: RunMigrationCheckInputs,
277
- ): Result<MigrationCheckResult, CliStructuredError> {
310
+ ): Promise<Result<MigrationCheckResult, CliStructuredError>> {
278
311
  const { spaces, spaceFilter } = inputs;
279
312
 
280
313
  if (spaceFilter !== undefined && !isValidSpaceId(spaceFilter)) {
@@ -287,7 +320,7 @@ export function runMigrationCheck(
287
320
  const scopedSpaces =
288
321
  spaceFilter !== undefined ? spaces.filter((s) => s.spaceId === spaceFilter) : spaces;
289
322
 
290
- const failures = scopedSpaces.flatMap(checkSpace);
323
+ const failures = (await Promise.all(scopedSpaces.map(checkSpace))).flat();
291
324
  if (failures.length === 0) {
292
325
  return ok({ ok: true, failures: [], summary: 'All checks passed' });
293
326
  }
@@ -365,7 +398,7 @@ async function executeMigrationCheckCommand(
365
398
  });
366
399
  }
367
400
 
368
- const checkResult = runMigrationCheck({
401
+ const checkResult = await runMigrationCheck({
369
402
  spaces,
370
403
  ...(options.space !== undefined ? { spaceFilter: options.space } : {}),
371
404
  });
@@ -558,7 +591,11 @@ async function checkSingleTarget(
558
591
  });
559
592
  }
560
593
 
561
- const snapshotFailure = checkSnapshotConsistency(matchedSpace.spaceId, matchedPkg);
594
+ const snapshotFailure = await checkSnapshotConsistency(
595
+ matchedSpace.spaceId,
596
+ matchedPkg,
597
+ matchedSpace.projectMigrationsDir,
598
+ );
562
599
  if (snapshotFailure) failures.push(snapshotFailure);
563
600
 
564
601
  const resolvedSpaceId = matchedSpace.spaceId !== 'app' ? matchedSpace.spaceId : undefined;
@@ -9,7 +9,6 @@ import {
9
9
  APP_SPACE_ID,
10
10
  isValidSpaceId,
11
11
  listContractSpaceDirectories,
12
- RESERVED_SPACE_SUBDIR_NAMES,
13
12
  } from '@prisma-next/migration-tools/spaces';
14
13
  import { ifDefined } from '@prisma-next/utils/defined';
15
14
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
@@ -81,10 +80,7 @@ export function listRefsByContractHash(
81
80
 
82
81
  async function orderedOnDiskSpaceIds(projectMigrationsDir: string): Promise<readonly string[]> {
83
82
  const candidateDirs = await listContractSpaceDirectories(projectMigrationsDir);
84
- return candidateDirs
85
- .filter((name) => !RESERVED_SPACE_SUBDIR_NAMES.has(name))
86
- .filter(isValidSpaceId)
87
- .sort(compareSpaceIds);
83
+ return candidateDirs.filter(isValidSpaceId).sort(compareSpaceIds);
88
84
  }
89
85
 
90
86
  /**
@@ -14,12 +14,13 @@ import type { Contract } from '@prisma-next/contract/types';
14
14
  import { getEmittedArtifactPaths } from '@prisma-next/emitter';
15
15
  import { APP_SPACE_ID, createControlStack } from '@prisma-next/framework-components/control';
16
16
  import { loadContractSpaceAggregate } from '@prisma-next/migration-tools/aggregate';
17
- import { computeMigrationHash } from '@prisma-next/migration-tools/hash';
18
17
  import {
19
- copyFilesWithRename,
20
- formatMigrationDirName,
21
- writeMigrationPackage,
22
- } from '@prisma-next/migration-tools/io';
18
+ contractSnapshotDir,
19
+ snapshotsImportPathFrom,
20
+ writeContractSnapshot,
21
+ } from '@prisma-next/migration-tools/contract-snapshot-store';
22
+ import { computeMigrationHash } from '@prisma-next/migration-tools/hash';
23
+ import { formatMigrationDirName, writeMigrationPackage } from '@prisma-next/migration-tools/io';
23
24
  import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
24
25
  import { findLatestMigration } from '@prisma-next/migration-tools/migration-graph';
25
26
  import { writeMigrationTs } from '@prisma-next/migration-tools/migration-ts';
@@ -28,7 +29,6 @@ import { Command } from 'commander';
28
29
  import { join, relative } from 'pathe';
29
30
  import {
30
31
  CliStructuredError,
31
- errorFileNotFound,
32
32
  errorRuntime,
33
33
  errorTargetMigrationNotSupported,
34
34
  errorUnexpected,
@@ -133,7 +133,6 @@ async function executeMigrationNewCommand(
133
133
  const graph = aggregate.app.graph();
134
134
 
135
135
  let fromHash: string | null = null;
136
- let fromContractSourceDir: string | null = null;
137
136
 
138
137
  if (packages.length > 0) {
139
138
  if (options.from) {
@@ -147,17 +146,10 @@ async function executeMigrationNewCommand(
147
146
  );
148
147
  }
149
148
  fromHash = match.metadata.to;
150
- fromContractSourceDir = match.dirPath;
151
149
  } else {
152
150
  const latestMigration = findLatestMigration(graph);
153
151
  if (latestMigration) {
154
152
  fromHash = latestMigration.to;
155
- const leafPkg = packages.find(
156
- (p) => p.metadata.migrationHash === latestMigration.migrationHash,
157
- );
158
- if (leafPkg) {
159
- fromContractSourceDir = leafPkg.dirPath;
160
- }
161
153
  }
162
154
  }
163
155
  }
@@ -209,39 +201,23 @@ async function executeMigrationNewCommand(
209
201
 
210
202
  await writeMigrationPackage(packageDir, metadata, []);
211
203
  const destinationArtifacts = getEmittedArtifactPaths(contractPathAbsolute);
212
- await copyFilesWithRename(packageDir, [
213
- { sourcePath: destinationArtifacts.jsonPath, destName: 'end-contract.json' },
214
- { sourcePath: destinationArtifacts.dtsPath, destName: 'end-contract.d.ts' },
204
+ const [contractJsonRaw, contractDts] = await Promise.all([
205
+ readFile(destinationArtifacts.jsonPath, 'utf-8'),
206
+ readFile(destinationArtifacts.dtsPath, 'utf-8'),
215
207
  ]);
216
- if (fromContractSourceDir !== null) {
217
- const sourceArtifacts = getEmittedArtifactPaths(
218
- join(fromContractSourceDir, 'end-contract.json'),
219
- );
220
- try {
221
- await copyFilesWithRename(packageDir, [
222
- { sourcePath: sourceArtifacts.jsonPath, destName: 'start-contract.json' },
223
- { sourcePath: sourceArtifacts.dtsPath, destName: 'start-contract.d.ts' },
224
- ]);
225
- } catch (error) {
226
- if (error instanceof Error && (error as { code?: string }).code === 'ENOENT') {
227
- return notOk(
228
- errorFileNotFound(sourceArtifacts.jsonPath, {
229
- why: `Predecessor migration is missing its destination contract snapshot at ${sourceArtifacts.jsonPath}`,
230
- fix: 'Re-emit the predecessor migration (`prisma-next migration plan` from its source) so its sibling `end-contract.json` is restored, then re-run this command.',
231
- }),
232
- );
233
- }
234
- throw error;
235
- }
236
- }
208
+ await writeContractSnapshot(migrationsDir, toStorageHash, {
209
+ contractJson: JSON.parse(contractJsonRaw) as unknown,
210
+ contractDts,
211
+ });
237
212
 
238
213
  const planner = migrations.createPlanner(controlAdapter);
239
214
  const emptyPlan = planner.emptyMigration(
240
215
  {
241
216
  packageDir,
242
- contractJsonPath: join(packageDir, 'end-contract.json'),
217
+ contractJsonPath: join(contractSnapshotDir(migrationsDir, toStorageHash), 'contract.json'),
243
218
  fromHash,
244
219
  toHash: toStorageHash,
220
+ snapshotsImportPath: snapshotsImportPathFrom(packageDir, migrationsDir),
245
221
  },
246
222
  APP_SPACE_ID,
247
223
  );