@prisma-next/cli 0.11.0-dev.5 → 0.11.0-dev.50

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 (170) hide show
  1. package/dist/cli-errors-DFF1LlfU.mjs +215 -0
  2. package/dist/cli-errors-DFF1LlfU.mjs.map +1 -0
  3. package/dist/cli.mjs +9 -10
  4. package/dist/cli.mjs.map +1 -1
  5. package/dist/{client-oXO2WCPD.mjs → client-5uvDppD8.mjs} +23 -21
  6. package/dist/client-5uvDppD8.mjs.map +1 -0
  7. package/dist/{command-helpers-DtavI0wJ.mjs → command-helpers-4UNsRRc4.mjs} +427 -9
  8. package/dist/command-helpers-4UNsRRc4.mjs.map +1 -0
  9. package/dist/commands/contract-emit.d.mts.map +1 -1
  10. package/dist/commands/contract-emit.mjs +1 -1
  11. package/dist/commands/contract-infer.d.mts.map +1 -1
  12. package/dist/commands/contract-infer.mjs +1 -1
  13. package/dist/commands/db-init.d.mts.map +1 -1
  14. package/dist/commands/db-init.mjs +33 -7
  15. package/dist/commands/db-init.mjs.map +1 -1
  16. package/dist/commands/db-schema.d.mts.map +1 -1
  17. package/dist/commands/db-schema.mjs +3 -4
  18. package/dist/commands/db-schema.mjs.map +1 -1
  19. package/dist/commands/db-sign.d.mts.map +1 -1
  20. package/dist/commands/db-sign.mjs +6 -7
  21. package/dist/commands/db-sign.mjs.map +1 -1
  22. package/dist/commands/db-update.d.mts.map +1 -1
  23. package/dist/commands/db-update.mjs +36 -8
  24. package/dist/commands/db-update.mjs.map +1 -1
  25. package/dist/commands/db-verify.d.mts.map +1 -1
  26. package/dist/commands/db-verify.mjs +1 -1
  27. package/dist/commands/migrate.d.mts +5 -1
  28. package/dist/commands/migrate.d.mts.map +1 -1
  29. package/dist/commands/migrate.mjs +79 -39
  30. package/dist/commands/migrate.mjs.map +1 -1
  31. package/dist/commands/migration-check.d.mts +4 -3
  32. package/dist/commands/migration-check.d.mts.map +1 -1
  33. package/dist/commands/migration-check.mjs +1 -280
  34. package/dist/commands/migration-graph.d.mts +1 -1
  35. package/dist/commands/migration-graph.d.mts.map +1 -1
  36. package/dist/commands/migration-graph.mjs +3 -4
  37. package/dist/commands/migration-graph.mjs.map +1 -1
  38. package/dist/commands/migration-list.d.mts +63 -12
  39. package/dist/commands/migration-list.d.mts.map +1 -1
  40. package/dist/commands/migration-list.mjs +2 -103
  41. package/dist/commands/migration-log.d.mts.map +1 -1
  42. package/dist/commands/migration-log.mjs +3 -4
  43. package/dist/commands/migration-log.mjs.map +1 -1
  44. package/dist/commands/migration-new.d.mts.map +1 -1
  45. package/dist/commands/migration-new.mjs +33 -38
  46. package/dist/commands/migration-new.mjs.map +1 -1
  47. package/dist/commands/migration-plan.d.mts +2 -1
  48. package/dist/commands/migration-plan.d.mts.map +1 -1
  49. package/dist/commands/migration-plan.mjs +1 -1
  50. package/dist/commands/migration-show.d.mts +4 -55
  51. package/dist/commands/migration-show.d.mts.map +1 -1
  52. package/dist/commands/migration-show.mjs +62 -153
  53. package/dist/commands/migration-show.mjs.map +1 -1
  54. package/dist/commands/migration-status.d.mts +5 -40
  55. package/dist/commands/migration-status.d.mts.map +1 -1
  56. package/dist/commands/migration-status.mjs +93 -67
  57. package/dist/commands/migration-status.mjs.map +1 -1
  58. package/dist/commands/ref.d.mts +1 -1
  59. package/dist/commands/ref.d.mts.map +1 -1
  60. package/dist/commands/ref.mjs +34 -9
  61. package/dist/commands/ref.mjs.map +1 -1
  62. package/dist/config-loader-B6sJjXTv.mjs.map +1 -1
  63. package/dist/config-loader.d.mts.map +1 -1
  64. package/dist/{contract-emit-o-8VmdQX.mjs → contract-emit-C-CFGZsI.mjs} +9 -6
  65. package/dist/{contract-emit-o-8VmdQX.mjs.map → contract-emit-C-CFGZsI.mjs.map} +1 -1
  66. package/dist/{contract-emit-CmsklifJ.mjs → contract-emit-CuUzzM46.mjs} +5 -6
  67. package/dist/{contract-emit-CmsklifJ.mjs.map → contract-emit-CuUzzM46.mjs.map} +1 -1
  68. package/dist/{contract-enrichment-Dani0mMW.mjs → contract-enrichment-XmUPhmsS.mjs} +4 -25
  69. package/dist/contract-enrichment-XmUPhmsS.mjs.map +1 -0
  70. package/dist/{contract-infer-pKkiCt7C.mjs → contract-infer-C98ZaRhp.mjs} +3 -4
  71. package/dist/{contract-infer-pKkiCt7C.mjs.map → contract-infer-C98ZaRhp.mjs.map} +1 -1
  72. package/dist/contract-space-aggregate-loader-CVHGuA35.mjs +170 -0
  73. package/dist/contract-space-aggregate-loader-CVHGuA35.mjs.map +1 -0
  74. package/dist/{db-verify-AoIUriL4.mjs → db-verify-BWl1Yxi-.mjs} +6 -7
  75. package/dist/{db-verify-AoIUriL4.mjs.map → db-verify-BWl1Yxi-.mjs.map} +1 -1
  76. package/dist/exports/control-api.d.mts +1 -1
  77. package/dist/exports/control-api.d.mts.map +1 -1
  78. package/dist/exports/control-api.mjs +3 -3
  79. package/dist/exports/index.d.mts.map +1 -1
  80. package/dist/exports/index.mjs +1 -1
  81. package/dist/exports/index.mjs.map +1 -1
  82. package/dist/exports/init-output.d.mts.map +1 -1
  83. package/dist/exports/init-output.mjs +1 -1
  84. package/dist/extension-pack-inputs-BiY86HbQ.mjs +62 -0
  85. package/dist/extension-pack-inputs-BiY86HbQ.mjs.map +1 -0
  86. package/dist/{framework-components-65gOHkHB.mjs → framework-components-DTcjouhS.mjs} +2 -2
  87. package/dist/{framework-components-65gOHkHB.mjs.map → framework-components-DTcjouhS.mjs.map} +1 -1
  88. package/dist/{global-flags-CdE7M0d9.d.mts → global-flags-DWsQ6SSI.d.mts} +1 -1
  89. package/dist/global-flags-DWsQ6SSI.d.mts.map +1 -0
  90. package/dist/glyph-mode-CBB4emzO.d.mts +5 -0
  91. package/dist/glyph-mode-CBB4emzO.d.mts.map +1 -0
  92. package/dist/{graph-render-DJVv0_uf.mjs → graph-render-D2FnLpuK.mjs} +1 -1
  93. package/dist/{graph-render-DJVv0_uf.mjs.map → graph-render-D2FnLpuK.mjs.map} +1 -1
  94. package/dist/{init-Db5Itt5r.mjs → init-C7PvN163.mjs} +5 -5
  95. package/dist/{init-Db5Itt5r.mjs.map → init-C7PvN163.mjs.map} +1 -1
  96. package/dist/{inspect-live-schema-LeWvkZVz.mjs → inspect-live-schema-BRCWQ-Sr.mjs} +5 -5
  97. package/dist/{inspect-live-schema-LeWvkZVz.mjs.map → inspect-live-schema-BRCWQ-Sr.mjs.map} +1 -1
  98. package/dist/migration-check-DoskM1nB.mjs +341 -0
  99. package/dist/migration-check-DoskM1nB.mjs.map +1 -0
  100. package/dist/migration-cli.d.mts.map +1 -1
  101. package/dist/migration-cli.mjs +4 -4
  102. package/dist/migration-cli.mjs.map +1 -1
  103. package/dist/{migration-command-scaffold-BtkunvFQ.mjs → migration-command-scaffold-CXLkoIJx.mjs} +5 -5
  104. package/dist/{migration-command-scaffold-BtkunvFQ.mjs.map → migration-command-scaffold-CXLkoIJx.mjs.map} +1 -1
  105. package/dist/migration-list-B2-iQ5Jd.mjs +646 -0
  106. package/dist/migration-list-B2-iQ5Jd.mjs.map +1 -0
  107. package/dist/{migration-plan-C2jeH1J5.mjs → migration-plan-BqmIKQpZ.mjs} +341 -88
  108. package/dist/migration-plan-BqmIKQpZ.mjs.map +1 -0
  109. package/dist/{migration-types-BXWvz12q.d.mts → migration-types-q64xAI_J.d.mts} +1 -1
  110. package/dist/{migration-types-BXWvz12q.d.mts.map → migration-types-q64xAI_J.d.mts.map} +1 -1
  111. package/dist/{migrations-CwZMa1Ck.mjs → migrations-BcVTutso.mjs} +12 -13
  112. package/dist/migrations-BcVTutso.mjs.map +1 -0
  113. package/dist/{output-BlsrGMEF.mjs → output-B60Gw5fu.mjs} +1 -1
  114. package/dist/{output-BlsrGMEF.mjs.map → output-B60Gw5fu.mjs.map} +1 -1
  115. package/dist/{progress-adapter-DFfvZcYL.mjs → progress-adapter-xASh41wr.mjs} +1 -1
  116. package/dist/{progress-adapter-DFfvZcYL.mjs.map → progress-adapter-xASh41wr.mjs.map} +1 -1
  117. package/dist/ref-advancement-DRh5Nquq.mjs +50 -0
  118. package/dist/ref-advancement-DRh5Nquq.mjs.map +1 -0
  119. package/dist/{types-C9FfXb1l.d.mts → types-CEtm6v6a.d.mts} +5 -11
  120. package/dist/types-CEtm6v6a.d.mts.map +1 -0
  121. package/dist/{verify-Bom75OYI.mjs → verify-DOHbbrub.mjs} +2 -2
  122. package/dist/{verify-Bom75OYI.mjs.map → verify-DOHbbrub.mjs.map} +1 -1
  123. package/package.json +20 -20
  124. package/src/commands/db-init.ts +48 -2
  125. package/src/commands/db-update.ts +45 -0
  126. package/src/commands/migrate.ts +120 -40
  127. package/src/commands/migration-check.ts +43 -83
  128. package/src/commands/migration-list.ts +173 -74
  129. package/src/commands/migration-new.ts +44 -48
  130. package/src/commands/migration-plan.ts +359 -128
  131. package/src/commands/migration-show.ts +65 -284
  132. package/src/commands/migration-status.ts +131 -99
  133. package/src/commands/ref.ts +46 -6
  134. package/src/control-api/client.ts +0 -1
  135. package/src/control-api/contract-enrichment.ts +6 -42
  136. package/src/control-api/operations/contract-emit.ts +7 -2
  137. package/src/control-api/operations/db-verify.ts +9 -5
  138. package/src/control-api/operations/migration-apply.ts +11 -19
  139. package/src/control-api/types.ts +0 -7
  140. package/src/migration-cli.ts +4 -4
  141. package/src/utils/cli-errors.ts +224 -0
  142. package/src/utils/command-helpers.ts +9 -4
  143. package/src/utils/contract-space-aggregate-loader.ts +221 -117
  144. package/src/utils/formatters/migration-list-data-column.ts +115 -0
  145. package/src/utils/formatters/migration-list-graph-layout.ts +268 -0
  146. package/src/utils/formatters/migration-list-graph-render.ts +314 -0
  147. package/src/utils/formatters/migration-list-render.ts +194 -0
  148. package/src/utils/formatters/migration-list-styler.ts +61 -0
  149. package/src/utils/formatters/migrations.ts +29 -38
  150. package/src/utils/glyph-mode.ts +22 -0
  151. package/src/utils/integrity-violation-to-check-failure.ts +130 -0
  152. package/src/utils/plan-resolution.ts +257 -0
  153. package/src/utils/ref-advancement.ts +68 -0
  154. package/src/utils/terminal-ui.ts +42 -1
  155. package/dist/cli-errors-Czmx92Zy.d.mts +0 -3
  156. package/dist/cli-errors-Djtz98Vm.mjs +0 -71
  157. package/dist/cli-errors-Djtz98Vm.mjs.map +0 -1
  158. package/dist/client-oXO2WCPD.mjs.map +0 -1
  159. package/dist/command-helpers-DtavI0wJ.mjs.map +0 -1
  160. package/dist/commands/migration-check.mjs.map +0 -1
  161. package/dist/commands/migration-list.mjs.map +0 -1
  162. package/dist/contract-enrichment-Dani0mMW.mjs.map +0 -1
  163. package/dist/contract-space-aggregate-loader-BmNQwlws.mjs +0 -160
  164. package/dist/contract-space-aggregate-loader-BmNQwlws.mjs.map +0 -1
  165. package/dist/global-flags-CdE7M0d9.d.mts.map +0 -1
  166. package/dist/migration-plan-C2jeH1J5.mjs.map +0 -1
  167. package/dist/migrations-CwZMa1Ck.mjs.map +0 -1
  168. package/dist/terminal-ui-BiB_8KNo.mjs +0 -379
  169. package/dist/terminal-ui-BiB_8KNo.mjs.map +0 -1
  170. package/dist/types-C9FfXb1l.d.mts.map +0 -1
@@ -2,109 +2,161 @@ import type { Contract } from '@prisma-next/contract/types';
2
2
  import type { ControlExtensionDescriptor } from '@prisma-next/framework-components/control';
3
3
  import type {
4
4
  ContractSpaceAggregate,
5
- LoadAggregateError,
6
- LoadAggregateInput,
7
- LoadAggregateOutput,
5
+ DeclaredExtensionEntry,
6
+ IntegrityQueryOptions,
7
+ IntegrityViolation,
8
8
  } from '@prisma-next/migration-tools/aggregate';
9
9
  import { loadContractSpaceAggregate } from '@prisma-next/migration-tools/aggregate';
10
- import type { OnDiskMigrationPackage } from '@prisma-next/migration-tools/package';
11
10
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
12
11
  import { CliStructuredError } from './cli-errors';
13
12
  import { toDeclaredExtensionsFromRaw } from './extension-pack-inputs';
14
13
 
14
+ const CONTRACT_SPACES_DOCS_URL = 'https://pris.ly/contract-spaces';
15
+
16
+ function contractSpaceError5002(
17
+ summary: string,
18
+ options: {
19
+ readonly why: string;
20
+ readonly fix: string;
21
+ readonly violations: readonly IntegrityViolation[];
22
+ },
23
+ ): CliStructuredError {
24
+ return new CliStructuredError('5002', summary, {
25
+ domain: 'MIG',
26
+ why: options.why,
27
+ fix: options.fix,
28
+ docsUrl: CONTRACT_SPACES_DOCS_URL,
29
+ meta: { violations: options.violations },
30
+ });
31
+ }
32
+
33
+ /**
34
+ * Build the `5002` structured-error envelope for a contract-space
35
+ * target mismatch. Shared between the declared-extension precheck (the
36
+ * descriptor's configured target disagrees with the project target) and
37
+ * the on-disk-contract check surfaced by `checkIntegrity`.
38
+ */
39
+ function targetMismatchError(
40
+ spaceId: string,
41
+ expected: string,
42
+ actual: string,
43
+ ): CliStructuredError {
44
+ return contractSpaceError5002(`Contract-space target mismatch for "${spaceId}"`, {
45
+ why: `Space "${spaceId}" targets "${actual}" but the project's adapter targets "${expected}".`,
46
+ fix: 'Update the extension descriptor to target the configured database, or change the project adapter.',
47
+ violations: [{ kind: 'targetMismatch', spaceId, expected, actual }],
48
+ });
49
+ }
50
+
15
51
  /**
16
- * Render a {@link LoadAggregateError} into a CLI structured-error
17
- * envelope. Preserves error codes `5001` (layout) and `5002` (marker /
18
- * disjointness / etc.) so existing integration tests and downstream
19
- * tooling continue to assert on the same `meta.violations[]` shape
20
- * they did under the old precheck/marker-check helpers.
52
+ * Human-readable detail for an integrity violation, used as the `why`
53
+ * of the `integrityFailure` envelope. Mirrors the messages the prior
54
+ * throw-on-load loader produced so downstream consumers see the same
55
+ * text for the same on-disk state.
21
56
  */
22
- export function mapLoadAggregateError(error: LoadAggregateError): CliStructuredError {
23
- if (error.kind === 'layoutViolation') {
24
- const lines = error.violations.map((v) => `- [${v.kind}] ${v.spaceId}`);
57
+ function describeIntegrityViolation(violation: IntegrityViolation): string {
58
+ switch (violation.kind) {
59
+ case 'hashMismatch':
60
+ return `Migration "${violation.dirName}" stored hash "${violation.stored}" does not match computed hash "${violation.computed}".`;
61
+ case 'providedInvariantsMismatch':
62
+ return `Migration "${violation.dirName}" providedInvariants in migration.json disagrees with ops.json.`;
63
+ case 'packageUnloadable':
64
+ return `Migration "${violation.dirName}" could not be loaded: ${violation.detail}`;
65
+ case 'sameSourceAndTarget':
66
+ return `Migration "${violation.dirName}" has source equal to target (${violation.hash}) with no data invariant — a true no-op self-edge.`;
67
+ case 'headRefMissing':
68
+ return `Head ref \`refs/head.json\` is missing for contract space "${violation.spaceId}".`;
69
+ case 'headRefNotInGraph':
70
+ return `Head ref ${violation.hash} for contract space "${violation.spaceId}" is not present in the migration graph.`;
71
+ case 'refUnreadable':
72
+ return `Ref "${violation.refName}" for contract space "${violation.spaceId}" is unreadable: ${violation.detail}`;
73
+ case 'duplicateMigrationHash':
74
+ return `Multiple migrations in space "${violation.spaceId}" share migrationHash "${violation.migrationHash}" (${violation.dirNames.join(', ')}).`;
75
+ default: {
76
+ const spaceId = 'spaceId' in violation ? violation.spaceId : '*';
77
+ return `Integrity violation "${violation.kind}" for contract space "${spaceId}".`;
78
+ }
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Map the integrity violations `checkIntegrity` reports into a single
84
+ * CLI structured-error envelope, preserving the error codes the prior
85
+ * throw-on-load loader emitted: `5001` (layout drift, bundled) and
86
+ * `5002` (target / disjointness / contract-validation / structural
87
+ * integrity). Returns `null` when there is nothing to refuse on.
88
+ *
89
+ * Precedence reproduces the prior loader's first-failure ordering:
90
+ * layout drift first (every offence bundled into one envelope), then
91
+ * target mismatch, then disjointness, then a contract-validation
92
+ * failure, then any remaining structural integrity violation.
93
+ */
94
+ export function mapIntegrityViolations(
95
+ violations: readonly IntegrityViolation[],
96
+ ): CliStructuredError | null {
97
+ if (violations.length === 0) return null;
98
+
99
+ const layout = violations.filter(
100
+ (v): v is Extract<IntegrityViolation, { kind: 'orphanSpaceDir' | 'declaredButUnmigrated' }> =>
101
+ v.kind === 'orphanSpaceDir' || v.kind === 'declaredButUnmigrated',
102
+ );
103
+ if (layout.length > 0) {
104
+ const lines = layout.map((v) => `- [${v.kind}] ${v.spaceId}`);
25
105
  const summary =
26
- error.violations.length === 1
106
+ layout.length === 1
27
107
  ? 'Contract-space layout violation detected'
28
- : `Contract-space layout violations detected (${error.violations.length})`;
108
+ : `Contract-space layout violations detected (${layout.length})`;
29
109
  return new CliStructuredError('5001', summary, {
30
110
  domain: 'MIG',
31
111
  why: `The on-disk \`migrations/\` directory and your \`extensionPacks\` declaration are not in agreement.\n${lines.join('\n')}`,
32
- fix: 'Run `prisma-next migrate` to materialise on-disk artefacts for declared extensions, or remove the orphan directory.',
33
- docsUrl: 'https://pris.ly/contract-spaces',
34
- meta: {
35
- violations: error.violations.map((v) => ({
36
- kind: v.kind,
37
- spaceId: v.spaceId,
38
- })),
39
- },
112
+ fix: 'Declare the extension in `extensionPacks` and re-emit its contract-space artefacts, or remove the orphan `migrations/<space>` directory.',
113
+ docsUrl: CONTRACT_SPACES_DOCS_URL,
114
+ meta: { violations: layout },
40
115
  });
41
116
  }
42
- if (error.kind === 'disjointnessViolation') {
43
- return new CliStructuredError(
44
- '5002',
45
- `Contract-space disjointness violation: storage element "${error.element}" claimed by multiple spaces`,
46
- {
47
- domain: 'MIG',
48
- why: `Spaces ${error.claimedBy.map((s) => `"${s}"`).join(', ')} all claim the storage element "${error.element}". Each storage element must be owned by exactly one contract space.`,
49
- fix: 'Update the conflicting contracts so each storage element is claimed by exactly one space.',
50
- docsUrl: 'https://pris.ly/contract-spaces',
51
- meta: {
52
- violations: [
53
- {
54
- kind: 'disjointness',
55
- spaceId: error.claimedBy.join(','),
56
- element: error.element,
57
- claimedBy: error.claimedBy,
58
- },
59
- ],
60
- },
61
- },
117
+
118
+ const targetMismatch = violations.find((v) => v.kind === 'targetMismatch');
119
+ if (targetMismatch && targetMismatch.kind === 'targetMismatch') {
120
+ return targetMismatchError(
121
+ targetMismatch.spaceId,
122
+ targetMismatch.expected,
123
+ targetMismatch.actual,
62
124
  );
63
125
  }
64
- if (error.kind === 'integrityFailure') {
65
- return new CliStructuredError(
66
- '5002',
67
- `Contract-space integrity failure for "${error.spaceId}"`,
126
+
127
+ const disjointness = violations.find((v) => v.kind === 'disjointness');
128
+ if (disjointness && disjointness.kind === 'disjointness') {
129
+ return contractSpaceError5002(
130
+ `Contract-space disjointness violation: storage element "${disjointness.element}" claimed by multiple spaces`,
68
131
  {
69
- domain: 'MIG',
70
- why: error.detail,
71
- fix: 'Run `prisma-next migrate` to refresh on-disk artefacts, or restore the on-disk `migrations/` directory from version control.',
72
- docsUrl: 'https://pris.ly/contract-spaces',
73
- meta: {
74
- violations: [{ kind: 'integrity', spaceId: error.spaceId, detail: error.detail }],
75
- },
132
+ why: `Spaces ${disjointness.claimedBy.map((s) => `"${s}"`).join(', ')} all claim the storage element "${disjointness.element}". Each storage element must be owned by exactly one contract space.`,
133
+ fix: 'Update the conflicting contracts so each storage element is claimed by exactly one space.',
134
+ violations: [disjointness],
76
135
  },
77
136
  );
78
137
  }
79
- if (error.kind === 'validationFailure') {
80
- return new CliStructuredError(
81
- '5002',
82
- `Contract-space contract validation failed for "${error.spaceId}"`,
138
+
139
+ const contractUnreadable = violations.find((v) => v.kind === 'contractUnreadable');
140
+ if (contractUnreadable && contractUnreadable.kind === 'contractUnreadable') {
141
+ return contractSpaceError5002(
142
+ `Contract-space contract validation failed for "${contractUnreadable.spaceId}"`,
83
143
  {
84
- domain: 'MIG',
85
- why: error.detail,
86
- fix: 'Run `prisma-next migrate` to refresh on-disk artefacts, or fix the extension descriptor producing the invalid contract.',
87
- meta: {
88
- violations: [{ kind: 'validation', spaceId: error.spaceId, detail: error.detail }],
89
- },
144
+ why: contractUnreadable.detail,
145
+ fix: 'Re-emit the extension contract with `prisma-next contract emit`, or fix the extension pack descriptor producing the invalid contract.',
146
+ violations: [contractUnreadable],
90
147
  },
91
148
  );
92
149
  }
93
- // targetMismatch
94
- return new CliStructuredError('5002', `Contract-space target mismatch for "${error.spaceId}"`, {
95
- domain: 'MIG',
96
- why: `Space "${error.spaceId}" targets "${error.actual}" but the project's adapter targets "${error.expected}".`,
97
- fix: 'Update the extension descriptor to target the configured database, or change the project adapter.',
98
- meta: {
99
- violations: [
100
- {
101
- kind: 'targetMismatch',
102
- spaceId: error.spaceId,
103
- expected: error.expected,
104
- actual: error.actual,
105
- },
106
- ],
107
- },
150
+
151
+ // Any remaining recoverable structural violation refuses as an
152
+ // integrity failure, surfacing the first one's detail (every violation
153
+ // is still computed; the gate just renders one envelope).
154
+ const structural = violations[0]!;
155
+ const spaceId = 'spaceId' in structural ? structural.spaceId : '*';
156
+ return contractSpaceError5002(`Contract-space integrity failure for "${spaceId}"`, {
157
+ why: describeIntegrityViolation(structural),
158
+ fix: 'Re-emit the affected migration package(s) or restore the on-disk `migrations/` directory from version control.',
159
+ violations: [structural],
108
160
  });
109
161
  }
110
162
 
@@ -121,57 +173,109 @@ export interface BuildAggregateInputs<TFamilyId extends string, TTargetId extend
121
173
  readonly appContract: Contract;
122
174
  readonly extensionPacks: ReadonlyArray<ControlExtensionDescriptor<TFamilyId, TTargetId>>;
123
175
  readonly deserializeContract: (contractJson: unknown) => Contract;
124
- /**
125
- * App-space migration packages to hydrate the app member's
126
- * migration graph with. Defaults to `[]` (matches the `db init` /
127
- * `db update` daily-driver behaviour, where the app's authored
128
- * `migrations/` graph is not walked — the planner uses the synth
129
- * strategy for the app member instead).
130
- *
131
- * `migrate` callers thread the user's authored app-space
132
- * packages (loaded via `loadMigrationPackages(appMigrationsDir)`)
133
- * through here so the graph-walk strategy can plot a path through
134
- * them — the prod-time replay path explicitly forbids synth.
135
- */
136
- readonly appMigrationPackages?: ReadonlyArray<OnDiskMigrationPackage>;
176
+ }
177
+
178
+ function declaredExtensionsFromInputs(
179
+ extensionPacks: BuildAggregateInputs<string, string>['extensionPacks'],
180
+ ): readonly DeclaredExtensionEntry[] {
181
+ return toDeclaredExtensionsFromRaw(extensionPacks as ReadonlyArray<unknown>);
137
182
  }
138
183
 
139
184
  /**
140
- * Run the aggregate loader at the CLI surface, mapping any
141
- * {@link LoadAggregateError} into a {@link CliStructuredError} envelope.
142
- *
143
- * App-side migration packages flow through `inputs.appMigrationPackages`
144
- * (defaulting to `[]`). `db init` / `db update` leave it empty: the
145
- * planner's `synth` strategy is used for the app member (driven by
146
- * `callerPolicy.ignoreGraphFor`), so the app's authored `migrations/`
147
- * graph does not need to be walked. `migrate` threads the
148
- * already-loaded app-space packages through so the graph-walk strategy
149
- * can plot a path through them — replay forbids synth.
150
- *
151
- * @see specs/contract-space-aggregate-spec.md § Loader.
185
+ * Reject extension descriptors whose configured target disagrees with
186
+ * the project target before any on-disk read.
152
187
  */
153
- export async function buildContractSpaceAggregate<
188
+ export function refuseDeclaredExtensionTargetMismatch<
189
+ TFamilyId extends string,
190
+ TTargetId extends string,
191
+ >(inputs: BuildAggregateInputs<TFamilyId, TTargetId>): CliStructuredError | null {
192
+ for (const declared of declaredExtensionsFromInputs(inputs.extensionPacks)) {
193
+ if (declared.targetId !== inputs.targetId) {
194
+ return targetMismatchError(declared.id, inputs.targetId, declared.targetId);
195
+ }
196
+ }
197
+ return null;
198
+ }
199
+
200
+ /**
201
+ * Load the tolerant {@link ContractSpaceAggregate} once at the CLI
202
+ * surface. Construction never throws on disk content; callers query
203
+ * {@link ContractSpaceAggregate.app} / extension facets instead of
204
+ * re-reading `migrations/`.
205
+ */
206
+ export async function loadContractSpaceAggregateForCli<
154
207
  TFamilyId extends string,
155
208
  TTargetId extends string,
156
209
  >(
157
210
  inputs: BuildAggregateInputs<TFamilyId, TTargetId>,
158
211
  ): Promise<Result<ContractSpaceAggregate, CliStructuredError>> {
159
- const declaredExtensions = toDeclaredExtensionsFromRaw(
160
- inputs.extensionPacks as ReadonlyArray<unknown>,
161
- );
212
+ const targetFailure = refuseDeclaredExtensionTargetMismatch(inputs);
213
+ if (targetFailure) {
214
+ return notOk(targetFailure);
215
+ }
162
216
 
163
- const loadInput: LoadAggregateInput = {
164
- targetId: inputs.targetId,
217
+ const aggregate = await loadContractSpaceAggregate({
165
218
  migrationsDir: inputs.migrationsDir,
166
- appContract: inputs.appContract,
167
- declaredExtensions,
168
219
  deserializeContract: inputs.deserializeContract,
169
- appMigrationPackages: inputs.appMigrationPackages ?? [],
170
- };
220
+ appContract: inputs.appContract,
221
+ });
222
+ return ok(aggregate);
223
+ }
224
+
225
+ /**
226
+ * Run `checkIntegrity` on a loaded aggregate and map violations into
227
+ * the contract-space refusal envelope, or return `null` when the model
228
+ * is acceptable for the requested check scope.
229
+ */
230
+ export function refuseContractSpaceIntegrity(
231
+ aggregate: ContractSpaceAggregate,
232
+ options: IntegrityQueryOptions,
233
+ ): CliStructuredError | null {
234
+ return mapIntegrityViolations(aggregate.checkIntegrity(options));
235
+ }
171
236
 
172
- const result: LoadAggregateOutput = await loadContractSpaceAggregate(loadInput);
173
- if (!result.ok) {
174
- return notOk(mapLoadAggregateError(result.failure));
237
+ const PACKAGE_CORRUPTION_KINDS = new Set<IntegrityViolation['kind']>([
238
+ 'hashMismatch',
239
+ 'providedInvariantsMismatch',
240
+ 'packageUnloadable',
241
+ ]);
242
+
243
+ /**
244
+ * Reader-subset integrity refusal for `migration status`: package
245
+ * corruptions only (`hashMismatch`, `providedInvariantsMismatch`,
246
+ * `packageUnloadable`).
247
+ */
248
+ export function refusePackageCorruptionOnAggregate(
249
+ aggregate: ContractSpaceAggregate,
250
+ ): CliStructuredError | null {
251
+ const corruption = aggregate.checkIntegrity().filter((v) => PACKAGE_CORRUPTION_KINDS.has(v.kind));
252
+ return mapIntegrityViolations(corruption);
253
+ }
254
+
255
+ /**
256
+ * Construct the tolerant {@link ContractSpaceAggregate} at the CLI
257
+ * surface and apply the explicit integrity refusal.
258
+ *
259
+ * App-space migration packages are read from `migrations/<app>/` by the
260
+ * loader itself; callers no longer thread them through.
261
+ */
262
+ export async function buildContractSpaceAggregate<
263
+ TFamilyId extends string,
264
+ TTargetId extends string,
265
+ >(
266
+ inputs: BuildAggregateInputs<TFamilyId, TTargetId>,
267
+ ): Promise<Result<ContractSpaceAggregate, CliStructuredError>> {
268
+ const declaredExtensions = declaredExtensionsFromInputs(inputs.extensionPacks);
269
+ const loaded = await loadContractSpaceAggregateForCli(inputs);
270
+ if (!loaded.ok) {
271
+ return loaded;
272
+ }
273
+ const failure = refuseContractSpaceIntegrity(loaded.value, {
274
+ declaredExtensions,
275
+ checkContracts: true,
276
+ });
277
+ if (failure) {
278
+ return notOk(failure);
175
279
  }
176
- return ok(result.value.aggregate);
280
+ return ok(loaded.value);
177
281
  }
@@ -0,0 +1,115 @@
1
+ import type { MigrationEdgeKind } from '@prisma-next/migration-tools/migration-list-graph-topology';
2
+ import type { MigrationListEntry } from '@prisma-next/migration-tools/migration-list-types';
3
+ import type { GlyphMode } from '../glyph-mode';
4
+ import type { MigrationListStyler } from './migration-list-render';
5
+
6
+ export const MIGRATION_LIST_HASH_WIDTH = 7;
7
+ export const MIGRATION_LIST_EMPTY_SOURCE = '∅';
8
+ export const MIGRATION_LIST_ASCII_EMPTY_SOURCE = '-';
9
+ export const MIGRATION_LIST_FORWARD_EDGE_GLYPH = '→';
10
+ export const MIGRATION_LIST_ASCII_FORWARD_EDGE_GLYPH = '->';
11
+ export const MIGRATION_LIST_DECORATION_PREFIX = ' ';
12
+
13
+ export const MIGRATION_LIST_UNICODE_KIND_GLYPH: Record<MigrationEdgeKind, string> = {
14
+ forward: '*',
15
+ rollback: '↩',
16
+ self: '⟲',
17
+ };
18
+
19
+ export const MIGRATION_LIST_ASCII_KIND_GLYPH: Record<MigrationEdgeKind, string> = {
20
+ forward: '*',
21
+ rollback: '<',
22
+ self: '~',
23
+ };
24
+
25
+ export function migrationListKindGlyph(glyphMode: GlyphMode, edgeKind: MigrationEdgeKind): string {
26
+ return glyphMode === 'ascii'
27
+ ? MIGRATION_LIST_ASCII_KIND_GLYPH[edgeKind]
28
+ : MIGRATION_LIST_UNICODE_KIND_GLYPH[edgeKind];
29
+ }
30
+
31
+ export function migrationListForwardArrow(glyphMode: GlyphMode): string {
32
+ return glyphMode === 'ascii'
33
+ ? MIGRATION_LIST_ASCII_FORWARD_EDGE_GLYPH
34
+ : MIGRATION_LIST_FORWARD_EDGE_GLYPH;
35
+ }
36
+
37
+ export function migrationListEmptySource(glyphMode: GlyphMode): string {
38
+ return glyphMode === 'ascii' ? MIGRATION_LIST_ASCII_EMPTY_SOURCE : MIGRATION_LIST_EMPTY_SOURCE;
39
+ }
40
+
41
+ export function abbreviateContractHash(hash: string): string {
42
+ const stripped = hash.startsWith('sha256:') ? hash.slice(7) : hash;
43
+ return stripped.slice(0, MIGRATION_LIST_HASH_WIDTH);
44
+ }
45
+
46
+ export function computeMigrationDirNameWidth(migrations: readonly MigrationListEntry[]): number {
47
+ if (migrations.length === 0) return 0;
48
+ return Math.max(...migrations.map((entry) => entry.dirName.length)) + 2;
49
+ }
50
+
51
+ function formatSourceColumn(
52
+ from: string | null,
53
+ style: MigrationListStyler,
54
+ emptySource: string,
55
+ ): string {
56
+ if (from === null) {
57
+ return style.glyph(emptySource) + ' '.repeat(MIGRATION_LIST_HASH_WIDTH - emptySource.length);
58
+ }
59
+ return style.sourceHash(abbreviateContractHash(from));
60
+ }
61
+
62
+ export function formatDecorations(
63
+ providedInvariants: readonly string[],
64
+ refs: readonly string[],
65
+ style: MigrationListStyler,
66
+ ): string {
67
+ const blocks: string[] = [];
68
+ if (providedInvariants.length > 0) {
69
+ blocks.push(style.invariants(providedInvariants));
70
+ }
71
+ if (refs.length > 0) {
72
+ blocks.push(style.refs(refs));
73
+ }
74
+ if (blocks.length === 0) return '';
75
+ return `${MIGRATION_LIST_DECORATION_PREFIX}${blocks.join(' ')}`;
76
+ }
77
+
78
+ export interface MigrationDataColumnOptions {
79
+ readonly dirNameWidth: number;
80
+ readonly edgeKind: MigrationEdgeKind;
81
+ readonly style: MigrationListStyler;
82
+ readonly forwardArrow?: string;
83
+ readonly emptySource?: string;
84
+ }
85
+
86
+ export function formatMigrationDataColumn(
87
+ migration: MigrationListEntry,
88
+ options: MigrationDataColumnOptions,
89
+ ): string {
90
+ const {
91
+ dirNameWidth,
92
+ edgeKind,
93
+ style,
94
+ forwardArrow = MIGRATION_LIST_FORWARD_EDGE_GLYPH,
95
+ emptySource = MIGRATION_LIST_EMPTY_SOURCE,
96
+ } = options;
97
+ const dirNamePadding = ' '.repeat(Math.max(0, dirNameWidth - migration.dirName.length));
98
+ const dirName = `${style.dirName(migration.dirName)}${dirNamePadding}`;
99
+ const decorations = formatDecorations(migration.providedInvariants, migration.refs, style);
100
+
101
+ if (edgeKind === 'self') {
102
+ const contractHash = migration.from ?? migration.to;
103
+ const hash = style.sourceHash(abbreviateContractHash(contractHash));
104
+ return `${dirName}${hash}${decorations}`;
105
+ }
106
+
107
+ const source = formatSourceColumn(migration.from, style, emptySource);
108
+ const arrow = style.glyph(forwardArrow);
109
+ const dest = style.destHash(abbreviateContractHash(migration.to));
110
+ return `${dirName}${source} ${arrow} ${dest}${decorations}`;
111
+ }
112
+
113
+ export function formatNodeLineDataColumn(contractHash: string, style: MigrationListStyler): string {
114
+ return style.sourceHash(abbreviateContractHash(contractHash));
115
+ }