@prisma-next/cli 0.12.0-dev.5 → 0.12.0-dev.51

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 (174) 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-DC-UlBLy.mjs} +83 -58
  5. package/dist/client-DC-UlBLy.mjs.map +1 -0
  6. package/dist/{command-helpers-Bbw1GbwL.mjs → command-helpers-esJGBD4W.mjs} +317 -23
  7. package/dist/command-helpers-esJGBD4W.mjs.map +1 -0
  8. package/dist/commands/contract-emit.mjs +1 -1
  9. package/dist/commands/contract-infer.mjs +1 -1
  10. package/dist/commands/db-init.mjs +4 -5
  11. package/dist/commands/db-init.mjs.map +1 -1
  12. package/dist/commands/db-schema.mjs +3 -3
  13. package/dist/commands/db-sign.mjs +4 -4
  14. package/dist/commands/db-update.d.mts.map +1 -1
  15. package/dist/commands/db-update.mjs +10 -7
  16. package/dist/commands/db-update.mjs.map +1 -1
  17. package/dist/commands/db-verify.mjs +1 -1
  18. package/dist/commands/migrate.d.mts +2 -2
  19. package/dist/commands/migrate.d.mts.map +1 -1
  20. package/dist/commands/migrate.mjs +6 -8
  21. package/dist/commands/migrate.mjs.map +1 -1
  22. package/dist/commands/migration-check.d.mts +55 -13
  23. package/dist/commands/migration-check.d.mts.map +1 -1
  24. package/dist/commands/migration-check.mjs +3 -2
  25. package/dist/commands/migration-graph.d.mts +17 -8
  26. package/dist/commands/migration-graph.d.mts.map +1 -1
  27. package/dist/commands/migration-graph.mjs +183 -2
  28. package/dist/commands/migration-graph.mjs.map +1 -0
  29. package/dist/commands/migration-list.d.mts +25 -27
  30. package/dist/commands/migration-list.d.mts.map +1 -1
  31. package/dist/commands/migration-list.mjs +2 -190
  32. package/dist/commands/migration-log.d.mts +9 -19
  33. package/dist/commands/migration-log.d.mts.map +1 -1
  34. package/dist/commands/migration-log.mjs +1 -137
  35. package/dist/commands/migration-new.mjs +3 -3
  36. package/dist/commands/migration-plan.d.mts +1 -1
  37. package/dist/commands/migration-plan.mjs +1 -1
  38. package/dist/commands/migration-show.d.mts +17 -21
  39. package/dist/commands/migration-show.d.mts.map +1 -1
  40. package/dist/commands/migration-show.mjs +23 -35
  41. package/dist/commands/migration-show.mjs.map +1 -1
  42. package/dist/commands/migration-status.d.mts +42 -144
  43. package/dist/commands/migration-status.d.mts.map +1 -1
  44. package/dist/commands/migration-status.mjs +3 -759
  45. package/dist/commands/ref.d.mts +1 -1
  46. package/dist/commands/ref.mjs +3 -3
  47. package/dist/commands/telemetry/index.d.mts +7 -0
  48. package/dist/commands/telemetry/index.d.mts.map +1 -0
  49. package/dist/commands/telemetry/index.mjs +2 -0
  50. package/dist/{contract-at-errors-BxP-TOMl.mjs → contract-at-errors-COZAemUl.mjs} +2 -2
  51. package/dist/{contract-at-errors-BxP-TOMl.mjs.map → contract-at-errors-COZAemUl.mjs.map} +1 -1
  52. package/dist/{contract-emit-DxcGl4Uq.mjs → contract-emit-Bv46RAIO.mjs} +3 -3
  53. package/dist/{contract-emit-DxcGl4Uq.mjs.map → contract-emit-Bv46RAIO.mjs.map} +1 -1
  54. package/dist/{contract-emit-D-4jrNve.mjs → contract-emit-DIWImLqS.mjs} +5 -5
  55. package/dist/{contract-emit-D-4jrNve.mjs.map → contract-emit-DIWImLqS.mjs.map} +1 -1
  56. package/dist/{contract-infer-D8uEbJuu.mjs → contract-infer-DpGN9SAj.mjs} +3 -3
  57. package/dist/{contract-infer-D8uEbJuu.mjs.map → contract-infer-DpGN9SAj.mjs.map} +1 -1
  58. package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs → contract-space-aggregate-loader-CpNVrBqW.mjs} +63 -5
  59. package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs.map → contract-space-aggregate-loader-CpNVrBqW.mjs.map} +1 -1
  60. package/dist/{db-verify-v_vUKXTU.mjs → db-verify-Cq16Obsw.mjs} +4 -4
  61. package/dist/{db-verify-v_vUKXTU.mjs.map → db-verify-Cq16Obsw.mjs.map} +1 -1
  62. package/dist/exports/control-api.d.mts +2 -2
  63. package/dist/exports/control-api.d.mts.map +1 -1
  64. package/dist/exports/control-api.mjs +2 -2
  65. package/dist/exports/index.mjs +1 -1
  66. package/dist/exports/init-output.mjs +1 -1
  67. package/dist/{framework-components-fYXjz_in.mjs → framework-components-BO9VO43s.mjs} +2 -2
  68. package/dist/{framework-components-fYXjz_in.mjs.map → framework-components-BO9VO43s.mjs.map} +1 -1
  69. package/dist/{global-flags-DEHjV8_s.d.mts → global-flags-CV5LhrFg.d.mts} +1 -1
  70. package/dist/{global-flags-DEHjV8_s.d.mts.map → global-flags-CV5LhrFg.d.mts.map} +1 -1
  71. package/dist/{init-Cv9UzWL5.mjs → init-C0rjiQ9I.mjs} +5 -58
  72. package/dist/init-C0rjiQ9I.mjs.map +1 -0
  73. package/dist/{inspect-live-schema-C6ohV_oQ.mjs → inspect-live-schema-CRDKTNcf.mjs} +3 -3
  74. package/dist/{inspect-live-schema-C6ohV_oQ.mjs.map → inspect-live-schema-CRDKTNcf.mjs.map} +1 -1
  75. package/dist/migration-check-BxWlQBOs.mjs +573 -0
  76. package/dist/migration-check-BxWlQBOs.mjs.map +1 -0
  77. package/dist/{migration-command-scaffold-CjvwO6at.mjs → migration-command-scaffold-BDd9abqW.mjs} +3 -3
  78. package/dist/{migration-command-scaffold-CjvwO6at.mjs.map → migration-command-scaffold-BDd9abqW.mjs.map} +1 -1
  79. package/dist/migration-graph-space-render-CeNXh_Wy.mjs +1966 -0
  80. package/dist/migration-graph-space-render-CeNXh_Wy.mjs.map +1 -0
  81. package/dist/migration-list-vJWFuXca.mjs +228 -0
  82. package/dist/migration-list-vJWFuXca.mjs.map +1 -0
  83. package/dist/migration-log-6rcHQSI4.mjs +222 -0
  84. package/dist/migration-log-6rcHQSI4.mjs.map +1 -0
  85. package/dist/migration-path-target-UkxkgXnv.mjs +38 -0
  86. package/dist/migration-path-target-UkxkgXnv.mjs.map +1 -0
  87. package/dist/{migration-plan-9DJ7q7_z.mjs → migration-plan-CHu_erQ5.mjs} +5 -6
  88. package/dist/{migration-plan-9DJ7q7_z.mjs.map → migration-plan-CHu_erQ5.mjs.map} +1 -1
  89. package/dist/migration-status-Bjv91dE7.mjs +444 -0
  90. package/dist/migration-status-Bjv91dE7.mjs.map +1 -0
  91. package/dist/{output-B60Gw5fu.mjs → output-BD61elic.mjs} +1 -1
  92. package/dist/{output-B60Gw5fu.mjs.map → output-BD61elic.mjs.map} +1 -1
  93. package/dist/{ref-advancement-DUZqsue6.mjs → ref-advancement-CJY9zOv7.mjs} +1 -1
  94. package/dist/{ref-advancement-DUZqsue6.mjs.map → ref-advancement-CJY9zOv7.mjs.map} +1 -1
  95. package/dist/schemas-BL33A3i-.d.mts +193 -0
  96. package/dist/schemas-BL33A3i-.d.mts.map +1 -0
  97. package/dist/schemas-DJY2O09F.mjs +112 -0
  98. package/dist/schemas-DJY2O09F.mjs.map +1 -0
  99. package/dist/telemetry-CZkgkR_O.mjs +122 -0
  100. package/dist/telemetry-CZkgkR_O.mjs.map +1 -0
  101. package/dist/{terminal-ui-5Y6mrg93.d.mts → terminal-ui-BgLiAOYi.d.mts} +1 -1
  102. package/dist/{terminal-ui-5Y6mrg93.d.mts.map → terminal-ui-BgLiAOYi.d.mts.map} +1 -1
  103. package/dist/{types-Dt_SfqFm.d.mts → types-qV41eEXH.d.mts} +44 -31
  104. package/dist/types-qV41eEXH.d.mts.map +1 -0
  105. package/dist/{verify-DCA9Sldu.mjs → verify-IilvIk_E.mjs} +2 -2
  106. package/dist/{verify-DCA9Sldu.mjs.map → verify-IilvIk_E.mjs.map} +1 -1
  107. package/package.json +22 -19
  108. package/src/cli.ts +5 -0
  109. package/src/commands/db-update.ts +7 -1
  110. package/src/commands/init/index.ts +6 -35
  111. package/src/commands/init/init.ts +1 -14
  112. package/src/commands/init/inputs.ts +0 -75
  113. package/src/commands/json/schemas.ts +195 -0
  114. package/src/commands/migrate.ts +6 -6
  115. package/src/commands/migration-check.ts +469 -134
  116. package/src/commands/migration-graph.ts +162 -91
  117. package/src/commands/migration-list.ts +69 -39
  118. package/src/commands/migration-log.ts +52 -102
  119. package/src/commands/migration-show.ts +31 -66
  120. package/src/commands/migration-status-overlay.ts +61 -0
  121. package/src/commands/migration-status.ts +453 -1067
  122. package/src/commands/telemetry/index.ts +107 -0
  123. package/src/commands/telemetry/status.ts +67 -0
  124. package/src/control-api/client.ts +20 -9
  125. package/src/control-api/operations/contract-emit.ts +2 -2
  126. package/src/control-api/operations/db-init.ts +3 -3
  127. package/src/control-api/operations/{db-apply.ts → db-run.ts} +37 -10
  128. package/src/control-api/operations/db-update.ts +4 -4
  129. package/src/control-api/operations/{migration-apply.ts → migrate.ts} +32 -24
  130. package/src/control-api/operations/{apply.ts → run-migration.ts} +33 -27
  131. package/src/control-api/types.ts +46 -29
  132. package/src/utils/cli-errors.ts +70 -2
  133. package/src/utils/formatters/errors.ts +11 -0
  134. package/src/utils/formatters/migration-graph-lane-colors.ts +194 -0
  135. package/src/utils/formatters/migration-graph-layout.ts +51 -7
  136. package/src/utils/formatters/migration-graph-rows.ts +128 -15
  137. package/src/utils/formatters/migration-graph-space-render.ts +138 -0
  138. package/src/utils/formatters/migration-graph-tree-render.ts +405 -77
  139. package/src/utils/formatters/migration-list-data-column.ts +4 -91
  140. package/src/utils/formatters/migration-list-graph-topology.ts +72 -94
  141. package/src/utils/formatters/migration-list-render.ts +123 -71
  142. package/src/utils/formatters/migration-list-styler.ts +48 -5
  143. package/src/utils/formatters/migration-list-types.ts +5 -21
  144. package/src/utils/formatters/migration-log-table.ts +205 -0
  145. package/src/utils/formatters/migrations.ts +33 -11
  146. package/src/utils/global-flags.ts +35 -0
  147. package/src/utils/integrity-violation-to-check-failure.ts +28 -19
  148. package/src/utils/legend.ts +38 -0
  149. package/src/utils/migration-path-target.ts +60 -0
  150. package/src/utils/telemetry.ts +68 -32
  151. package/dist/client-KgJorIvG.mjs.map +0 -1
  152. package/dist/command-helpers-Bbw1GbwL.mjs.map +0 -1
  153. package/dist/commands/migration-list.mjs.map +0 -1
  154. package/dist/commands/migration-log.mjs.map +0 -1
  155. package/dist/commands/migration-status.mjs.map +0 -1
  156. package/dist/extension-pack-inputs-IDvjRCi3.mjs +0 -62
  157. package/dist/extension-pack-inputs-IDvjRCi3.mjs.map +0 -1
  158. package/dist/graph-render-rFAqZujX.mjs +0 -1081
  159. package/dist/graph-render-rFAqZujX.mjs.map +0 -1
  160. package/dist/init-Cv9UzWL5.mjs.map +0 -1
  161. package/dist/migration-check-BiBJoYYW.mjs +0 -341
  162. package/dist/migration-check-BiBJoYYW.mjs.map +0 -1
  163. package/dist/migration-graph-D7DVUElV.mjs +0 -1232
  164. package/dist/migration-graph-D7DVUElV.mjs.map +0 -1
  165. package/dist/migration-list-styler-BRwF4-gy.mjs +0 -399
  166. package/dist/migration-list-styler-BRwF4-gy.mjs.map +0 -1
  167. package/dist/migration-types-D2FW63pr.d.mts +0 -15
  168. package/dist/migration-types-D2FW63pr.d.mts.map +0 -1
  169. package/dist/migrations-Cv2jxNNK.mjs +0 -228
  170. package/dist/migrations-Cv2jxNNK.mjs.map +0 -1
  171. package/dist/types-Dt_SfqFm.d.mts.map +0 -1
  172. package/src/utils/formatters/graph-migration-mapper.ts +0 -235
  173. package/src/utils/formatters/graph-render.ts +0 -1323
  174. package/src/utils/formatters/graph-types.ts +0 -120
@@ -0,0 +1,194 @@
1
+ import { createColors } from 'colorette';
2
+ import type { StructuralCell } from './migration-graph-layout';
3
+
4
+ export type LaneColorizer = (text: string) => string;
5
+
6
+ const { magenta, cyan, green, yellow, blueBright, red } = createColors({ useColor: true });
7
+
8
+ export const LANE_COLOR_CYCLE: readonly LaneColorizer[] = [
9
+ magenta,
10
+ cyan,
11
+ green,
12
+ yellow,
13
+ blueBright,
14
+ red,
15
+ ];
16
+
17
+ /**
18
+ * The leftmost lane (column 0) renders neutral — no palette hue. Columns ≥ 1
19
+ * rotate through {@link LANE_COLOR_CYCLE}.
20
+ */
21
+ export const NEUTRAL_LANE_COLUMN = 0;
22
+
23
+ /**
24
+ * The hue for a gutter column. The leftmost lane (column 0) is **neutral** — it
25
+ * has nothing to be told apart from in the common single-lane linear case, so
26
+ * the renderer dims it rather than tinting it; the rotating palette is reserved
27
+ * for columns ≥ 1 (where a second lane exists to distinguish). Callers must dim
28
+ * column 0 themselves; this returns identity for it as a guard. A lane freed and
29
+ * reused by a later branch keeps its column's hue — coloring is by position, not
30
+ * branch identity, exactly like `git log --graph`.
31
+ */
32
+ export function laneColorForColumn(column: number): LaneColorizer {
33
+ if (column <= NEUTRAL_LANE_COLUMN) {
34
+ return (text) => text;
35
+ }
36
+ const colorizer = LANE_COLOR_CYCLE[(column - 1) % LANE_COLOR_CYCLE.length];
37
+ return colorizer ?? ((text) => text);
38
+ }
39
+
40
+ /**
41
+ * Style a structural glyph by its resolved colour column. Column 0 and the
42
+ * neutral sentinel render dim (`dimLane`); columns ≥ 1 take a palette hue.
43
+ */
44
+ export function stylerForLaneColumn(
45
+ colorColumn: number,
46
+ colorize: boolean,
47
+ dimLane: (text: string) => string,
48
+ ): LaneColorizer {
49
+ if (!colorize || colorColumn <= NEUTRAL_LANE_COLUMN) {
50
+ return dimLane;
51
+ }
52
+ return laneColorForColumn(colorColumn);
53
+ }
54
+
55
+ /**
56
+ * The colour-source column for each cell of a row, resolved together because a
57
+ * routed back-arc spans columns and must read as **one hue** rather than a
58
+ * per-column "rainbow".
59
+ */
60
+ export interface RowArcLaneColors {
61
+ /** Colour column for a cell's structural glyph (lane / spine / arc body). */
62
+ readonly lane: readonly number[];
63
+ /** Colour column for a node arc-pair's connector half (`◂` / `─`). */
64
+ readonly connector: readonly number[];
65
+ /**
66
+ * Colour column for the trailing `─` of a landing tee (`┴─`). The junction
67
+ * (`lane`) keeps its own column; the dash leads into the next converging arc.
68
+ */
69
+ readonly dash: readonly number[];
70
+ }
71
+
72
+ /**
73
+ * Resolve per-cell colour columns for a node/arc row. Scanning right-to-left
74
+ * lets each arc segment inherit the hue of the arc it leads into.
75
+ */
76
+ export function resolveRowArcLaneColors(cells: readonly StructuralCell[]): RowArcLaneColors {
77
+ const lane = new Array<number>(cells.length);
78
+ const connector = new Array<number>(cells.length);
79
+ const dash = new Array<number>(cells.length);
80
+ let arcCorner = NEUTRAL_LANE_COLUMN;
81
+ let landingAnchor = NEUTRAL_LANE_COLUMN;
82
+ for (let column = cells.length - 1; column >= 0; column--) {
83
+ const cell = cells[column];
84
+ connector[column] = landingAnchor !== NEUTRAL_LANE_COLUMN ? landingAnchor : arcCorner;
85
+ switch (cell?.kind) {
86
+ case 'arc-branch-corner':
87
+ arcCorner = column;
88
+ lane[column] = column;
89
+ dash[column] = column;
90
+ break;
91
+ case 'arc-land-corner':
92
+ arcCorner = column;
93
+ landingAnchor = column;
94
+ lane[column] = column;
95
+ dash[column] = column;
96
+ break;
97
+ case 'arc-branch-tee':
98
+ lane[column] = column;
99
+ dash[column] = column;
100
+ break;
101
+ case 'arc-land-tee':
102
+ lane[column] = column;
103
+ dash[column] = landingAnchor === NEUTRAL_LANE_COLUMN ? column : landingAnchor;
104
+ landingAnchor = column;
105
+ break;
106
+ case 'arc-crossing':
107
+ case 'arc-land-bridge': {
108
+ const served = landingAnchor !== NEUTRAL_LANE_COLUMN ? landingAnchor : arcCorner;
109
+ lane[column] = served;
110
+ dash[column] = served;
111
+ break;
112
+ }
113
+ case 'horizontal-pass':
114
+ lane[column] = arcCorner === NEUTRAL_LANE_COLUMN ? column : arcCorner;
115
+ dash[column] = lane[column] ?? column;
116
+ break;
117
+ case 'node':
118
+ lane[column] = column;
119
+ dash[column] = column;
120
+ arcCorner = NEUTRAL_LANE_COLUMN;
121
+ landingAnchor = NEUTRAL_LANE_COLUMN;
122
+ break;
123
+ default:
124
+ lane[column] = column;
125
+ dash[column] = column;
126
+ arcCorner = NEUTRAL_LANE_COLUMN;
127
+ landingAnchor = NEUTRAL_LANE_COLUMN;
128
+ }
129
+ }
130
+ return { lane, connector, dash };
131
+ }
132
+
133
+ /**
134
+ * Per-cell colour for a forward branch/merge connector row, split into the
135
+ * cell's junction `glyph` and its trailing `dash`.
136
+ */
137
+ export interface ConnectorLaneColors {
138
+ /** Colour column for a cell's junction glyph (`├` / `┬` / `┴` / `╮` / `╯`). */
139
+ readonly glyph: readonly number[];
140
+ /** Colour column for a tee's trailing `─` — the branch it leads into. */
141
+ readonly dash: readonly number[];
142
+ }
143
+
144
+ /**
145
+ * Resolve per-cell connector colours. Scanning right-to-left, a corner or an
146
+ * intermediate tee anchors its own lane, but a tee's trailing dash leads into
147
+ * the branch on its right.
148
+ */
149
+ export function resolveConnectorLaneColors(
150
+ cells: readonly StructuralCell[],
151
+ startLane: number,
152
+ ): ConnectorLaneColors {
153
+ const glyph = new Array<number>(cells.length);
154
+ const dash = new Array<number>(cells.length);
155
+ let owner = NEUTRAL_LANE_COLUMN;
156
+ for (let column = cells.length - 1; column >= 0; column--) {
157
+ const cell = cells[column];
158
+ switch (cell?.kind) {
159
+ case 'branch-corner':
160
+ case 'merge-corner':
161
+ owner = column;
162
+ glyph[column] = column;
163
+ dash[column] = column;
164
+ break;
165
+ case 'branch-tee':
166
+ case 'merge-tee':
167
+ if (column === startLane) {
168
+ const served = owner === NEUTRAL_LANE_COLUMN ? column : owner;
169
+ glyph[column] = column;
170
+ dash[column] = served;
171
+ } else {
172
+ dash[column] = owner === NEUTRAL_LANE_COLUMN ? column : owner;
173
+ glyph[column] = column;
174
+ owner = column;
175
+ }
176
+ break;
177
+ case 'arc-crossing':
178
+ glyph[column] = column;
179
+ dash[column] = owner === NEUTRAL_LANE_COLUMN ? column : owner;
180
+ owner = column;
181
+ break;
182
+ case 'horizontal-pass': {
183
+ const served = owner === NEUTRAL_LANE_COLUMN ? column : owner;
184
+ glyph[column] = served;
185
+ dash[column] = served;
186
+ break;
187
+ }
188
+ default:
189
+ glyph[column] = column;
190
+ dash[column] = column;
191
+ }
192
+ }
193
+ return { glyph, dash };
194
+ }
@@ -21,6 +21,7 @@ export type StructuralCell =
21
21
  | { readonly kind: 'arc-branch-corner' }
22
22
  | { readonly kind: 'arc-branch-tee' }
23
23
  | { readonly kind: 'arc-land-corner' }
24
+ | { readonly kind: 'arc-land-tee' }
24
25
  | { readonly kind: 'arc-crossing' }
25
26
  | { readonly kind: 'arc-land-bridge' }
26
27
  | {
@@ -379,6 +380,7 @@ function emptyCells(width: number): StructuralCell[] {
379
380
  function buildBranchConnectorCells(
380
381
  startLane: number,
381
382
  endLane: number,
383
+ fanTargetLanes: ReadonlySet<number>,
382
384
  activeLanes: ReadonlySet<number>,
383
385
  gridWidth: number,
384
386
  ): StructuralCell[] {
@@ -393,7 +395,13 @@ function buildBranchConnectorCells(
393
395
  } else if (lane === endLane) {
394
396
  cells[lane] = { kind: 'branch-corner' };
395
397
  } else if (lane > startLane && lane < endLane) {
396
- cells[lane] = { kind: 'branch-tee' };
398
+ if (fanTargetLanes.has(lane)) {
399
+ cells[lane] = { kind: 'branch-tee' };
400
+ } else if (activeLanes.has(lane)) {
401
+ cells[lane] = { kind: 'arc-crossing' };
402
+ } else {
403
+ cells[lane] = { kind: 'branch-tee' };
404
+ }
397
405
  }
398
406
  }
399
407
  return cells;
@@ -402,6 +410,7 @@ function buildBranchConnectorCells(
402
410
  function buildMergeConnectorCells(
403
411
  startLane: number,
404
412
  endLane: number,
413
+ fanTargetLanes: ReadonlySet<number>,
405
414
  activeLanes: ReadonlySet<number>,
406
415
  gridWidth: number,
407
416
  ): StructuralCell[] {
@@ -416,7 +425,13 @@ function buildMergeConnectorCells(
416
425
  } else if (lane === endLane) {
417
426
  cells[lane] = { kind: 'merge-corner' };
418
427
  } else if (lane > startLane && lane < endLane) {
419
- cells[lane] = activeLanes.has(lane) ? { kind: 'merge-tee' } : { kind: 'horizontal-pass' };
428
+ if (fanTargetLanes.has(lane)) {
429
+ cells[lane] = { kind: 'merge-tee' };
430
+ } else if (activeLanes.has(lane)) {
431
+ cells[lane] = { kind: 'arc-crossing' };
432
+ } else {
433
+ cells[lane] = { kind: 'horizontal-pass' };
434
+ }
420
435
  }
421
436
  }
422
437
  return cells;
@@ -689,6 +704,15 @@ function applySkipRollbackRouting(
689
704
  .map((other) => other.backLane);
690
705
  const maxCoSourcedLane = Math.max(...coSourcedLanes);
691
706
 
707
+ // Back-lanes of arcs that converge on this same target node. They share the
708
+ // node's landing row, so each inner lane reads as a `┴` junction (the outer
709
+ // arcs' horizontal bridge passes through it on the way to the node) and only
710
+ // the outermost closes the corner with `╯`.
711
+ const coLandingLanes = routes
712
+ .filter((other) => other.edge.to === edge.to)
713
+ .map((other) => other.backLane);
714
+ const maxCoLandingLane = Math.max(...coLandingLanes);
715
+
692
716
  const sourceRow = result[sourceRowIndex];
693
717
  if (sourceRow !== undefined) {
694
718
  const cells = sourceRow.cells;
@@ -750,6 +774,7 @@ function applySkipRollbackRouting(
750
774
  const existing = cells[backLane];
751
775
  if (
752
776
  existing?.kind !== 'arc-land-corner' &&
777
+ existing?.kind !== 'arc-land-tee' &&
753
778
  existing?.kind !== 'arc-land-bridge' &&
754
779
  existing?.kind !== 'arc-branch-corner' &&
755
780
  existing?.kind !== 'arc-branch-tee' &&
@@ -766,6 +791,12 @@ function applySkipRollbackRouting(
766
791
  const contractHash = targetRow.contractHash ?? EMPTY_CONTRACT_HASH;
767
792
  cells[targetCol] = { kind: 'node', contractHash, arcLand: true };
768
793
  for (let lane = targetCol + 1; lane < backLane; lane += 1) {
794
+ // An inner converging arc's own landing junction: the outer arcs' bridge
795
+ // passes through it (`┴`) while its own vertical run closes here.
796
+ if (coLandingLanes.includes(lane)) {
797
+ cells[lane] = { kind: 'arc-land-tee' };
798
+ continue;
799
+ }
769
800
  // A bridged lane that carries another arc OR a forward vertical still
770
801
  // active at this row must cross over it (`┼`) rather than overwrite it
771
802
  // with a bare bridge (`──`).
@@ -774,7 +805,8 @@ function applySkipRollbackRouting(
774
805
  existing !== undefined &&
775
806
  existing.kind !== 'empty' &&
776
807
  existing.kind !== 'horizontal-pass' &&
777
- existing.kind !== 'arc-land-bridge';
808
+ existing.kind !== 'arc-land-bridge' &&
809
+ existing.kind !== 'arc-land-tee';
778
810
  const crossed =
779
811
  occupied ||
780
812
  routes.some(
@@ -785,7 +817,10 @@ function applySkipRollbackRouting(
785
817
  );
786
818
  cells[lane] = crossed ? { kind: 'arc-crossing' } : { kind: 'arc-land-bridge' };
787
819
  }
788
- cells[backLane] = { kind: 'arc-land-corner' };
820
+ // Inner converging arcs close as a landing tee so the outermost arc's
821
+ // bridge reads through to the node; only the outermost arc draws `╯`.
822
+ cells[backLane] =
823
+ backLane < maxCoLandingLane ? { kind: 'arc-land-tee' } : { kind: 'arc-land-corner' };
789
824
  for (const other of routes) {
790
825
  if (other.backLane <= backLane) continue;
791
826
  if (!routeCrossesRow(other, targetRowIndex, result)) continue;
@@ -793,6 +828,7 @@ function applySkipRollbackRouting(
793
828
  const existing = cells[other.backLane];
794
829
  if (
795
830
  existing?.kind !== 'arc-land-corner' &&
831
+ existing?.kind !== 'arc-land-tee' &&
796
832
  existing?.kind !== 'arc-land-bridge' &&
797
833
  existing?.kind !== 'node'
798
834
  ) {
@@ -922,13 +958,14 @@ function layoutComponent(
922
958
  const endLane = Math.max(...laneIndices);
923
959
  ensureGridWidth(endLane + 1);
924
960
  const activeLanes = new Set(activeLaneIndices());
961
+ const fanTargetLanes = new Set(laneIndices);
925
962
  rows.push({
926
963
  kind: 'merge-connector',
927
964
  contractHash,
928
965
  startLane,
929
966
  endLane,
930
967
  branchCount: laneIndices.length,
931
- cells: buildMergeConnectorCells(startLane, endLane, activeLanes, gridWidth),
968
+ cells: buildMergeConnectorCells(startLane, endLane, fanTargetLanes, activeLanes, gridWidth),
932
969
  });
933
970
  for (const index of laneIndices) {
934
971
  if (index !== startLane) setLane(index, null);
@@ -941,6 +978,7 @@ function layoutComponent(
941
978
  startLane: number,
942
979
  endLane: number,
943
980
  branchCount: number,
981
+ fanTargetLanes: readonly number[],
944
982
  ): void {
945
983
  ensureGridWidth(endLane + 1);
946
984
  const activeLanes = new Set(activeLaneIndices());
@@ -950,7 +988,13 @@ function layoutComponent(
950
988
  startLane,
951
989
  endLane,
952
990
  branchCount,
953
- cells: buildBranchConnectorCells(startLane, endLane, activeLanes, gridWidth),
991
+ cells: buildBranchConnectorCells(
992
+ startLane,
993
+ endLane,
994
+ new Set(fanTargetLanes),
995
+ activeLanes,
996
+ gridWidth,
997
+ ),
954
998
  });
955
999
  }
956
1000
 
@@ -1046,7 +1090,7 @@ function layoutComponent(
1046
1090
 
1047
1091
  if (groups.length >= 2) {
1048
1092
  const endLane = Math.max(...laneForGroup);
1049
- emitBranchConnector(node, column, endLane, groups.length);
1093
+ emitBranchConnector(node, column, endLane, groups.length, laneForGroup);
1050
1094
  }
1051
1095
 
1052
1096
  for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
@@ -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,138 @@
1
+ import type { MigrationGraph } from '@prisma-next/migration-tools/graph';
2
+ import type { GlyphMode } from '../glyph-mode';
3
+ import { buildMigrationGraphLayout } from './migration-graph-layout';
4
+ import { buildMigrationGraphRows } from './migration-graph-rows';
5
+ import {
6
+ computeMaxDirNameLengthForLayout,
7
+ computeMaxEdgeTreePrefixWidthForLayout,
8
+ type MigrationEdgeAnnotation,
9
+ renderMigrationGraphTree,
10
+ } from './migration-graph-tree-render';
11
+ import {
12
+ buildEdgeAnnotationsByHashFromListEntries,
13
+ buildRefsByHashFromListEntries,
14
+ type MigrationListStyler,
15
+ } from './migration-list-render';
16
+ import type { MigrationListEntry } from './migration-list-types';
17
+
18
+ export { buildEdgeAnnotationsByHashFromListEntries } from './migration-list-render';
19
+
20
+ export function mergeMigrationEdgeAnnotations(
21
+ listOverlay: ReadonlyMap<string, MigrationEdgeAnnotation>,
22
+ statusOverlay: ReadonlyMap<string, MigrationEdgeAnnotation>,
23
+ ): ReadonlyMap<string, MigrationEdgeAnnotation> {
24
+ const merged = new Map<string, MigrationEdgeAnnotation>();
25
+ for (const [migrationHash, listAnnotation] of listOverlay) {
26
+ const statusAnnotation = statusOverlay.get(migrationHash);
27
+ merged.set(migrationHash, {
28
+ ...listAnnotation,
29
+ ...(statusAnnotation?.status !== undefined ? { status: statusAnnotation.status } : {}),
30
+ });
31
+ }
32
+ return merged;
33
+ }
34
+
35
+ export interface RenderMigrationGraphSpaceTreeInput {
36
+ readonly graph: MigrationGraph;
37
+ readonly migrations: readonly MigrationListEntry[];
38
+ readonly liveContractHash: string;
39
+ readonly glyphMode: GlyphMode;
40
+ readonly colorize: boolean;
41
+ readonly refsByHash?: ReadonlyMap<string, readonly string[]>;
42
+ readonly statusOverlayByHash?: ReadonlyMap<string, MigrationEdgeAnnotation>;
43
+ readonly dbHash?: string;
44
+ readonly styler?: MigrationListStyler;
45
+ readonly globalMaxEdgeTreePrefixWidth?: number;
46
+ readonly globalMaxDirNameWidth?: number;
47
+ }
48
+
49
+ export interface ComputeGlobalMaxEdgeTreePrefixWidthInput {
50
+ readonly graph: MigrationGraph;
51
+ readonly liveContractHash: string;
52
+ }
53
+
54
+ export function computeGlobalMaxEdgeTreePrefixWidth(
55
+ inputs: readonly ComputeGlobalMaxEdgeTreePrefixWidthInput[],
56
+ ): number {
57
+ let globalMax = 0;
58
+ for (const input of inputs) {
59
+ const rowModel = buildMigrationGraphRows(input.graph, {
60
+ contractHash: input.liveContractHash,
61
+ });
62
+ const layout = buildMigrationGraphLayout(rowModel);
63
+ globalMax = Math.max(globalMax, computeMaxEdgeTreePrefixWidthForLayout(layout));
64
+ }
65
+ return globalMax;
66
+ }
67
+
68
+ export function computeGlobalMaxDirNameWidth(
69
+ inputs: readonly ComputeGlobalMaxEdgeTreePrefixWidthInput[],
70
+ ): number {
71
+ let globalMax = 0;
72
+ for (const input of inputs) {
73
+ const rowModel = buildMigrationGraphRows(input.graph, {
74
+ contractHash: input.liveContractHash,
75
+ });
76
+ const layout = buildMigrationGraphLayout(rowModel);
77
+ globalMax = Math.max(globalMax, computeMaxDirNameLengthForLayout(layout));
78
+ }
79
+ return globalMax;
80
+ }
81
+
82
+ function renderMigrationGraphSpaceTreeInternal(input: RenderMigrationGraphSpaceTreeInput): string {
83
+ const rowModel = buildMigrationGraphRows(input.graph, {
84
+ contractHash: input.liveContractHash,
85
+ });
86
+ const layout = buildMigrationGraphLayout(rowModel);
87
+ const listOverlay = buildEdgeAnnotationsByHashFromListEntries(input.migrations);
88
+ const edgeAnnotationsByHash =
89
+ input.statusOverlayByHash === undefined
90
+ ? listOverlay
91
+ : mergeMigrationEdgeAnnotations(listOverlay, input.statusOverlayByHash);
92
+ return renderMigrationGraphTree(layout, {
93
+ refsByHash: input.refsByHash ?? buildRefsByHashFromListEntries(input.migrations),
94
+ contractHash: input.liveContractHash,
95
+ edgeAnnotationsByHash,
96
+ colorize: input.colorize,
97
+ glyphMode: input.glyphMode,
98
+ ...(input.dbHash !== undefined ? { dbHash: input.dbHash } : {}),
99
+ ...(input.styler !== undefined ? { styler: input.styler } : {}),
100
+ ...(input.globalMaxEdgeTreePrefixWidth !== undefined
101
+ ? { globalMaxEdgeTreePrefixWidth: input.globalMaxEdgeTreePrefixWidth }
102
+ : {}),
103
+ ...(input.globalMaxDirNameWidth !== undefined
104
+ ? { globalMaxDirNameWidth: input.globalMaxDirNameWidth }
105
+ : {}),
106
+ });
107
+ }
108
+
109
+ export function renderMigrationGraphSpaceTree(input: RenderMigrationGraphSpaceTreeInput): string {
110
+ return renderMigrationGraphSpaceTreeInternal(input);
111
+ }
112
+
113
+ export function renderMigrationGraphSpaceTrees(
114
+ inputs: readonly RenderMigrationGraphSpaceTreeInput[],
115
+ ): readonly string[] {
116
+ const globalMaxTreePrefix =
117
+ inputs.length > 1 ? computeGlobalMaxEdgeTreePrefixWidth(inputs) : undefined;
118
+ const globalMaxDirName = inputs.length > 1 ? computeGlobalMaxDirNameWidth(inputs) : undefined;
119
+ return inputs.map((input) =>
120
+ renderMigrationGraphSpaceTreeInternal({
121
+ ...input,
122
+ ...(globalMaxTreePrefix !== undefined
123
+ ? { globalMaxEdgeTreePrefixWidth: globalMaxTreePrefix }
124
+ : {}),
125
+ ...(globalMaxDirName !== undefined ? { globalMaxDirNameWidth: globalMaxDirName } : {}),
126
+ }),
127
+ );
128
+ }
129
+
130
+ export function indentMigrationGraphTreeBlock(treeOutput: string, indent: string): string {
131
+ if (treeOutput.length === 0) {
132
+ return treeOutput;
133
+ }
134
+ return treeOutput
135
+ .split('\n')
136
+ .map((line) => (line.length === 0 ? line : `${indent}${line}`))
137
+ .join('\n');
138
+ }