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

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-VwM8xCZV.mjs +574 -0
  99. package/dist/migration-check-VwM8xCZV.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-CD-LC2Ip.mjs +447 -0
  115. package/dist/migration-status-CD-LC2Ip.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
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Occlusion renderer for the line/plane/occlusion migration-graph.
3
+ *
4
+ * Per cell: pick the topmost-plane line (lowest plane number = drawn on top),
5
+ * look up its glyph, apply colour from the line's lane or role. Lower-plane
6
+ * lines are occluded (not drawn).
7
+ *
8
+ * Colour is forced via createColors({ useColor: true }) regardless of NO_COLOR.
9
+ */
10
+
11
+ import { createColors } from 'colorette';
12
+ import {
13
+ type Cell,
14
+ type CellLine,
15
+ DEFAULT_COLS_PER_LANE,
16
+ type Direction,
17
+ type Grid,
18
+ type PathRole,
19
+ } from './migration-graph-model';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Force-colour seam — always emits ANSI regardless of NO_COLOR.
23
+ // Same technique as gallery-cells.ts.
24
+ // ---------------------------------------------------------------------------
25
+ const palette = createColors({ useColor: true });
26
+
27
+ // Lane colour palette: lane N → colour N+1 (lane0=white, lane1=cyan, …).
28
+ // No red (reads as an error). The on-path highlight uses greenBright (SGR 92),
29
+ // distinct from flat-lane green (SGR 32).
30
+ type Colorizer = (text: string) => string;
31
+
32
+ const LANE_COLORIZERS: Colorizer[] = [
33
+ palette.white,
34
+ palette.cyan,
35
+ palette.yellow,
36
+ palette.blueBright,
37
+ palette.magenta,
38
+ palette.green,
39
+ ];
40
+
41
+ function laneColor(lane: number): Colorizer {
42
+ return LANE_COLORIZERS[lane % LANE_COLORIZERS.length] ?? ((t) => t);
43
+ }
44
+
45
+ /**
46
+ * The colourizer for a lane's hue (lane0 = white, lane1 = cyan, …). Exported
47
+ * so the per-row LABEL renderer can tint a migration name in its lane's colour,
48
+ * matching the node `○`, the edges, and the arrows drawn in the gutter — one
49
+ * colour per lane across glyph and text.
50
+ */
51
+ export function laneColorizer(lane: number): (text: string) => string {
52
+ return laneColor(lane);
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Focus colour: on-path → green, off-path → dim. Read straight off the line's
57
+ // role; a defined role always overrides the lane rotation.
58
+ // ---------------------------------------------------------------------------
59
+ function roleColor(role: PathRole): Colorizer {
60
+ return role === 'on-path' ? palette.greenBright : palette.dim;
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Glyph alphabet — unicode and ASCII variants.
65
+ // ---------------------------------------------------------------------------
66
+ export type GraphGlyphMode = 'unicode' | 'ascii';
67
+
68
+ interface GraphGlyphAlphabet {
69
+ readonly vertical: string;
70
+ readonly horizontal: string;
71
+ readonly cornerUpRight: string;
72
+ readonly cornerDownRight: string;
73
+ readonly cornerUpLeft: string;
74
+ readonly cornerDownLeft: string;
75
+ readonly arrowUp: string;
76
+ readonly arrowDown: string;
77
+ readonly node: string;
78
+ readonly selfLoop: string;
79
+ readonly landingArrow: string;
80
+ readonly fallback: string;
81
+ }
82
+
83
+ const UNICODE_ALPHABET: GraphGlyphAlphabet = {
84
+ vertical: '│',
85
+ horizontal: '─',
86
+ cornerUpRight: '╰',
87
+ cornerDownRight: '╭',
88
+ cornerUpLeft: '╯',
89
+ cornerDownLeft: '╮',
90
+ arrowUp: '↑',
91
+ arrowDown: '↓',
92
+ node: '○',
93
+ selfLoop: '⟲',
94
+ landingArrow: '◂',
95
+ fallback: '?',
96
+ };
97
+
98
+ const ASCII_ALPHABET: GraphGlyphAlphabet = {
99
+ vertical: '|',
100
+ horizontal: '-',
101
+ cornerUpRight: '\\',
102
+ cornerDownRight: '/',
103
+ cornerUpLeft: '/',
104
+ cornerDownLeft: '\\',
105
+ arrowUp: '^',
106
+ arrowDown: 'v',
107
+ node: '*',
108
+ selfLoop: '@',
109
+ landingArrow: '<',
110
+ fallback: '?',
111
+ };
112
+
113
+ function alphabetFor(mode: GraphGlyphMode): GraphGlyphAlphabet {
114
+ return mode === 'ascii' ? ASCII_ALPHABET : UNICODE_ALPHABET;
115
+ }
116
+
117
+ function glyphFor(dirs: ReadonlySet<Direction>, alphabet: GraphGlyphAlphabet): string {
118
+ const has = (d: Direction) => dirs.has(d);
119
+
120
+ if (has('up') && has('down') && !has('left') && !has('right')) return alphabet.vertical;
121
+ if (has('left') && has('right') && !has('up') && !has('down')) return alphabet.horizontal;
122
+ if (has('up') && has('right') && !has('down') && !has('left')) return alphabet.cornerUpRight;
123
+ if (has('down') && has('right') && !has('up') && !has('left')) return alphabet.cornerDownRight;
124
+ if (has('up') && has('left') && !has('down') && !has('right')) return alphabet.cornerUpLeft;
125
+ if (has('down') && has('left') && !has('up') && !has('right')) return alphabet.cornerDownLeft;
126
+ if (has('up') && !has('down') && !has('left') && !has('right')) return alphabet.arrowUp;
127
+ if (has('down') && !has('up') && !has('left') && !has('right')) return alphabet.arrowDown;
128
+
129
+ // Fallback: shouldn't happen in well-formed grids
130
+ return alphabet.fallback;
131
+ }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // renderCell — project one cell to a coloured string fragment.
135
+ // ---------------------------------------------------------------------------
136
+
137
+ const NO_COLOR: Colorizer = (t) => t;
138
+
139
+ function renderCell(cell: Cell, colorEnabled: boolean, alphabet: GraphGlyphAlphabet): string {
140
+ // Node marker overrides everything
141
+ if (cell.node !== undefined) {
142
+ // Every node uses ○ — the ∅ identifier is only used as the label, not as a
143
+ // glyph, per the golden colour model. Colour by role (focus) or lane (flat).
144
+ const colorize = !colorEnabled
145
+ ? NO_COLOR
146
+ : cell.node.role !== undefined
147
+ ? roleColor(cell.node.role)
148
+ : laneColor(cell.node.lane);
149
+ return colorize(alphabet.node);
150
+ }
151
+
152
+ if (cell.lines.length === 0) {
153
+ return ' ';
154
+ }
155
+
156
+ // Pick the drawn line by occlusion: lowest plane number wins (drawn on top).
157
+ // At an equal plane, on-path beats off-path explicitly — never rely on array
158
+ // order to break the tie (the single-owner invariant should already prevent
159
+ // a same-plane on/off-path collision, but the priority is made explicit here).
160
+ const topLine = cell.lines.reduce<CellLine>((best, current) => {
161
+ if (current.plane < best.plane) return current;
162
+ if (current.plane > best.plane) return best;
163
+ if (current.line.role === 'on-path' && best.line.role !== 'on-path') return current;
164
+ return best;
165
+ }, cell.lines[0]!);
166
+
167
+ const glyph =
168
+ topLine.selfLoop === true
169
+ ? alphabet.selfLoop
170
+ : topLine.landingArrow === true
171
+ ? alphabet.landingArrow
172
+ : glyphFor(topLine.directions, alphabet);
173
+ const colorize = !colorEnabled
174
+ ? NO_COLOR
175
+ : topLine.line.role !== undefined
176
+ ? roleColor(topLine.line.role)
177
+ : laneColor(topLine.line.lane);
178
+ return colorize(glyph);
179
+ }
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // RenderGridOptions
183
+ // ---------------------------------------------------------------------------
184
+
185
+ export interface RenderGridOptions {
186
+ readonly colorize?: boolean;
187
+ readonly colsPerLane?: number;
188
+ readonly glyphMode?: GraphGlyphMode;
189
+ }
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // renderGrid — the main render function.
193
+ //
194
+ // Produces the final string: one line per grid row, each cell rendered to
195
+ // a coloured character. Trailing empty cells are trimmed, but we always
196
+ // include up to the last non-empty cell's connector column
197
+ // (the full 2-col-per-lane width for the active lane count).
198
+ // ---------------------------------------------------------------------------
199
+
200
+ /**
201
+ * Render a single grid row to a coloured string. A completely empty row returns
202
+ * the empty string (the row is NOT dropped) so callers that pair grid rows with
203
+ * an external per-row label list keep a 1:1 index correspondence. `renderGrid`
204
+ * itself drops empty rows for its standalone output (but preserves separator rows).
205
+ */
206
+ export function renderGridRow(
207
+ row: readonly (Cell | undefined)[],
208
+ opts: RenderGridOptions = {},
209
+ ): string {
210
+ // Inter-component separator row — always renders as an empty line.
211
+ if (row[0]?.separator === true) {
212
+ return '';
213
+ }
214
+
215
+ // Find the last non-empty cell index
216
+ let lastNonEmpty = -1;
217
+ for (let i = row.length - 1; i >= 0; i--) {
218
+ const cell = row[i];
219
+ if (cell !== undefined && (cell.lines.length > 0 || cell.node !== undefined)) {
220
+ lastNonEmpty = i;
221
+ break;
222
+ }
223
+ }
224
+
225
+ if (lastNonEmpty < 0) {
226
+ return '';
227
+ }
228
+
229
+ // Extend to the next even column boundary (connector col of the current lane)
230
+ // so that connector columns are always present for active lane ranges.
231
+ const colsPerLane = opts.colsPerLane ?? DEFAULT_COLS_PER_LANE;
232
+ const colorEnabled = opts.colorize ?? true;
233
+ const alphabet = alphabetFor(opts.glyphMode ?? 'unicode');
234
+ const lastLane = Math.floor(lastNonEmpty / colsPerLane);
235
+ const lastConnectorCol = lastLane * colsPerLane + (colsPerLane - 1);
236
+ const renderThrough = Math.max(lastNonEmpty, lastConnectorCol);
237
+
238
+ let line = '';
239
+ for (let col = 0; col <= Math.min(renderThrough, row.length - 1); col++) {
240
+ const cell = row[col];
241
+ line += cell === undefined ? ' ' : renderCell(cell, colorEnabled, alphabet);
242
+ }
243
+ return line;
244
+ }
245
+
246
+ export function renderGrid(grid: Grid, opts: RenderGridOptions = {}): string {
247
+ const lines: string[] = [];
248
+ for (const row of grid) {
249
+ const isSeparator = row[0]?.separator === true;
250
+ const rendered = renderGridRow(row, opts);
251
+ if (rendered === '' && !isSeparator) {
252
+ // Completely empty non-separator row — skip in standalone output.
253
+ continue;
254
+ }
255
+ lines.push(rendered);
256
+ }
257
+ return lines.join('\n');
258
+ }
@@ -173,10 +173,19 @@ function compareNodesTipsFirst(a: string, b: string, rank: ReadonlyMap<string, n
173
173
  * at the same rank — stable across edge-insertion order and correct under
174
174
  * diamonds, cross-links, and rollbacks.
175
175
  */
176
+ function maxRank(rank: ReadonlyMap<string, number>): number {
177
+ let max = 0;
178
+ for (const value of rank.values()) {
179
+ if (value > max) max = value;
180
+ }
181
+ return max;
182
+ }
183
+
176
184
  function layerNodesByLongestForwardPath(
177
185
  componentNodes: ReadonlySet<string>,
178
186
  topology: MigrationListGraphTopology,
179
187
  graph: MigrationGraph,
188
+ contractHash: string | undefined,
180
189
  ): readonly string[] {
181
190
  const forwardOut = new Map<string, string[]>();
182
191
 
@@ -224,6 +233,15 @@ function layerNodesByLongestForwardPath(
224
233
  }
225
234
  }
226
235
 
236
+ if (
237
+ contractHash !== undefined &&
238
+ contractHash !== EMPTY_CONTRACT_HASH &&
239
+ componentNodes.has(contractHash) &&
240
+ (forwardOut.get(contractHash) ?? []).length === 0
241
+ ) {
242
+ rank.set(contractHash, maxRank(rank) + 1);
243
+ }
244
+
227
245
  return [...componentNodes].sort((a, b) => compareNodesTipsFirst(a, b, rank));
228
246
  }
229
247
 
@@ -262,6 +280,99 @@ function detachedContractHash(
262
280
  : undefined;
263
281
  }
264
282
 
283
+ function isForwardLeaf(node: string, edges: readonly ClassifiedEdge[]): boolean {
284
+ return !edges.some((e) => e.kind === 'forward' && e.from === node && e.from !== e.to);
285
+ }
286
+
287
+ function forwardReachableFrom(
288
+ start: string,
289
+ forwardTo: ReadonlyMap<string, readonly string[]>,
290
+ ): ReadonlySet<string> {
291
+ const reachable = new Set<string>([start]);
292
+ const queue = [start];
293
+ while (queue.length > 0) {
294
+ const node = queue.shift();
295
+ if (node === undefined) continue;
296
+ for (const next of forwardTo.get(node) ?? []) {
297
+ if (!reachable.has(next)) {
298
+ reachable.add(next);
299
+ queue.push(next);
300
+ }
301
+ }
302
+ }
303
+ return reachable;
304
+ }
305
+
306
+ function buildForwardToMap(edges: readonly ClassifiedEdge[]): Map<string, string[]> {
307
+ const forwardTo = new Map<string, string[]>();
308
+ for (const edge of edges) {
309
+ if (edge.kind !== 'forward' || edge.from === edge.to) continue;
310
+ const bucket = forwardTo.get(edge.from);
311
+ if (bucket) bucket.push(edge.to);
312
+ else forwardTo.set(edge.from, [edge.to]);
313
+ }
314
+ return forwardTo;
315
+ }
316
+
317
+ function sortEdgesForContractHashTrunk(
318
+ edges: ClassifiedEdge[],
319
+ contractHash: string | undefined,
320
+ ): ClassifiedEdge[] {
321
+ if (
322
+ contractHash === undefined ||
323
+ contractHash === EMPTY_CONTRACT_HASH ||
324
+ !isForwardLeaf(contractHash, edges)
325
+ ) {
326
+ return edges;
327
+ }
328
+
329
+ const preferredLeaf = contractHash;
330
+ const forwardTo = buildForwardToMap(edges);
331
+ const reachability = new Map<string, ReadonlySet<string>>();
332
+ function canReachContractHash(from: string): boolean {
333
+ let cached = reachability.get(from);
334
+ if (cached === undefined) {
335
+ cached = forwardReachableFrom(from, forwardTo);
336
+ reachability.set(from, cached);
337
+ }
338
+ return cached.has(preferredLeaf);
339
+ }
340
+
341
+ function trunkBias(edge: ClassifiedEdge): number {
342
+ if (edge.kind !== 'forward' || edge.from === edge.to) return 0;
343
+ if (edge.to === preferredLeaf) return 2;
344
+ if (canReachContractHash(edge.to)) return 1;
345
+ return 0;
346
+ }
347
+
348
+ return edges
349
+ .map((edge, index) => ({ edge, index, bias: trunkBias(edge) }))
350
+ .sort((a, b) => {
351
+ if (a.edge.from !== b.edge.from) return a.index - b.index;
352
+ if (a.bias !== b.bias) return b.bias - a.bias;
353
+ return a.index - b.index;
354
+ })
355
+ .map(({ edge }) => edge);
356
+ }
357
+
358
+ function rebuildEdgeLookupMaps(edges: readonly ClassifiedEdge[]): {
359
+ edgesByFrom: Map<string, ClassifiedEdge[]>;
360
+ edgesByTo: Map<string, ClassifiedEdge[]>;
361
+ } {
362
+ const edgesByFrom = new Map<string, ClassifiedEdge[]>();
363
+ const edgesByTo = new Map<string, ClassifiedEdge[]>();
364
+ for (const classified of edges) {
365
+ const fromBucket = edgesByFrom.get(classified.from);
366
+ if (fromBucket) fromBucket.push(classified);
367
+ else edgesByFrom.set(classified.from, [classified]);
368
+
369
+ const toBucket = edgesByTo.get(classified.to);
370
+ if (toBucket) toBucket.push(classified);
371
+ else edgesByTo.set(classified.to, [classified]);
372
+ }
373
+ return { edgesByFrom, edgesByTo };
374
+ }
375
+
265
376
  export function buildMigrationGraphRows(
266
377
  graph: MigrationGraph,
267
378
  options: BuildMigrationGraphRowsOptions = {},
@@ -284,31 +395,23 @@ export function buildMigrationGraphRows(
284
395
 
285
396
  // 2. Build classified edge list
286
397
  const edges: ClassifiedEdge[] = [];
287
- const edgesByFrom = new Map<string, ClassifiedEdge[]>();
288
- const edgesByTo = new Map<string, ClassifiedEdge[]>();
289
398
 
290
399
  for (const edgeList of graph.forwardChain.values()) {
291
400
  for (const edge of edgeList) {
292
401
  const kind = topology.kindByMigrationHash.get(edge.migrationHash) ?? 'forward';
293
- const classified: ClassifiedEdge = {
402
+ edges.push({
294
403
  migrationHash: edge.migrationHash,
295
404
  from: edge.from,
296
405
  to: edge.to,
297
406
  dirName: edge.dirName,
298
407
  kind,
299
- };
300
- edges.push(classified);
301
-
302
- const fromBucket = edgesByFrom.get(edge.from);
303
- if (fromBucket) fromBucket.push(classified);
304
- else edgesByFrom.set(edge.from, [classified]);
305
-
306
- const toBucket = edgesByTo.get(edge.to);
307
- if (toBucket) toBucket.push(classified);
308
- else edgesByTo.set(edge.to, [classified]);
408
+ });
309
409
  }
310
410
  }
311
411
 
412
+ const sortedEdges = sortEdgesForContractHashTrunk(edges, options.contractHash);
413
+ const { edgesByFrom, edgesByTo } = rebuildEdgeLookupMaps(sortedEdges);
414
+
312
415
  // 3. Find weakly-connected components (ordered: EMPTY first, then lex)
313
416
  const components = weaklyConnectedComponents(graph);
314
417
 
@@ -318,7 +421,12 @@ export function buildMigrationGraphRows(
318
421
  if (i > 0) nodes.push(null);
319
422
  const component = components[i];
320
423
  if (component === undefined) continue;
321
- const ordered = layerNodesByLongestForwardPath(component, topology, graph);
424
+ const ordered = layerNodesByLongestForwardPath(
425
+ component,
426
+ topology,
427
+ graph,
428
+ options.contractHash,
429
+ );
322
430
  for (const node of ordered) {
323
431
  nodes.push(node);
324
432
  }
@@ -332,5 +440,10 @@ export function buildMigrationGraphRows(
332
440
  nodes.unshift(detached);
333
441
  }
334
442
 
335
- return { nodes, edges, edgesByFrom, edgesByTo };
443
+ return {
444
+ nodes,
445
+ edges: sortedEdges,
446
+ edgesByFrom,
447
+ edgesByTo,
448
+ };
336
449
  }
@@ -0,0 +1,188 @@
1
+ import type { MigrationGraph } from '@prisma-next/migration-tools/graph';
2
+ import type { GlyphMode } from '../glyph-mode';
3
+ import {
4
+ computeLabelColumn,
5
+ computeMaxDirNameWidth,
6
+ renderMigrationGraphCommand,
7
+ } from './migration-graph-command-render';
8
+ import { buildGrid } from './migration-graph-grid-layout';
9
+ import type { MigrationEdgeAnnotation } from './migration-graph-labels';
10
+ import type { Highlight } from './migration-graph-model';
11
+ import { buildMigrationGraphRows } from './migration-graph-rows';
12
+ import {
13
+ buildEdgeAnnotationsByHashFromListEntries,
14
+ buildRefsByHashFromListEntries,
15
+ type MigrationListStyler,
16
+ } from './migration-list-render';
17
+ import type { MigrationListEntry } from './migration-list-types';
18
+
19
+ export { buildEdgeAnnotationsByHashFromListEntries } from './migration-list-render';
20
+
21
+ export function mergeMigrationEdgeAnnotations(
22
+ listOverlay: ReadonlyMap<string, MigrationEdgeAnnotation>,
23
+ statusOverlay: ReadonlyMap<string, MigrationEdgeAnnotation>,
24
+ ): ReadonlyMap<string, MigrationEdgeAnnotation> {
25
+ const merged = new Map<string, MigrationEdgeAnnotation>();
26
+ for (const [migrationHash, listAnnotation] of listOverlay) {
27
+ const statusAnnotation = statusOverlay.get(migrationHash);
28
+ merged.set(migrationHash, {
29
+ ...listAnnotation,
30
+ ...(statusAnnotation?.status !== undefined ? { status: statusAnnotation.status } : {}),
31
+ });
32
+ }
33
+ return merged;
34
+ }
35
+
36
+ /**
37
+ * Translate `migrate --show` per-edge path-highlight annotations into a
38
+ * {@link Highlight}. With any `pathHighlight` present the result is focus mode
39
+ * (on-path lifted green, off-path dim); otherwise flat (lane-rotation colour).
40
+ */
41
+ export function highlightFromEdgeAnnotations(
42
+ edgeAnnotationsByHash: ReadonlyMap<string, MigrationEdgeAnnotation>,
43
+ ): Highlight {
44
+ const onPath = new Set<string>();
45
+ let anyPathHighlight = false;
46
+ for (const [migrationHash, annotation] of edgeAnnotationsByHash) {
47
+ if (annotation.pathHighlight === undefined) continue;
48
+ anyPathHighlight = true;
49
+ if (annotation.pathHighlight === 'on-path') onPath.add(migrationHash);
50
+ }
51
+ return anyPathHighlight ? { mode: 'focus', onPath } : { mode: 'flat', onPath: new Set() };
52
+ }
53
+
54
+ export interface RenderMigrationGraphSpaceTreeInput {
55
+ readonly graph: MigrationGraph;
56
+ readonly migrations: readonly MigrationListEntry[];
57
+ readonly liveContractHash: string;
58
+ readonly glyphMode: GlyphMode;
59
+ readonly colorize: boolean;
60
+ readonly refsByHash?: ReadonlyMap<string, readonly string[]>;
61
+ readonly statusOverlayByHash?: ReadonlyMap<string, MigrationEdgeAnnotation>;
62
+ readonly dbHash?: string;
63
+ readonly styler?: MigrationListStyler;
64
+ /**
65
+ * Cross-space override for the gutter→label column (the widest gutter across
66
+ * sibling space sections, plus the label gap). Named for historical
67
+ * continuity with the previous renderer's prefix-width input.
68
+ */
69
+ readonly globalMaxEdgeTreePrefixWidth?: number;
70
+ readonly globalMaxDirNameWidth?: number;
71
+ /**
72
+ * Whether this render is for the app space. When false, `contractHash` is not
73
+ * forwarded to `buildMigrationGraphRows` (suppressing the floating working-
74
+ * contract node) and the `@contract` marker is suppressed. Defaults to `true`.
75
+ */
76
+ readonly isAppSpace?: boolean;
77
+ }
78
+
79
+ export interface ComputeGlobalMaxEdgeTreePrefixWidthInput {
80
+ readonly graph: MigrationGraph;
81
+ readonly liveContractHash: string;
82
+ }
83
+
84
+ function buildGridForInput(input: ComputeGlobalMaxEdgeTreePrefixWidthInput): {
85
+ readonly grid: ReturnType<typeof buildGrid>;
86
+ readonly rowModel: ReturnType<typeof buildMigrationGraphRows>;
87
+ } {
88
+ const rowModel = buildMigrationGraphRows(input.graph, { contractHash: input.liveContractHash });
89
+ const grid = buildGrid(rowModel, {}, { mode: 'flat', onPath: new Set() });
90
+ return { grid, rowModel };
91
+ }
92
+
93
+ /**
94
+ * The widest gutter→label column across the given space layouts. Cross-space
95
+ * callers pass this back in so every section's labels share one column.
96
+ */
97
+ export function computeGlobalMaxEdgeTreePrefixWidth(
98
+ inputs: readonly ComputeGlobalMaxEdgeTreePrefixWidthInput[],
99
+ glyphMode: GlyphMode = 'unicode',
100
+ ): number {
101
+ let globalMax = 0;
102
+ for (const input of inputs) {
103
+ const { grid } = buildGridForInput(input);
104
+ globalMax = Math.max(globalMax, computeLabelColumn(grid, glyphMode));
105
+ }
106
+ return globalMax;
107
+ }
108
+
109
+ export function computeGlobalMaxDirNameWidth(
110
+ inputs: readonly ComputeGlobalMaxEdgeTreePrefixWidthInput[],
111
+ ): number {
112
+ let globalMax = 0;
113
+ for (const input of inputs) {
114
+ const { rowModel } = buildGridForInput(input);
115
+ globalMax = Math.max(globalMax, computeMaxDirNameWidth(rowModel));
116
+ }
117
+ return globalMax;
118
+ }
119
+
120
+ function renderMigrationGraphSpaceTreeInternal(input: RenderMigrationGraphSpaceTreeInput): string {
121
+ const appSpace = input.isAppSpace !== false;
122
+ const rowModel = buildMigrationGraphRows(input.graph, {
123
+ ...(appSpace ? { contractHash: input.liveContractHash } : {}),
124
+ });
125
+ const listOverlay = buildEdgeAnnotationsByHashFromListEntries(input.migrations);
126
+ const edgeAnnotationsByHash =
127
+ input.statusOverlayByHash === undefined
128
+ ? listOverlay
129
+ : mergeMigrationEdgeAnnotations(listOverlay, input.statusOverlayByHash);
130
+ const highlight = highlightFromEdgeAnnotations(edgeAnnotationsByHash);
131
+ const grid = buildGrid(rowModel, {}, highlight);
132
+
133
+ return renderMigrationGraphCommand({
134
+ grid,
135
+ rowModel,
136
+ colorize: input.colorize,
137
+ glyphMode: input.glyphMode,
138
+ contractHash: input.liveContractHash,
139
+ isAppSpace: appSpace,
140
+ edgeAnnotationsByHash,
141
+ refsByHash: input.refsByHash ?? buildRefsByHashFromListEntries(input.migrations),
142
+ ...(input.dbHash !== undefined ? { dbHash: input.dbHash } : {}),
143
+ ...(input.styler !== undefined ? { styler: input.styler } : {}),
144
+ ...(input.globalMaxEdgeTreePrefixWidth !== undefined
145
+ ? { globalLabelColumn: input.globalMaxEdgeTreePrefixWidth }
146
+ : {}),
147
+ ...(input.globalMaxDirNameWidth !== undefined
148
+ ? { globalMaxDirNameWidth: input.globalMaxDirNameWidth }
149
+ : {}),
150
+ });
151
+ }
152
+
153
+ export function renderMigrationGraphSpaceTree(input: RenderMigrationGraphSpaceTreeInput): string {
154
+ return renderMigrationGraphSpaceTreeInternal(input);
155
+ }
156
+
157
+ export function renderMigrationGraphSpaceTrees(
158
+ inputs: readonly RenderMigrationGraphSpaceTreeInput[],
159
+ ): readonly string[] {
160
+ const globalInputs: ComputeGlobalMaxEdgeTreePrefixWidthInput[] = inputs.map((input) => ({
161
+ graph: input.graph,
162
+ liveContractHash: input.liveContractHash,
163
+ }));
164
+ const glyphMode = inputs[0]?.glyphMode ?? 'unicode';
165
+ const globalLabelColumn =
166
+ inputs.length > 1 ? computeGlobalMaxEdgeTreePrefixWidth(globalInputs, glyphMode) : undefined;
167
+ const globalMaxDirName =
168
+ inputs.length > 1 ? computeGlobalMaxDirNameWidth(globalInputs) : undefined;
169
+ return inputs.map((input) =>
170
+ renderMigrationGraphSpaceTreeInternal({
171
+ ...input,
172
+ ...(globalLabelColumn !== undefined
173
+ ? { globalMaxEdgeTreePrefixWidth: globalLabelColumn }
174
+ : {}),
175
+ ...(globalMaxDirName !== undefined ? { globalMaxDirNameWidth: globalMaxDirName } : {}),
176
+ }),
177
+ );
178
+ }
179
+
180
+ export function indentMigrationGraphTreeBlock(treeOutput: string, indent: string): string {
181
+ if (treeOutput.length === 0) {
182
+ return treeOutput;
183
+ }
184
+ return treeOutput
185
+ .split('\n')
186
+ .map((line) => (line.length === 0 ? line : `${indent}${line}`))
187
+ .join('\n');
188
+ }