@prisma-next/cli 0.12.0 → 0.13.0-dev.1

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 (213) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.mjs +180 -163
  3. package/dist/cli.mjs.map +1 -1
  4. package/dist/{client-KgJorIvG.mjs → client-CJzuo5wX.mjs} +222 -107
  5. package/dist/client-CJzuo5wX.mjs.map +1 -0
  6. package/dist/{command-helpers-Bbw1GbwL.mjs → command-helpers-DGMvGBeX.mjs} +318 -25
  7. package/dist/command-helpers-DGMvGBeX.mjs.map +1 -0
  8. package/dist/commands/contract-emit.d.mts.map +1 -1
  9. package/dist/commands/contract-emit.mjs +1 -1
  10. package/dist/commands/contract-infer.d.mts.map +1 -1
  11. package/dist/commands/contract-infer.mjs +1 -1
  12. package/dist/commands/db-init.d.mts.map +1 -1
  13. package/dist/commands/db-init.mjs +4 -5
  14. package/dist/commands/db-init.mjs.map +1 -1
  15. package/dist/commands/db-schema.d.mts.map +1 -1
  16. package/dist/commands/db-schema.mjs +3 -3
  17. package/dist/commands/db-schema.mjs.map +1 -1
  18. package/dist/commands/db-sign.d.mts.map +1 -1
  19. package/dist/commands/db-sign.mjs +6 -6
  20. package/dist/commands/db-sign.mjs.map +1 -1
  21. package/dist/commands/db-update.d.mts.map +1 -1
  22. package/dist/commands/db-update.mjs +10 -7
  23. package/dist/commands/db-update.mjs.map +1 -1
  24. package/dist/commands/db-verify.d.mts.map +1 -1
  25. package/dist/commands/db-verify.mjs +1 -1
  26. package/dist/commands/migrate.d.mts +37 -3
  27. package/dist/commands/migrate.d.mts.map +1 -1
  28. package/dist/commands/migrate.mjs +298 -12
  29. package/dist/commands/migrate.mjs.map +1 -1
  30. package/dist/commands/migration-check.d.mts +55 -13
  31. package/dist/commands/migration-check.d.mts.map +1 -1
  32. package/dist/commands/migration-check.mjs +3 -2
  33. package/dist/commands/migration-graph.d.mts +17 -8
  34. package/dist/commands/migration-graph.d.mts.map +1 -1
  35. package/dist/commands/migration-graph.mjs +185 -2
  36. package/dist/commands/migration-graph.mjs.map +1 -0
  37. package/dist/commands/migration-list.d.mts +26 -27
  38. package/dist/commands/migration-list.d.mts.map +1 -1
  39. package/dist/commands/migration-list.mjs +2 -190
  40. package/dist/commands/migration-log.d.mts +9 -19
  41. package/dist/commands/migration-log.d.mts.map +1 -1
  42. package/dist/commands/migration-log.mjs +1 -137
  43. package/dist/commands/migration-new.d.mts.map +1 -1
  44. package/dist/commands/migration-new.mjs +6 -5
  45. package/dist/commands/migration-new.mjs.map +1 -1
  46. package/dist/commands/migration-plan.d.mts +1 -1
  47. package/dist/commands/migration-plan.d.mts.map +1 -1
  48. package/dist/commands/migration-plan.mjs +1 -1
  49. package/dist/commands/migration-show.d.mts +17 -21
  50. package/dist/commands/migration-show.d.mts.map +1 -1
  51. package/dist/commands/migration-show.mjs +24 -36
  52. package/dist/commands/migration-show.mjs.map +1 -1
  53. package/dist/commands/migration-status.d.mts +42 -144
  54. package/dist/commands/migration-status.d.mts.map +1 -1
  55. package/dist/commands/migration-status.mjs +3 -759
  56. package/dist/commands/ref.d.mts +1 -1
  57. package/dist/commands/ref.d.mts.map +1 -1
  58. package/dist/commands/ref.mjs +4 -4
  59. package/dist/commands/ref.mjs.map +1 -1
  60. package/dist/commands/telemetry/index.d.mts +7 -0
  61. package/dist/commands/telemetry/index.d.mts.map +1 -0
  62. package/dist/commands/telemetry/index.mjs +2 -0
  63. package/dist/{config-loader-B6sJjXTv.mjs → config-loader-p9JMrekQ.mjs} +1 -1
  64. package/dist/{config-loader-B6sJjXTv.mjs.map → config-loader-p9JMrekQ.mjs.map} +1 -1
  65. package/dist/config-loader.mjs +1 -1
  66. package/dist/{contract-at-errors-BxP-TOMl.mjs → contract-at-errors-CFXsstzm.mjs} +2 -2
  67. package/dist/{contract-at-errors-BxP-TOMl.mjs.map → contract-at-errors-CFXsstzm.mjs.map} +1 -1
  68. package/dist/{contract-emit-DxcGl4Uq.mjs → contract-emit-B_qriF8B.mjs} +5 -5
  69. package/dist/{contract-emit-DxcGl4Uq.mjs.map → contract-emit-B_qriF8B.mjs.map} +1 -1
  70. package/dist/{contract-emit-D-4jrNve.mjs → contract-emit-C8HmtboH.mjs} +12 -7
  71. package/dist/contract-emit-C8HmtboH.mjs.map +1 -0
  72. package/dist/{contract-enrichment-a0V5Y_mL.mjs → contract-enrichment-gn9sWbPw.mjs} +1 -1
  73. package/dist/{contract-enrichment-a0V5Y_mL.mjs.map → contract-enrichment-gn9sWbPw.mjs.map} +1 -1
  74. package/dist/{contract-infer-D8uEbJuu.mjs → contract-infer-BYT_ra_U.mjs} +5 -5
  75. package/dist/contract-infer-BYT_ra_U.mjs.map +1 -0
  76. package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs → contract-space-aggregate-loader-ClI1KN6d.mjs} +5 -5
  77. package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs.map → contract-space-aggregate-loader-ClI1KN6d.mjs.map} +1 -1
  78. package/dist/{db-verify-v_vUKXTU.mjs → db-verify-C24FKhb7.mjs} +6 -6
  79. package/dist/{db-verify-v_vUKXTU.mjs.map → db-verify-C24FKhb7.mjs.map} +1 -1
  80. package/dist/exports/control-api.d.mts +5 -3
  81. package/dist/exports/control-api.d.mts.map +1 -1
  82. package/dist/exports/control-api.mjs +3 -3
  83. package/dist/exports/index.mjs +1 -1
  84. package/dist/exports/index.mjs.map +1 -1
  85. package/dist/exports/init-output.d.mts +1 -3
  86. package/dist/exports/init-output.d.mts.map +1 -1
  87. package/dist/exports/init-output.mjs +1 -1
  88. package/dist/{extension-pack-inputs-IDvjRCi3.mjs → extension-pack-inputs-1ySHqxKG.mjs} +1 -1
  89. package/dist/{extension-pack-inputs-IDvjRCi3.mjs.map → extension-pack-inputs-1ySHqxKG.mjs.map} +1 -1
  90. package/dist/{framework-components-fYXjz_in.mjs → framework-components-YVQHhPH7.mjs} +2 -2
  91. package/dist/{framework-components-fYXjz_in.mjs.map → framework-components-YVQHhPH7.mjs.map} +1 -1
  92. package/dist/{global-flags-DEHjV8_s.d.mts → global-flags-BpoOYtNZ.d.mts} +1 -1
  93. package/dist/{global-flags-DEHjV8_s.d.mts.map → global-flags-BpoOYtNZ.d.mts.map} +1 -1
  94. package/dist/{init-Cv9UzWL5.mjs → init-0HwB-Vh8.mjs} +5 -58
  95. package/dist/init-0HwB-Vh8.mjs.map +1 -0
  96. package/dist/{inspect-live-schema-C6ohV_oQ.mjs → inspect-live-schema-DF6IwcDl.mjs} +7 -5
  97. package/dist/inspect-live-schema-DF6IwcDl.mjs.map +1 -0
  98. package/dist/migration-check-soB5uZEQ.mjs +573 -0
  99. package/dist/migration-check-soB5uZEQ.mjs.map +1 -0
  100. package/dist/migration-cli.mjs +1 -1
  101. package/dist/migration-cli.mjs.map +1 -1
  102. package/dist/{migration-command-scaffold-CjvwO6at.mjs → migration-command-scaffold-DA-Lhx6o.mjs} +5 -5
  103. package/dist/{migration-command-scaffold-CjvwO6at.mjs.map → migration-command-scaffold-DA-Lhx6o.mjs.map} +1 -1
  104. package/dist/migration-graph-command-render-CEez7YUK.mjs +1960 -0
  105. package/dist/migration-graph-command-render-CEez7YUK.mjs.map +1 -0
  106. package/dist/migration-list-DlJJ_38Z.mjs +230 -0
  107. package/dist/migration-list-DlJJ_38Z.mjs.map +1 -0
  108. package/dist/migration-log-CG0qQAFm.mjs +222 -0
  109. package/dist/migration-log-CG0qQAFm.mjs.map +1 -0
  110. package/dist/migration-path-target-Ce6OZImp.mjs +38 -0
  111. package/dist/migration-path-target-Ce6OZImp.mjs.map +1 -0
  112. package/dist/{migration-plan-9DJ7q7_z.mjs → migration-plan-z5Ing-TD.mjs} +9 -8
  113. package/dist/migration-plan-z5Ing-TD.mjs.map +1 -0
  114. package/dist/migration-status-CgWSoI_g.mjs +446 -0
  115. package/dist/migration-status-CgWSoI_g.mjs.map +1 -0
  116. package/dist/{output-B60Gw5fu.mjs → output-mEQ74_nd.mjs} +1 -1
  117. package/dist/{output-B60Gw5fu.mjs.map → output-mEQ74_nd.mjs.map} +1 -1
  118. package/dist/{progress-adapter-C644QK8l.mjs → progress-adapter-CjAeTxY_.mjs} +1 -1
  119. package/dist/{progress-adapter-C644QK8l.mjs.map → progress-adapter-CjAeTxY_.mjs.map} +1 -1
  120. package/dist/{ref-advancement-DUZqsue6.mjs → ref-advancement-BkXlikCA.mjs} +1 -1
  121. package/dist/{ref-advancement-DUZqsue6.mjs.map → ref-advancement-BkXlikCA.mjs.map} +1 -1
  122. package/dist/schemas-CeGMYFYX.d.mts +191 -0
  123. package/dist/schemas-CeGMYFYX.d.mts.map +1 -0
  124. package/dist/schemas-KhXMzNA_.mjs +112 -0
  125. package/dist/schemas-KhXMzNA_.mjs.map +1 -0
  126. package/dist/telemetry-BIM4beEO.mjs +122 -0
  127. package/dist/telemetry-BIM4beEO.mjs.map +1 -0
  128. package/dist/{terminal-ui-5Y6mrg93.d.mts → terminal-ui-DGRNFWna.d.mts} +1 -1
  129. package/dist/terminal-ui-DGRNFWna.d.mts.map +1 -0
  130. package/dist/{types-Dt_SfqFm.d.mts → types-C_tYiJYx.d.mts} +53 -31
  131. package/dist/types-C_tYiJYx.d.mts.map +1 -0
  132. package/dist/{verify-DCA9Sldu.mjs → verify-DcOYZ1tH.mjs} +2 -2
  133. package/dist/{verify-DCA9Sldu.mjs.map → verify-DcOYZ1tH.mjs.map} +1 -1
  134. package/package.json +26 -22
  135. package/src/cli.ts +5 -0
  136. package/src/commands/contract-infer.ts +2 -2
  137. package/src/commands/db-update.ts +7 -1
  138. package/src/commands/init/index.ts +6 -35
  139. package/src/commands/init/init.ts +1 -14
  140. package/src/commands/init/inputs.ts +0 -75
  141. package/src/commands/inspect-live-schema.ts +10 -0
  142. package/src/commands/json/schemas.ts +195 -0
  143. package/src/commands/migrate.ts +527 -8
  144. package/src/commands/migration-check.ts +469 -134
  145. package/src/commands/migration-graph.ts +164 -91
  146. package/src/commands/migration-list.ts +72 -39
  147. package/src/commands/migration-log.ts +52 -102
  148. package/src/commands/migration-new.ts +2 -1
  149. package/src/commands/migration-plan.ts +2 -1
  150. package/src/commands/migration-show.ts +31 -66
  151. package/src/commands/migration-status-overlay.ts +61 -0
  152. package/src/commands/migration-status.ts +458 -1066
  153. package/src/commands/telemetry/index.ts +107 -0
  154. package/src/commands/telemetry/status.ts +67 -0
  155. package/src/control-api/client.ts +70 -9
  156. package/src/control-api/operations/contract-emit.ts +22 -2
  157. package/src/control-api/operations/db-init.ts +6 -3
  158. package/src/control-api/operations/{db-apply.ts → db-run.ts} +55 -14
  159. package/src/control-api/operations/db-update.ts +7 -4
  160. package/src/control-api/operations/db-verify.ts +15 -5
  161. package/src/control-api/operations/{migration-apply.ts → migrate.ts} +181 -80
  162. package/src/control-api/operations/{apply.ts → run-migration.ts} +33 -27
  163. package/src/control-api/types.ts +56 -29
  164. package/src/utils/cli-errors.ts +70 -2
  165. package/src/utils/formatters/errors.ts +11 -0
  166. package/src/utils/formatters/migration-graph-command-render.ts +239 -0
  167. package/src/utils/formatters/migration-graph-grid-layout.ts +1134 -0
  168. package/src/utils/formatters/migration-graph-labels.ts +408 -0
  169. package/src/utils/formatters/migration-graph-model.ts +103 -0
  170. package/src/utils/formatters/migration-graph-occlusion-render.ts +258 -0
  171. package/src/utils/formatters/migration-graph-rows.ts +128 -15
  172. package/src/utils/formatters/migration-graph-space-render.ts +188 -0
  173. package/src/utils/formatters/migration-list-data-column.ts +4 -91
  174. package/src/utils/formatters/migration-list-graph-topology.ts +72 -94
  175. package/src/utils/formatters/migration-list-render.ts +135 -71
  176. package/src/utils/formatters/migration-list-styler.ts +46 -5
  177. package/src/utils/formatters/migration-list-types.ts +5 -21
  178. package/src/utils/formatters/migration-log-table.ts +205 -0
  179. package/src/utils/formatters/migrations.ts +33 -11
  180. package/src/utils/global-flags.ts +35 -0
  181. package/src/utils/integrity-violation-to-check-failure.ts +28 -19
  182. package/src/utils/legend.ts +38 -0
  183. package/src/utils/migration-path-target.ts +60 -0
  184. package/src/utils/telemetry.ts +68 -32
  185. package/dist/client-KgJorIvG.mjs.map +0 -1
  186. package/dist/command-helpers-Bbw1GbwL.mjs.map +0 -1
  187. package/dist/commands/migration-list.mjs.map +0 -1
  188. package/dist/commands/migration-log.mjs.map +0 -1
  189. package/dist/commands/migration-status.mjs.map +0 -1
  190. package/dist/contract-emit-D-4jrNve.mjs.map +0 -1
  191. package/dist/contract-infer-D8uEbJuu.mjs.map +0 -1
  192. package/dist/graph-render-rFAqZujX.mjs +0 -1081
  193. package/dist/graph-render-rFAqZujX.mjs.map +0 -1
  194. package/dist/init-Cv9UzWL5.mjs.map +0 -1
  195. package/dist/inspect-live-schema-C6ohV_oQ.mjs.map +0 -1
  196. package/dist/migration-check-BiBJoYYW.mjs +0 -341
  197. package/dist/migration-check-BiBJoYYW.mjs.map +0 -1
  198. package/dist/migration-graph-D7DVUElV.mjs +0 -1232
  199. package/dist/migration-graph-D7DVUElV.mjs.map +0 -1
  200. package/dist/migration-list-styler-BRwF4-gy.mjs +0 -399
  201. package/dist/migration-list-styler-BRwF4-gy.mjs.map +0 -1
  202. package/dist/migration-plan-9DJ7q7_z.mjs.map +0 -1
  203. package/dist/migration-types-D2FW63pr.d.mts +0 -15
  204. package/dist/migration-types-D2FW63pr.d.mts.map +0 -1
  205. package/dist/migrations-Cv2jxNNK.mjs +0 -228
  206. package/dist/migrations-Cv2jxNNK.mjs.map +0 -1
  207. package/dist/terminal-ui-5Y6mrg93.d.mts.map +0 -1
  208. package/dist/types-Dt_SfqFm.d.mts.map +0 -1
  209. package/src/utils/formatters/graph-migration-mapper.ts +0 -235
  210. package/src/utils/formatters/graph-render.ts +0 -1323
  211. package/src/utils/formatters/graph-types.ts +0 -120
  212. package/src/utils/formatters/migration-graph-layout.ts +0 -1119
  213. package/src/utils/formatters/migration-graph-tree-render.ts +0 -459
@@ -1,32 +1,11 @@
1
+ import stringWidth from 'string-width';
1
2
  import type { GlyphMode } from '../glyph-mode';
2
- import type { MigrationEdgeKind } from './migration-list-graph-topology';
3
- import type { MigrationListStyler } from './migration-list-render';
4
- import type { MigrationListEntry } from './migration-list-types';
5
3
 
6
4
  export const MIGRATION_LIST_HASH_WIDTH = 7;
7
5
  export const MIGRATION_LIST_EMPTY_SOURCE = '∅';
8
6
  export const MIGRATION_LIST_ASCII_EMPTY_SOURCE = '-';
9
7
  export const MIGRATION_LIST_FORWARD_EDGE_GLYPH = '→';
10
8
  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
9
 
31
10
  export function migrationListForwardArrow(glyphMode: GlyphMode): string {
32
11
  return glyphMode === 'ascii'
@@ -43,73 +22,7 @@ export function abbreviateContractHash(hash: string): string {
43
22
  return stripped.slice(0, MIGRATION_LIST_HASH_WIDTH);
44
23
  }
45
24
 
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));
25
+ export function padFromHashColumn(text: string, width: number): string {
26
+ const padding = Math.max(0, width - stringWidth(text));
27
+ return `${' '.repeat(padding)}${text}`;
115
28
  }
@@ -11,8 +11,7 @@ export interface MigrationListGraphTopology {
11
11
  }
12
12
 
13
13
  // ---------------------------------------------------------------------------
14
- // Shared classifier — operates on a normalized edge shape common to both
15
- // MigrationListEntry (Tier-2) and MigrationEdge / MigrationGraph (Tier-3).
14
+ // Shared classifier — operates on a normalized edge shape for MigrationGraph.
16
15
  // ---------------------------------------------------------------------------
17
16
 
18
17
  interface NormalizedEdge {
@@ -30,10 +29,30 @@ function bumpDegree(map: Map<string, number>, key: string): void {
30
29
  map.set(key, (map.get(key) ?? 0) + 1);
31
30
  }
32
31
 
33
- function forwardRootsForDepth(
32
+ function compareNodesRootFirst(a: string, b: string): number {
33
+ if (a === EMPTY_CONTRACT_HASH) return -1;
34
+ if (b === EMPTY_CONTRACT_HASH) return 1;
35
+ return a.localeCompare(b);
36
+ }
37
+
38
+ /**
39
+ * Shortest-path distance of each node from the forward roots, over the given
40
+ * candidate edges. Roots are the in-degree-0 nodes (baseline first, then lex);
41
+ * a rooted component therefore distances every node by how many forward steps
42
+ * it sits from a root. A component with no root (a pure cycle) is seeded from
43
+ * its single lexically-smallest node so the cycle still gets a stable layering.
44
+ *
45
+ * Crucially this is *shortest* path, not longest: a backward (rollback) edge
46
+ * `deep → shallow` never offers a shorter route to the already-shallower
47
+ * target, so it is inert here. Distances are thus stable whether or not the
48
+ * rollbacks are still in the candidate set — which is what lets the peel below
49
+ * tell a genuine back-edge (target strictly shallower than source) apart from a
50
+ * forward edge that merely happens to share the back-edge's cycle.
51
+ */
52
+ function forwardDistances(
34
53
  nodes: ReadonlySet<string>,
35
54
  candidates: readonly NormalizedEdge[],
36
- ): readonly string[] {
55
+ ): Map<string, number> {
37
56
  const inDegree = new Map<string, number>();
38
57
  for (const node of nodes) {
39
58
  inDegree.set(node, 0);
@@ -42,44 +61,24 @@ function forwardRootsForDepth(
42
61
  bumpDegree(inDegree, edge.to);
43
62
  }
44
63
 
45
- const roots: string[] = [];
46
- for (const node of nodes) {
47
- if ((inDegree.get(node) ?? 0) === 0) {
48
- roots.push(node);
49
- }
50
- }
51
- roots.sort((a, b) => {
52
- if (a === EMPTY_CONTRACT_HASH) return -1;
53
- if (b === EMPTY_CONTRACT_HASH) return 1;
54
- return a.localeCompare(b);
55
- });
56
- if (roots.length > 0) return roots;
64
+ const roots = [...nodes].filter((node) => (inDegree.get(node) ?? 0) === 0);
65
+ roots.sort(compareNodesRootFirst);
66
+ const seeds = roots.length > 0 ? roots : [...nodes].sort(compareNodesRootFirst).slice(0, 1);
57
67
 
58
- return [...nodes].sort((a, b) => {
59
- if (a === EMPTY_CONTRACT_HASH) return -1;
60
- if (b === EMPTY_CONTRACT_HASH) return 1;
61
- return a.localeCompare(b);
62
- });
63
- }
64
-
65
- function longestPathDepths(
66
- nodes: ReadonlySet<string>,
67
- candidates: readonly NormalizedEdge[],
68
- ): Map<string, number> {
69
- const depth = new Map<string, number>();
70
- for (const root of forwardRootsForDepth(nodes, candidates)) {
71
- depth.set(root, 0);
68
+ const dist = new Map<string, number>();
69
+ for (const seed of seeds) {
70
+ dist.set(seed, 0);
72
71
  }
73
72
 
74
73
  const maxPasses = nodes.size;
75
74
  for (let pass = 0; pass < maxPasses; pass++) {
76
75
  let changed = false;
77
76
  for (const edge of candidates) {
78
- const base = depth.get(edge.from);
77
+ const base = dist.get(edge.from);
79
78
  if (base === undefined) continue;
80
79
  const next = base + 1;
81
- if (next > (depth.get(edge.to) ?? -1)) {
82
- depth.set(edge.to, next);
80
+ if (next < (dist.get(edge.to) ?? Number.POSITIVE_INFINITY)) {
81
+ dist.set(edge.to, next);
83
82
  changed = true;
84
83
  }
85
84
  }
@@ -87,12 +86,12 @@ function longestPathDepths(
87
86
  }
88
87
 
89
88
  for (const node of nodes) {
90
- if (!depth.has(node)) {
91
- depth.set(node, 0);
89
+ if (!dist.has(node)) {
90
+ dist.set(node, 0);
92
91
  }
93
92
  }
94
93
 
95
- return depth;
94
+ return dist;
96
95
  }
97
96
 
98
97
  function canReachForward(
@@ -126,45 +125,24 @@ function canReachForward(
126
125
  return false;
127
126
  }
128
127
 
129
- function isMarginalForwardEdge(
130
- nodes: ReadonlySet<string>,
131
- candidates: readonly NormalizedEdge[],
132
- edge: NormalizedEdge,
133
- ): boolean {
134
- const without = candidates.filter((candidate) => candidate !== edge);
135
- const depthWithout = longestPathDepths(nodes, without);
136
- const depthWith = longestPathDepths(nodes, candidates);
137
- const fromDepth = depthWithout.get(edge.from) ?? 0;
138
- const toWith = depthWith.get(edge.to) ?? 0;
139
- return toWith > fromDepth;
140
- }
141
-
142
- // The first branch is the load-bearing one: a forward edge `from to` is a
143
- // disguised node-skipping rollback when, after removing it, `to` can still
144
- // reach `from` and `from` sits strictly deeper than `to + 1` (a longer path
145
- // already connects them). This branch fires on every cycle-closing edge, and
146
- // the caller peels exactly one edge (dirName-max) per iteration before
147
- // recomputing — so cycles are broken deterministically regardless of edge
148
- // order. `isMarginalForwardEdge` is only a fallback for the residual case and
149
- // is reached only while the candidate set is still cyclic.
150
- function shouldPeelForwardEdge(
151
- nodes: ReadonlySet<string>,
152
- candidates: readonly NormalizedEdge[],
153
- edge: NormalizedEdge,
154
- ): boolean {
155
- const without = candidates.filter((candidate) => candidate !== edge);
156
- const depthWithout = longestPathDepths(nodes, without);
157
- const fromDepth = depthWithout.get(edge.from) ?? 0;
158
- const toWithout = depthWithout.get(edge.to) ?? 0;
159
-
160
- if (canReachForward(edge.to, edge.from, without) && fromDepth > toWithout + 1) {
161
- return true;
162
- }
163
-
164
- return !isMarginalForwardEdge(nodes, candidates, edge);
165
- }
166
-
167
- function peelNonMarginalForwardEdges(
128
+ /**
129
+ * Demote node-skipping rollbacks left forward by the DFS. An edge `from → to`
130
+ * is a rollback exactly when both hold:
131
+ * 1. `to` is a forward-ancestor of `from` — `to` can still reach `from` over
132
+ * the other forward edges, so the edge closes a cycle; and
133
+ * 2. `to` is strictly shallower than `from` (smaller forward distance) the
134
+ * edge points back toward the root rather than advancing history.
135
+ *
136
+ * Condition 2 is the discriminator: in a cycle created by a rollback every edge
137
+ * satisfies condition 1, but only the rollback itself runs deep → shallow. The
138
+ * forward chain edges run shallow → deep and are never peeled, however many
139
+ * rollbacks converge on the same target. Tight back-edges whose source and
140
+ * target sit at the same distance (mutual two-node cycles) are already resolved
141
+ * by the DFS immediate-parent rule, so they never reach this pass. One edge is
142
+ * peeled per iteration (dirName-descending tie-break) and distances/reachability
143
+ * are recomputed, making the outcome independent of edge input order.
144
+ */
145
+ function peelNodeSkippingRollbacks(
168
146
  nodes: ReadonlySet<string>,
169
147
  kindByMigrationHash: Map<string, MigrationEdgeKind>,
170
148
  nonSelf: readonly NormalizedEdge[],
@@ -172,13 +150,18 @@ function peelNonMarginalForwardEdges(
172
150
  let candidates = nonSelf.filter((edge) => kindByMigrationHash.get(edge.hash) === 'forward');
173
151
 
174
152
  while (candidates.length > 0) {
175
- const rollbackCandidates = candidates.filter((edge) =>
176
- shouldPeelForwardEdge(nodes, candidates, edge),
177
- );
178
- if (rollbackCandidates.length === 0) break;
179
-
180
- rollbackCandidates.sort(compareDirNameDesc);
181
- const rollback = rollbackCandidates[0];
153
+ const dist = forwardDistances(nodes, candidates);
154
+ const backEdges = candidates.filter((edge) => {
155
+ const toDist = dist.get(edge.to) ?? 0;
156
+ const fromDist = dist.get(edge.from) ?? 0;
157
+ if (toDist >= fromDist) return false;
158
+ const without = candidates.filter((candidate) => candidate !== edge);
159
+ return canReachForward(edge.to, edge.from, without);
160
+ });
161
+ if (backEdges.length === 0) break;
162
+
163
+ backEdges.sort(compareDirNameDesc);
164
+ const rollback = backEdges[0];
182
165
  if (rollback === undefined) break;
183
166
 
184
167
  kindByMigrationHash.set(rollback.hash, 'rollback');
@@ -189,8 +172,8 @@ function peelNonMarginalForwardEdges(
189
172
  /**
190
173
  * DFS with dirName-descending traversal. A GRAY target is a rollback only when it
191
174
  * is the immediate DFS parent of the source — cross-links to other GRAY nodes
192
- * stay forward. A follow-up peel pass drops node-skipping rollbacks (target can
193
- * reach the source on the forward subgraph and sits more than one rank below).
175
+ * stay forward. A follow-up peel pass demotes node-skipping rollbacks (target is
176
+ * a forward-ancestor of the source and sits strictly shallower than it).
194
177
  */
195
178
  function classifyNormalizedEdges(edges: readonly NormalizedEdge[]): MigrationListGraphTopology {
196
179
  const nodes = new Set<string>();
@@ -307,7 +290,7 @@ function classifyNormalizedEdges(edges: readonly NormalizedEdge[]): MigrationLis
307
290
  runDfsFrom(root);
308
291
  }
309
292
 
310
- peelNonMarginalForwardEdges(nodes, kindByMigrationHash, nonSelf);
293
+ peelNodeSkippingRollbacks(nodes, kindByMigrationHash, nonSelf);
311
294
 
312
295
  const forwardInDegree = new Map<string, number>();
313
296
  const forwardOutDegree = new Map<string, number>();
@@ -331,26 +314,21 @@ function canonicalFrom(from: string | null): string {
331
314
 
332
315
  /**
333
316
  * Classify forward/rollback/self for a Tier-2 `MigrationListEntry[]` edge set.
334
- * Returns the kind of each migration plus the forward in/out degree of each
335
- * contract node. This is the established Tier-2 surface; its behaviour is
336
- * unchanged — only its implementation now delegates to the shared classifier.
337
317
  */
338
318
  export function classifyMigrationListGraphTopology(
339
319
  entries: readonly MigrationListEntry[],
340
320
  ): MigrationListGraphTopology {
341
321
  const normalized: NormalizedEdge[] = entries.map((entry) => ({
342
- hash: entry.migrationHash,
343
- from: canonicalFrom(entry.from),
344
- to: entry.to,
345
- dirName: entry.dirName,
322
+ hash: entry.hash,
323
+ from: canonicalFrom(entry.fromContract),
324
+ to: entry.toContract,
325
+ dirName: entry.name,
346
326
  }));
347
327
  return classifyNormalizedEdges(normalized);
348
328
  }
349
329
 
350
330
  /**
351
331
  * Classify forward/rollback/self for a `MigrationGraph` edge set (Tier-3).
352
- * Delegates to the same shared classifier as `classifyMigrationListGraphTopology`
353
- * so both tiers agree on forward/rollback/self without duplicating logic.
354
332
  */
355
333
  export function classifyMigrationGraphTopology(graph: MigrationGraph): MigrationListGraphTopology {
356
334
  const normalized: NormalizedEdge[] = [];
@@ -1,16 +1,13 @@
1
+ import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
2
+ import type { MigrationEdge, MigrationGraph } from '@prisma-next/migration-tools/graph';
1
3
  import type { GlyphMode } from '../glyph-mode';
4
+ import type { MigrationEdgeAnnotation } from './migration-graph-labels';
2
5
  import {
3
- computeMigrationDirNameWidth,
4
- formatMigrationDataColumn,
5
- migrationListEmptySource,
6
- migrationListForwardArrow,
7
- migrationListKindGlyph,
8
- } from './migration-list-data-column';
9
- import {
10
- classifyMigrationListGraphTopology,
11
- type MigrationEdgeKind,
12
- type MigrationListGraphTopology,
13
- } from './migration-list-graph-topology';
6
+ computeGlobalMaxDirNameWidth,
7
+ computeGlobalMaxEdgeTreePrefixWidth,
8
+ indentMigrationGraphTreeBlock,
9
+ renderMigrationGraphSpaceTree,
10
+ } from './migration-graph-space-render';
14
11
  import type { MigrationListEntry, MigrationListResult } from './migration-list-types';
15
12
 
16
13
  export type { GlyphMode } from '../glyph-mode';
@@ -62,42 +59,83 @@ export const IDENTITY_MIGRATION_LIST_STYLER: MigrationListStyler = {
62
59
  emptyState: (text) => text,
63
60
  };
64
61
 
65
- function resolveEdgeKind(
66
- migrationHash: string,
67
- kindByMigrationHash: ReadonlyMap<string, MigrationEdgeKind>,
68
- ): MigrationEdgeKind {
69
- return kindByMigrationHash.get(migrationHash) ?? 'forward';
62
+ function canonicalFrom(from: string | null): string {
63
+ return from ?? EMPTY_CONTRACT_HASH;
70
64
  }
71
65
 
72
- function formatMigrationRow(
73
- migration: MigrationListEntry,
74
- dirNameWidth: number,
75
- edgeKind: MigrationEdgeKind,
76
- glyphMode: GlyphMode,
77
- style: MigrationListStyler,
78
- ): string {
79
- const kindColumn = `${style.kind(migrationListKindGlyph(glyphMode, edgeKind))} `;
80
- const data = formatMigrationDataColumn(migration, {
81
- dirNameWidth,
82
- edgeKind,
83
- style,
84
- forwardArrow: migrationListForwardArrow(glyphMode),
85
- emptySource: migrationListEmptySource(glyphMode),
86
- });
87
- return `${kindColumn}${data}`;
66
+ export function migrationGraphFromListEntries(
67
+ entries: readonly MigrationListEntry[],
68
+ ): MigrationGraph {
69
+ const nodes = new Set<string>();
70
+ const forwardChain = new Map<string, MigrationEdge[]>();
71
+ const reverseChain = new Map<string, MigrationEdge[]>();
72
+ const migrationByHash = new Map<string, MigrationEdge>();
73
+
74
+ for (const entry of entries) {
75
+ const from = canonicalFrom(entry.fromContract);
76
+ const edge: MigrationEdge = {
77
+ from,
78
+ to: entry.toContract,
79
+ migrationHash: entry.hash,
80
+ dirName: entry.name,
81
+ createdAt: entry.createdAt,
82
+ invariants: entry.providedInvariants,
83
+ };
84
+ nodes.add(from);
85
+ nodes.add(entry.toContract);
86
+ const forward = forwardChain.get(from);
87
+ if (forward) forward.push(edge);
88
+ else forwardChain.set(from, [edge]);
89
+ const reverse = reverseChain.get(entry.toContract);
90
+ if (reverse) reverse.push(edge);
91
+ else reverseChain.set(entry.toContract, [edge]);
92
+ migrationByHash.set(entry.hash, edge);
93
+ }
94
+
95
+ return { nodes, forwardChain, reverseChain, migrationByHash };
96
+ }
97
+
98
+ export function buildEdgeAnnotationsByHashFromListEntries(
99
+ entries: readonly MigrationListEntry[],
100
+ ): ReadonlyMap<string, MigrationEdgeAnnotation> {
101
+ const annotations = new Map<string, MigrationEdgeAnnotation>();
102
+ for (const entry of entries) {
103
+ annotations.set(entry.hash, {
104
+ operationCount: entry.operationCount,
105
+ invariants: entry.providedInvariants,
106
+ });
107
+ }
108
+ return annotations;
109
+ }
110
+
111
+ export function buildRefsByHashFromListEntries(
112
+ entries: readonly MigrationListEntry[],
113
+ ): ReadonlyMap<string, readonly string[]> {
114
+ const refsByHash = new Map<string, readonly string[]>();
115
+ for (const entry of entries) {
116
+ if (entry.refs.length > 0) {
117
+ refsByHash.set(entry.toContract, entry.refs);
118
+ }
119
+ }
120
+ return refsByHash;
88
121
  }
89
122
 
90
123
  function formatEmptyStateLine(spaceId: string, style: MigrationListStyler): string {
91
124
  return style.emptyState(`There are no migrations in migrations/${spaceId}/ yet`);
92
125
  }
93
126
 
94
- function renderSpaceBlock(
127
+ function renderSpaceTreeBlock(
95
128
  spaceId: string,
96
129
  migrations: readonly MigrationListEntry[],
97
130
  multiSpace: boolean,
98
131
  glyphMode: GlyphMode,
99
- kindByMigrationHash: ReadonlyMap<string, MigrationEdgeKind>,
100
132
  style: MigrationListStyler,
133
+ colorize: boolean,
134
+ liveContractHash: string,
135
+ graphForSpace: (spaceId: string) => MigrationGraph | undefined,
136
+ appSpaceId: string | undefined,
137
+ globalMaxEdgeTreePrefixWidth?: number,
138
+ globalMaxDirNameWidth?: number,
101
139
  ): readonly string[] {
102
140
  if (migrations.length === 0) {
103
141
  const emptyLine = formatEmptyStateLine(spaceId, style);
@@ -107,50 +145,75 @@ function renderSpaceBlock(
107
145
  return [style.spaceHeading(`${spaceId}:`), ` ${emptyLine}`];
108
146
  }
109
147
 
110
- const dirNameWidth = computeMigrationDirNameWidth(migrations);
111
- const rows = migrations.map((entry) =>
112
- formatMigrationRow(
113
- entry,
114
- dirNameWidth,
115
- resolveEdgeKind(entry.migrationHash, kindByMigrationHash),
116
- glyphMode,
117
- style,
118
- ),
119
- );
148
+ const graph = graphForSpace(spaceId) ?? migrationGraphFromListEntries(migrations);
149
+ const isAppSpace = appSpaceId === undefined ? undefined : spaceId === appSpaceId;
150
+ const treeOutput = renderMigrationGraphSpaceTree({
151
+ graph,
152
+ migrations,
153
+ liveContractHash,
154
+ glyphMode,
155
+ colorize,
156
+ refsByHash: buildRefsByHashFromListEntries(migrations),
157
+ styler: style,
158
+ ...(isAppSpace !== undefined ? { isAppSpace } : {}),
159
+ ...(globalMaxEdgeTreePrefixWidth !== undefined ? { globalMaxEdgeTreePrefixWidth } : {}),
160
+ ...(globalMaxDirNameWidth !== undefined ? { globalMaxDirNameWidth } : {}),
161
+ });
162
+
120
163
  if (!multiSpace) {
121
- return rows;
164
+ return treeOutput.length === 0 ? [] : [treeOutput];
122
165
  }
123
- return [style.spaceHeading(`${spaceId}:`), ...rows.map((row) => ` ${row}`)];
166
+
167
+ const indented = indentMigrationGraphTreeBlock(treeOutput, ' ');
168
+ return [style.spaceHeading(`${spaceId}:`), indented];
124
169
  }
125
170
 
126
- export function buildMigrationListTopologyBySpace(
127
- result: MigrationListResult,
128
- ): ReadonlyMap<string, MigrationListGraphTopology> {
129
- const topologyBySpaceId = new Map<string, MigrationListGraphTopology>();
130
- for (const space of result.spaces) {
131
- topologyBySpaceId.set(space.spaceId, classifyMigrationListGraphTopology(space.migrations));
132
- }
133
- return topologyBySpaceId;
171
+ export interface RenderMigrationListWithStyleOptions {
172
+ readonly colorize?: boolean;
173
+ readonly liveContractHash?: string;
174
+ readonly graphForSpace?: (spaceId: string) => MigrationGraph | undefined;
175
+ /**
176
+ * The space ID that is the app contract space. When provided, `@contract`
177
+ * and the floating working-contract node are shown only for this space.
178
+ * When absent, the renderer falls back to the default (`isAppSpace: true`
179
+ * for every space), which is safe for single-space callers.
180
+ */
181
+ readonly appSpaceId?: string;
134
182
  }
135
183
 
136
184
  /**
137
- * Compose the styled `migration list` output. The renderer is
138
- * presentation-neutral every token passes through `style` before
139
- * landing in the output, so the same composition serves the pure-text
140
- * path ({@link renderMigrationList} via
141
- * {@link IDENTITY_MIGRATION_LIST_STYLER}) and the ANSI-styled CLI path
142
- * (via the ANSI styler the CLI shell wires up).
185
+ * Compose the styled `migration list` human output via the shared tree
186
+ * renderer. Each on-disk migration is one edge row with package-fact
187
+ * annotations; refs decorate destination contract nodes.
188
+ *
189
+ * `options.colorize` must match whether `style` emits ANSI (e.g. both true for
190
+ * `createAnsiMigrationListStyler({ useColor: true })`).
143
191
  */
144
192
  export function renderMigrationListWithStyle(
145
193
  result: MigrationListResult,
146
194
  style: MigrationListStyler,
147
195
  glyphMode: GlyphMode = 'unicode',
148
- topologyBySpaceId: ReadonlyMap<
149
- string,
150
- MigrationListGraphTopology
151
- > = buildMigrationListTopologyBySpace(result),
196
+ options: RenderMigrationListWithStyleOptions = {},
152
197
  ): string {
153
198
  const multiSpace = result.spaces.length > 1;
199
+ const colorize = options.colorize ?? false;
200
+ const liveContractHash = options.liveContractHash ?? EMPTY_CONTRACT_HASH;
201
+ const graphForSpace = options.graphForSpace ?? (() => undefined);
202
+ const appSpaceId = options.appSpaceId;
203
+ const globalLayoutInputs = multiSpace
204
+ ? result.spaces
205
+ .filter((space) => space.migrations.length > 0)
206
+ .map((space) => ({
207
+ graph: graphForSpace(space.space) ?? migrationGraphFromListEntries(space.migrations),
208
+ liveContractHash,
209
+ }))
210
+ : [];
211
+ const globalMaxEdgeTreePrefixWidth =
212
+ globalLayoutInputs.length > 0
213
+ ? computeGlobalMaxEdgeTreePrefixWidth(globalLayoutInputs)
214
+ : undefined;
215
+ const globalMaxDirNameWidth =
216
+ globalLayoutInputs.length > 0 ? computeGlobalMaxDirNameWidth(globalLayoutInputs) : undefined;
154
217
  const lines: string[] = [];
155
218
 
156
219
  for (let index = 0; index < result.spaces.length; index++) {
@@ -158,18 +221,19 @@ export function renderMigrationListWithStyle(
158
221
  if (index > 0) {
159
222
  lines.push('');
160
223
  }
161
- const topology = topologyBySpaceId.get(space.spaceId);
162
- const kindByMigrationHash =
163
- topology?.kindByMigrationHash ??
164
- classifyMigrationListGraphTopology(space.migrations).kindByMigrationHash;
165
224
  lines.push(
166
- ...renderSpaceBlock(
167
- space.spaceId,
225
+ ...renderSpaceTreeBlock(
226
+ space.space,
168
227
  space.migrations,
169
228
  multiSpace,
170
229
  glyphMode,
171
- kindByMigrationHash,
172
230
  style,
231
+ colorize,
232
+ liveContractHash,
233
+ graphForSpace,
234
+ appSpaceId,
235
+ globalMaxEdgeTreePrefixWidth,
236
+ globalMaxDirNameWidth,
173
237
  ),
174
238
  );
175
239
  }