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

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 (108) hide show
  1. package/dist/cli.mjs +177 -160
  2. package/dist/cli.mjs.map +1 -1
  3. package/dist/{client-KgJorIvG.mjs → client-Cdxcme1x.mjs} +21 -8
  4. package/dist/client-Cdxcme1x.mjs.map +1 -0
  5. package/dist/{command-helpers-Bbw1GbwL.mjs → command-helpers-Cmdqyhz9.mjs} +32 -2
  6. package/dist/{command-helpers-Bbw1GbwL.mjs.map → command-helpers-Cmdqyhz9.mjs.map} +1 -1
  7. package/dist/commands/contract-emit.mjs +1 -1
  8. package/dist/commands/contract-infer.mjs +1 -1
  9. package/dist/commands/db-init.mjs +4 -4
  10. package/dist/commands/db-schema.mjs +3 -3
  11. package/dist/commands/db-sign.mjs +4 -4
  12. package/dist/commands/db-update.mjs +5 -5
  13. package/dist/commands/db-verify.mjs +1 -1
  14. package/dist/commands/migrate.d.mts +1 -1
  15. package/dist/commands/migrate.mjs +5 -5
  16. package/dist/commands/migration-check.mjs +1 -1
  17. package/dist/commands/migration-graph.d.mts +23 -5
  18. package/dist/commands/migration-graph.d.mts.map +1 -1
  19. package/dist/commands/migration-graph.mjs +2 -2
  20. package/dist/commands/migration-list.d.mts +3 -3
  21. package/dist/commands/migration-list.mjs +3 -3
  22. package/dist/commands/migration-log.d.mts +3 -3
  23. package/dist/commands/migration-log.mjs +3 -3
  24. package/dist/commands/migration-new.mjs +3 -3
  25. package/dist/commands/migration-plan.d.mts +1 -1
  26. package/dist/commands/migration-plan.mjs +1 -1
  27. package/dist/commands/migration-show.d.mts +1 -1
  28. package/dist/commands/migration-show.mjs +3 -3
  29. package/dist/commands/migration-status.d.mts +1 -1
  30. package/dist/commands/migration-status.mjs +4 -4
  31. package/dist/commands/migration-status.mjs.map +1 -1
  32. package/dist/commands/ref.d.mts +1 -1
  33. package/dist/commands/ref.mjs +2 -2
  34. package/dist/commands/telemetry/index.d.mts +7 -0
  35. package/dist/commands/telemetry/index.d.mts.map +1 -0
  36. package/dist/commands/telemetry/index.mjs +2 -0
  37. package/dist/{contract-at-errors-BxP-TOMl.mjs → contract-at-errors-Cz0z5PJi.mjs} +2 -2
  38. package/dist/{contract-at-errors-BxP-TOMl.mjs.map → contract-at-errors-Cz0z5PJi.mjs.map} +1 -1
  39. package/dist/{contract-emit-D-4jrNve.mjs → contract-emit-CC9jDOmu.mjs} +3 -3
  40. package/dist/{contract-emit-D-4jrNve.mjs.map → contract-emit-CC9jDOmu.mjs.map} +1 -1
  41. package/dist/{contract-emit-DxcGl4Uq.mjs → contract-emit-DPMij44i.mjs} +3 -3
  42. package/dist/{contract-emit-DxcGl4Uq.mjs.map → contract-emit-DPMij44i.mjs.map} +1 -1
  43. package/dist/{contract-infer-D8uEbJuu.mjs → contract-infer-DaFPNrZH.mjs} +3 -3
  44. package/dist/{contract-infer-D8uEbJuu.mjs.map → contract-infer-DaFPNrZH.mjs.map} +1 -1
  45. package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs → contract-space-aggregate-loader-CirAEsM8.mjs} +2 -2
  46. package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs.map → contract-space-aggregate-loader-CirAEsM8.mjs.map} +1 -1
  47. package/dist/{db-verify-v_vUKXTU.mjs → db-verify-BSA1a_W_.mjs} +4 -4
  48. package/dist/{db-verify-v_vUKXTU.mjs.map → db-verify-BSA1a_W_.mjs.map} +1 -1
  49. package/dist/exports/control-api.d.mts +1 -1
  50. package/dist/exports/control-api.d.mts.map +1 -1
  51. package/dist/exports/control-api.mjs +2 -2
  52. package/dist/exports/index.mjs +1 -1
  53. package/dist/exports/init-output.mjs +1 -1
  54. package/dist/{framework-components-fYXjz_in.mjs → framework-components-DynSvww4.mjs} +2 -2
  55. package/dist/{framework-components-fYXjz_in.mjs.map → framework-components-DynSvww4.mjs.map} +1 -1
  56. package/dist/{global-flags-DEHjV8_s.d.mts → global-flags-DG4uY5tV.d.mts} +1 -1
  57. package/dist/{global-flags-DEHjV8_s.d.mts.map → global-flags-DG4uY5tV.d.mts.map} +1 -1
  58. package/dist/{init-Cv9UzWL5.mjs → init-B6kKrmf7.mjs} +5 -58
  59. package/dist/init-B6kKrmf7.mjs.map +1 -0
  60. package/dist/{inspect-live-schema-C6ohV_oQ.mjs → inspect-live-schema-Dn56wDhG.mjs} +3 -3
  61. package/dist/{inspect-live-schema-C6ohV_oQ.mjs.map → inspect-live-schema-Dn56wDhG.mjs.map} +1 -1
  62. package/dist/{migration-check-BiBJoYYW.mjs → migration-check-DzH1u-O1.mjs} +2 -2
  63. package/dist/{migration-check-BiBJoYYW.mjs.map → migration-check-DzH1u-O1.mjs.map} +1 -1
  64. package/dist/{migration-command-scaffold-CjvwO6at.mjs → migration-command-scaffold-V52dV2Tv.mjs} +3 -3
  65. package/dist/{migration-command-scaffold-CjvwO6at.mjs.map → migration-command-scaffold-V52dV2Tv.mjs.map} +1 -1
  66. package/dist/{migration-graph-D7DVUElV.mjs → migration-graph-DKl_IYsF.mjs} +377 -85
  67. package/dist/migration-graph-DKl_IYsF.mjs.map +1 -0
  68. package/dist/{migration-list-styler-BRwF4-gy.mjs → migration-list-styler-COQbZmXk.mjs} +61 -46
  69. package/dist/migration-list-styler-COQbZmXk.mjs.map +1 -0
  70. package/dist/{migration-plan-9DJ7q7_z.mjs → migration-plan-CaeKCKp4.mjs} +5 -5
  71. package/dist/{migration-plan-9DJ7q7_z.mjs.map → migration-plan-CaeKCKp4.mjs.map} +1 -1
  72. package/dist/{migration-types-D2FW63pr.d.mts → migration-types-CAQ-0TEE.d.mts} +1 -1
  73. package/dist/{migration-types-D2FW63pr.d.mts.map → migration-types-CAQ-0TEE.d.mts.map} +1 -1
  74. package/dist/{migrations-Cv2jxNNK.mjs → migrations-DQ1t3XFL.mjs} +2 -2
  75. package/dist/{migrations-Cv2jxNNK.mjs.map → migrations-DQ1t3XFL.mjs.map} +1 -1
  76. package/dist/{output-B60Gw5fu.mjs → output-CF_hqzI-.mjs} +1 -1
  77. package/dist/{output-B60Gw5fu.mjs.map → output-CF_hqzI-.mjs.map} +1 -1
  78. package/dist/telemetry-Q88WHwlv.mjs +122 -0
  79. package/dist/telemetry-Q88WHwlv.mjs.map +1 -0
  80. package/dist/{terminal-ui-5Y6mrg93.d.mts → terminal-ui-C3xGyxW-.d.mts} +1 -1
  81. package/dist/{terminal-ui-5Y6mrg93.d.mts.map → terminal-ui-C3xGyxW-.d.mts.map} +1 -1
  82. package/dist/{types-Dt_SfqFm.d.mts → types-DiC683UW.d.mts} +8 -2
  83. package/dist/{types-Dt_SfqFm.d.mts.map → types-DiC683UW.d.mts.map} +1 -1
  84. package/dist/{verify-DCA9Sldu.mjs → verify-CreSJ1Mz.mjs} +2 -2
  85. package/dist/{verify-DCA9Sldu.mjs.map → verify-CreSJ1Mz.mjs.map} +1 -1
  86. package/package.json +22 -18
  87. package/src/cli.ts +5 -0
  88. package/src/commands/init/index.ts +6 -35
  89. package/src/commands/init/init.ts +1 -14
  90. package/src/commands/init/inputs.ts +0 -75
  91. package/src/commands/migration-graph.ts +43 -2
  92. package/src/commands/migration-status.ts +1 -1
  93. package/src/commands/telemetry/index.ts +107 -0
  94. package/src/commands/telemetry/status.ts +67 -0
  95. package/src/control-api/client.ts +11 -1
  96. package/src/control-api/operations/apply.ts +1 -0
  97. package/src/control-api/operations/migration-apply.ts +10 -3
  98. package/src/control-api/types.ts +12 -1
  99. package/src/utils/formatters/migration-graph-lane-colors.ts +31 -0
  100. package/src/utils/formatters/migration-graph-layout.ts +51 -7
  101. package/src/utils/formatters/migration-graph-tree-render.ts +414 -51
  102. package/src/utils/formatters/migration-list-graph-topology.ts +67 -83
  103. package/src/utils/global-flags.ts +35 -0
  104. package/src/utils/telemetry.ts +68 -32
  105. package/dist/client-KgJorIvG.mjs.map +0 -1
  106. package/dist/init-Cv9UzWL5.mjs.map +0 -1
  107. package/dist/migration-graph-D7DVUElV.mjs.map +0 -1
  108. package/dist/migration-list-styler-BRwF4-gy.mjs.map +0 -1
@@ -1,7 +1,8 @@
1
1
  import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
2
- import { bold } from 'colorette';
2
+ import { bold, createColors } from 'colorette';
3
3
  import stringWidth from 'string-width';
4
4
  import type { GlyphMode } from '../glyph-mode';
5
+ import { laneColorForColumn } from './migration-graph-lane-colors';
5
6
  import type {
6
7
  MigrationGraphGridModel,
7
8
  MigrationGraphGridRow,
@@ -48,6 +49,7 @@ interface MigrationGraphTreeGlyphPalette {
48
49
  readonly arcBranchCorner: string;
49
50
  readonly arcBranchTee: string;
50
51
  readonly arcLandCorner: string;
52
+ readonly arcLandTee: string;
51
53
  readonly arcCrossing: string;
52
54
  readonly arcLandBridge: string;
53
55
  readonly horizontalPass: string;
@@ -71,6 +73,7 @@ const UNICODE_PALETTE: MigrationGraphTreeGlyphPalette = {
71
73
  arcBranchCorner: '╮ ',
72
74
  arcBranchTee: '┬─',
73
75
  arcLandCorner: '╯ ',
76
+ arcLandTee: '┴─',
74
77
  arcCrossing: '┼─',
75
78
  arcLandBridge: '──',
76
79
  horizontalPass: '──',
@@ -94,6 +97,7 @@ const ASCII_PALETTE: MigrationGraphTreeGlyphPalette = {
94
97
  arcBranchCorner: '\\ ',
95
98
  arcBranchTee: '+-',
96
99
  arcLandCorner: '/ ',
100
+ arcLandTee: '+-',
97
101
  arcCrossing: '+-',
98
102
  arcLandBridge: '--',
99
103
  horizontalPass: '--',
@@ -116,56 +120,327 @@ function arrowForEdgeKind(
116
120
  return palette.edgeArrow[kind];
117
121
  }
118
122
 
123
+ /**
124
+ * The leftmost lane (column 0) renders with the neutral dim lane style rather
125
+ * than a palette hue — in the common single-lane case it has nothing to be told
126
+ * apart from. Used as the "no owning arc" sentinel during colour resolution.
127
+ */
128
+ const NEUTRAL_LANE = 0;
129
+
130
+ /**
131
+ * Forced bold for branch-coloured names. A branched name pairs its lane hue
132
+ * (also forced, via {@link laneColorForColumn}) with bold; both must emit even
133
+ * when colorette's ambient TTY detection is off, so the colorized branch name
134
+ * is deterministically bold + hue rather than hue-only.
135
+ */
136
+ const { bold: forcedBold } = createColors({ useColor: true });
137
+
138
+ /**
139
+ * The colour-source column for each cell of a row, resolved together because a
140
+ * routed back-arc spans columns and must read as **one hue** rather than a
141
+ * per-column "rainbow". An arc's horizontal bridges, corners, and node-pair
142
+ * connector all take the arc's owning back-lane column (the corner that closes
143
+ * the arc), not the column they pass through.
144
+ */
145
+ interface RowLaneColors {
146
+ /** Colour column for a cell's structural glyph (lane / spine / arc body). */
147
+ readonly lane: readonly number[];
148
+ /** Colour column for a node arc-pair's connector half (`◂` / `─`). */
149
+ readonly connector: readonly number[];
150
+ /**
151
+ * Colour column for the trailing `─` of a landing tee (`┴─`). The junction
152
+ * (`lane`) keeps its own column; the dash leads into the next converging arc.
153
+ */
154
+ readonly dash: readonly number[];
155
+ }
156
+
157
+ /**
158
+ * Resolve per-cell colour columns for a row. Scanning right-to-left lets each
159
+ * arc segment inherit the hue of the arc it leads into.
160
+ *
161
+ * On a converging-landing line (`○◂──────┴─┴─╯`), every horizontal dash segment
162
+ * takes the hue of the **nearest landing anchor** — the next `arc-land-tee` or
163
+ * `arc-land-corner` — to its right, i.e. the branch it leads into: the bridge
164
+ * run leads into the first converging arc, and each tee's trailing `─` leads
165
+ * into the next arc out. Tee/corner junction glyphs keep their own column hue.
166
+ * This mirrors the forward connector's `┬─` rule (see
167
+ * {@link resolveConnectorLaneColors}). A single (non-converging) landing has
168
+ * only the corner as an anchor, so its whole horizontal run reads as one hue.
169
+ *
170
+ * The source side (`○─`, `arc-branch-tee`, `arc-branch-corner`) and pure
171
+ * horizontal passes are unaffected: they track the nearest corner to the right
172
+ * (`arcCorner`), so a routed back-arc's source fan still reads as one hue. A
173
+ * crossing can only be one colour, so it takes the arc owning the horizontal
174
+ * run at this row; the crossed vertical lane is occluded at that one cell and
175
+ * reappears on the next row.
176
+ */
177
+ function resolveRowLaneColors(cells: readonly StructuralCell[]): RowLaneColors {
178
+ const lane = new Array<number>(cells.length);
179
+ const connector = new Array<number>(cells.length);
180
+ const dash = new Array<number>(cells.length);
181
+ let arcCorner = NEUTRAL_LANE;
182
+ let landingAnchor = NEUTRAL_LANE;
183
+ for (let column = cells.length - 1; column >= 0; column--) {
184
+ const cell = cells[column];
185
+ connector[column] = landingAnchor !== NEUTRAL_LANE ? landingAnchor : arcCorner;
186
+ switch (cell?.kind) {
187
+ case 'arc-branch-corner':
188
+ arcCorner = column;
189
+ lane[column] = column;
190
+ dash[column] = column;
191
+ break;
192
+ case 'arc-land-corner':
193
+ arcCorner = column;
194
+ landingAnchor = column;
195
+ lane[column] = column;
196
+ dash[column] = column;
197
+ break;
198
+ // An inner co-sourced arc's own back-lane junction: its vertical run
199
+ // continues below in this column, so the whole `┬─` keeps its own column.
200
+ case 'arc-branch-tee':
201
+ lane[column] = column;
202
+ dash[column] = column;
203
+ break;
204
+ // The symmetric co-landing junction: the `┴` keeps its own column (its
205
+ // vertical run continues above), but the trailing `─` leads into the next
206
+ // converging arc — the nearest landing anchor still to its right.
207
+ case 'arc-land-tee':
208
+ lane[column] = column;
209
+ dash[column] = landingAnchor === NEUTRAL_LANE ? column : landingAnchor;
210
+ landingAnchor = column;
211
+ break;
212
+ case 'arc-crossing':
213
+ case 'arc-land-bridge': {
214
+ const served = landingAnchor !== NEUTRAL_LANE ? landingAnchor : arcCorner;
215
+ lane[column] = served;
216
+ dash[column] = served;
217
+ break;
218
+ }
219
+ case 'horizontal-pass':
220
+ lane[column] = arcCorner === NEUTRAL_LANE ? column : arcCorner;
221
+ dash[column] = lane[column] ?? column;
222
+ break;
223
+ case 'node':
224
+ lane[column] = column;
225
+ dash[column] = column;
226
+ arcCorner = NEUTRAL_LANE;
227
+ landingAnchor = NEUTRAL_LANE;
228
+ break;
229
+ default:
230
+ lane[column] = column;
231
+ dash[column] = column;
232
+ arcCorner = NEUTRAL_LANE;
233
+ landingAnchor = NEUTRAL_LANE;
234
+ }
235
+ }
236
+ return { lane, connector, dash };
237
+ }
238
+
239
+ /**
240
+ * Per-cell colour for a forward branch/merge connector row, split into the
241
+ * cell's junction `glyph` and its trailing `dash`. A connector's horizontal run
242
+ * is one logical line (a fork into new lanes, or a merge into a surviving lane)
243
+ * and reads best as the colour of the lane each segment serves — not dim-gray
244
+ * or a per-pass-through-column "rainbow".
245
+ */
246
+ interface ConnectorLaneColors {
247
+ /** Colour column for a cell's junction glyph (`├` / `┬` / `┴` / `╮` / `╯`). */
248
+ readonly glyph: readonly number[];
249
+ /** Colour column for a tee's trailing `─` — the branch it leads into. */
250
+ readonly dash: readonly number[];
251
+ }
252
+
253
+ /**
254
+ * Resolve per-cell connector colours. Scanning right-to-left, a corner or an
255
+ * intermediate tee anchors its own lane (its junction glyph takes that column),
256
+ * but a tee's **trailing dash leads into the branch on its right** (the next
257
+ * branch point), so `┬─` reads as "this lane, then on toward the next" rather
258
+ * than tinting the dash with the left lane. The leading tee at `startLane` (the
259
+ * fork/merge origin) and pure horizontal segments inherit the nearest branch
260
+ * point to their right whole-cell, so the run into a branch — or collapsing
261
+ * into a merge corner — stays continuous. An `arc-crossing` keeps its junction
262
+ * glyph at its own column but re-anchors `owner` like an intermediate tee so
263
+ * dashes on both sides lead into the nearest branch on their right. Pass-through
264
+ * verticals outside the run keep their own column (column 0 stays neutral).
265
+ */
266
+ export function resolveConnectorLaneColors(
267
+ cells: readonly StructuralCell[],
268
+ startLane: number,
269
+ ): ConnectorLaneColors {
270
+ const glyph = new Array<number>(cells.length);
271
+ const dash = new Array<number>(cells.length);
272
+ let owner = NEUTRAL_LANE;
273
+ for (let column = cells.length - 1; column >= 0; column--) {
274
+ const cell = cells[column];
275
+ switch (cell?.kind) {
276
+ case 'branch-corner':
277
+ case 'merge-corner':
278
+ owner = column;
279
+ glyph[column] = column;
280
+ dash[column] = column;
281
+ break;
282
+ case 'branch-tee':
283
+ case 'merge-tee':
284
+ if (column === startLane) {
285
+ const served = owner === NEUTRAL_LANE ? column : owner;
286
+ glyph[column] = column;
287
+ dash[column] = served;
288
+ } else {
289
+ dash[column] = owner === NEUTRAL_LANE ? column : owner;
290
+ glyph[column] = column;
291
+ owner = column;
292
+ }
293
+ break;
294
+ case 'arc-crossing':
295
+ glyph[column] = column;
296
+ dash[column] = owner === NEUTRAL_LANE ? column : owner;
297
+ owner = column;
298
+ break;
299
+ case 'horizontal-pass': {
300
+ const served = owner === NEUTRAL_LANE ? column : owner;
301
+ glyph[column] = served;
302
+ dash[column] = served;
303
+ break;
304
+ }
305
+ default:
306
+ glyph[column] = column;
307
+ dash[column] = column;
308
+ }
309
+ }
310
+ return { glyph, dash };
311
+ }
312
+
313
+ /**
314
+ * Style a structural glyph by its resolved colour column. Column 0 and the
315
+ * neutral sentinel render dim (`style.lane`); columns ≥ 1 take a palette hue.
316
+ */
317
+ function laneStylerForColumn(
318
+ colorColumn: number,
319
+ colorize: boolean,
320
+ style: MigrationListStyler,
321
+ ): (text: string) => string {
322
+ if (!colorize || colorColumn <= NEUTRAL_LANE) {
323
+ return (text) => style.lane(text);
324
+ }
325
+ return laneColorForColumn(colorColumn);
326
+ }
327
+
328
+ /**
329
+ * Tint a branch-owned token (direction arrow, migration name) by its edge's
330
+ * lane so the whole branch row reads in one colour. Column 0 has nothing to be
331
+ * told apart from in the common linear chain, so it keeps the token's existing
332
+ * default styling (`fallback`) rather than a palette hue; only lanes ≥ 1 take a
333
+ * colour. With colour off, the fallback (also colourless) is used unchanged.
334
+ */
335
+ function branchStylerOrDefault(
336
+ column: number,
337
+ colorize: boolean,
338
+ fallback: (text: string) => string,
339
+ ): (text: string) => string {
340
+ if (!colorize || column <= NEUTRAL_LANE) {
341
+ return fallback;
342
+ }
343
+ return laneColorForColumn(column);
344
+ }
345
+
346
+ /**
347
+ * Render a connector tee (`├─` / `┬─` / `┴─`) with its junction glyph and its
348
+ * trailing dash coloured independently: the junction anchors its own lane while
349
+ * the dash leads into the branch on its right.
350
+ */
351
+ function renderConnectorTee(
352
+ pair: string,
353
+ glyphColumn: number,
354
+ dashColumn: number,
355
+ colorize: boolean,
356
+ style: MigrationListStyler,
357
+ ): string {
358
+ const glyph = laneStylerForColumn(glyphColumn, colorize, style);
359
+ if (glyphColumn === dashColumn) {
360
+ return glyph(pair);
361
+ }
362
+ return glyph(pair.slice(0, 1)) + laneStylerForColumn(dashColumn, colorize, style)(pair.slice(1));
363
+ }
364
+
119
365
  /**
120
366
  * A node-marker glyph pair (`○◂`, `○─`, `*<`, `*-`) is the contract node
121
- * marker (`○` / `*`) followed by an arc connector (`◂` / `─` / `<` / `-`).
122
- * The marker is the signal and stays bright (`style.kind`); the connector is
123
- * gutter and stays dim (`style.lane`) consistent with the plain node marker,
124
- * which is never dimmed.
367
+ * marker (`○` / `*`) followed by an arc connector (`◂` / `─` / `<` / `-`). The
368
+ * marker takes its own lane's hue (so each node visibly belongs to its branch);
369
+ * the connector follows the arc it belongs to (its owning back-lane hue).
370
+ * Direction arrows are handled elsewhere — they take their edge's lane hue too.
125
371
  */
126
- function renderNodeMarkerPair(pair: string, style: MigrationListStyler): string {
127
- return style.kind(pair.slice(0, 1)) + style.lane(pair.slice(1));
372
+ function renderNodeMarkerPair(
373
+ pair: string,
374
+ nodeColumn: number,
375
+ arcColumn: number,
376
+ colorize: boolean,
377
+ style: MigrationListStyler,
378
+ ): string {
379
+ const marker = laneStylerForColumn(nodeColumn, colorize, style);
380
+ const connector = laneStylerForColumn(arcColumn, colorize, style);
381
+ return marker(pair.slice(0, 1)) + connector(pair.slice(1));
128
382
  }
129
383
 
130
384
  function renderCellPair(
131
385
  cell: StructuralCell,
386
+ column: number,
387
+ colors: RowLaneColors,
388
+ colorize: boolean,
132
389
  style: MigrationListStyler,
133
390
  palette: MigrationGraphTreeGlyphPalette,
134
391
  ): string {
392
+ const laneColumn = colors.lane[column] ?? column;
393
+ const lane = laneStylerForColumn(laneColumn, colorize, style);
135
394
  switch (cell.kind) {
136
- case 'node':
137
- if (cell.arcLand === true) return renderNodeMarkerPair(palette.arcLand, style);
138
- if (cell.arcTee === true) return renderNodeMarkerPair(palette.arcTee, style);
139
- return style.kind(palette.node);
395
+ case 'node': {
396
+ const arcColumn = colors.connector[column] ?? NEUTRAL_LANE;
397
+ if (cell.arcLand === true) {
398
+ return renderNodeMarkerPair(palette.arcLand, column, arcColumn, colorize, style);
399
+ }
400
+ if (cell.arcTee === true) {
401
+ return renderNodeMarkerPair(palette.arcTee, column, arcColumn, colorize, style);
402
+ }
403
+ return lane(palette.node);
404
+ }
140
405
  case 'vertical-pass':
141
- return style.lane(palette.verticalPass);
406
+ return lane(palette.verticalPass);
142
407
  case 'edge-lane':
143
- // The lane stays dim; the direction arrow (↑ / ↓ / ⟲) is the signal and
144
- // stays bright, like the contract-node marker.
145
408
  return cell.ownsLabel
146
- ? style.lane(palette.verticalPass.trimEnd()) +
147
- style.kind(arrowForEdgeKind(cell.edgeKind, palette))
148
- : style.lane(palette.verticalPass);
409
+ ? lane(palette.verticalPass.trimEnd()) +
410
+ branchStylerOrDefault(
411
+ column,
412
+ colorize,
413
+ style.kind,
414
+ )(arrowForEdgeKind(cell.edgeKind, palette))
415
+ : lane(palette.verticalPass);
149
416
  case 'branch-tee':
150
- return style.lane(palette.branchTee);
417
+ return lane(palette.branchTee);
151
418
  case 'merge-tee':
152
- return style.lane(palette.mergeTee);
419
+ return lane(palette.mergeTee);
153
420
  case 'branch-corner':
154
- return style.lane(palette.branchCorner);
421
+ return lane(palette.branchCorner);
155
422
  case 'merge-corner':
156
- return style.lane(palette.mergeCorner);
423
+ return lane(palette.mergeCorner);
157
424
  case 'arc-branch-corner':
158
- return style.lane(palette.arcBranchCorner);
425
+ return lane(palette.arcBranchCorner);
159
426
  case 'arc-branch-tee':
160
- return style.lane(palette.arcBranchTee);
427
+ return lane(palette.arcBranchTee);
161
428
  case 'arc-land-corner':
162
- return style.lane(palette.arcLandCorner);
429
+ return lane(palette.arcLandCorner);
430
+ case 'arc-land-tee':
431
+ return renderConnectorTee(
432
+ palette.arcLandTee,
433
+ laneColumn,
434
+ colors.dash[column] ?? laneColumn,
435
+ colorize,
436
+ style,
437
+ );
163
438
  case 'arc-crossing':
164
- return style.lane(palette.arcCrossing);
439
+ return lane(palette.arcLandBridge);
165
440
  case 'arc-land-bridge':
166
- return style.lane(palette.arcLandBridge);
441
+ return lane(palette.arcLandBridge);
167
442
  case 'horizontal-pass':
168
- return style.lane(palette.horizontalPass);
443
+ return lane(palette.horizontalPass);
169
444
  case 'empty':
170
445
  return ' ';
171
446
  }
@@ -174,34 +449,56 @@ function renderCellPair(
174
449
  function renderConnectorRow(
175
450
  row: MigrationGraphGridRow,
176
451
  gridWidth: number,
452
+ colorize: boolean,
177
453
  style: MigrationListStyler,
178
454
  palette: MigrationGraphTreeGlyphPalette,
179
455
  ): string {
180
456
  const isMerge = row.kind === 'merge-connector';
181
457
  if (row.cells.length > 0) {
458
+ const colors = resolveConnectorLaneColors(row.cells, row.startLane ?? 0);
182
459
  let seenTee = false;
183
460
  let out = '';
184
- for (const cell of row.cells) {
461
+ for (let column = 0; column < row.cells.length; column++) {
462
+ const cell = row.cells[column];
463
+ if (cell === undefined) continue;
464
+ const glyphColumn = colors.glyph[column] ?? column;
465
+ const dashColumn = colors.dash[column] ?? glyphColumn;
466
+ const lane = laneStylerForColumn(glyphColumn, colorize, style);
185
467
  switch (cell.kind) {
186
468
  case 'branch-tee':
187
- out += style.lane(seenTee ? palette.connectorBranchTeeCo : palette.connectorBranchTee);
469
+ out += renderConnectorTee(
470
+ seenTee ? palette.connectorBranchTeeCo : palette.connectorBranchTee,
471
+ glyphColumn,
472
+ dashColumn,
473
+ colorize,
474
+ style,
475
+ );
188
476
  seenTee = true;
189
477
  break;
190
478
  case 'merge-tee':
191
- out += style.lane(seenTee ? palette.connectorMergeTeeCo : palette.connectorBranchTee);
479
+ out += renderConnectorTee(
480
+ seenTee ? palette.connectorMergeTeeCo : palette.connectorBranchTee,
481
+ glyphColumn,
482
+ dashColumn,
483
+ colorize,
484
+ style,
485
+ );
192
486
  seenTee = true;
193
487
  break;
194
488
  case 'branch-corner':
195
- out += style.lane(palette.branchCorner);
489
+ out += lane(palette.branchCorner);
196
490
  break;
197
491
  case 'merge-corner':
198
- out += style.lane(palette.mergeCorner);
492
+ out += lane(palette.mergeCorner);
199
493
  break;
200
494
  case 'vertical-pass':
201
- out += style.lane(palette.verticalPass);
495
+ out += lane(palette.verticalPass);
202
496
  break;
203
497
  case 'horizontal-pass':
204
- out += style.lane(palette.horizontalPass);
498
+ out += lane(palette.horizontalPass);
499
+ break;
500
+ case 'arc-crossing':
501
+ out += renderConnectorTee(palette.arcCrossing, glyphColumn, dashColumn, colorize, style);
205
502
  break;
206
503
  default:
207
504
  out += ' ';
@@ -218,13 +515,15 @@ function renderConnectorRow(
218
515
 
219
516
  const start = row.startLane ?? 0;
220
517
  const end = row.endLane ?? start;
518
+ // The whole fork/merge run reads as one line in the served lane's hue (the
519
+ // corner it reaches); pass-through columns outside the run keep their own.
520
+ const runLane = laneStylerForColumn(end, colorize, style);
221
521
  let out = '';
222
522
  for (let column = 0; column < gridWidth; column++) {
223
523
  if (column < start || column > end) out += ' ';
224
- else if (column === start) out += style.lane(palette.connectorBranchTee);
225
- else if (column === end)
226
- out += style.lane(isMerge ? palette.mergeCorner : palette.branchCorner);
227
- else out += style.lane(isMerge ? palette.connectorMergeTeeCo : palette.connectorBranchTeeCo);
524
+ else if (column === start) out += runLane(palette.connectorBranchTee);
525
+ else if (column === end) out += runLane(isMerge ? palette.mergeCorner : palette.branchCorner);
526
+ else out += runLane(isMerge ? palette.connectorMergeTeeCo : palette.connectorBranchTeeCo);
228
527
  }
229
528
  return out;
230
529
  }
@@ -297,6 +596,13 @@ function padVisible(text: string, targetWidth: number): string {
297
596
  return text + ' '.repeat(padding);
298
597
  }
299
598
 
599
+ const ANSI_ESCAPE = '\x1b';
600
+
601
+ function trimTrailingWhitespace(line: string): string {
602
+ const trailingSpaceBeforeReset = new RegExp(`[\\t ]+((?:${ANSI_ESCAPE}\\[[0-9;]*m)+)$`);
603
+ return line.replace(trailingSpaceBeforeReset, '$1').replace(/\s+$/, '');
604
+ }
605
+
300
606
  function gridWidthForModel(rows: readonly MigrationGraphGridRow[]): number {
301
607
  return rows.reduce(
302
608
  (max, row) =>
@@ -373,19 +679,32 @@ export function renderMigrationGraphTree(
373
679
  }
374
680
 
375
681
  if (row.kind === 'branch-connector' || row.kind === 'merge-connector') {
376
- lines.push(renderConnectorRow(row, gridWidth, style, palette).replace(/\s+$/, ''));
682
+ lines.push(
683
+ trimTrailingWhitespace(renderConnectorRow(row, gridWidth, opts.colorize, style, palette)),
684
+ );
377
685
  continue;
378
686
  }
379
687
 
380
- let gutter = row.cells.map((cell) => renderCellPair(cell, style, palette)).join('');
381
- const prevRow = model.rows[rowIndex - 1];
688
+ const cellColors = resolveRowLaneColors(row.cells);
689
+ let gutter = row.cells
690
+ .map((cell, column) =>
691
+ renderCellPair(cell, column, cellColors, opts.colorize, style, palette),
692
+ )
693
+ .join('');
382
694
  let laneSpan = row.cells.length;
383
695
  if (row.kind === 'node') {
384
696
  const contractHash = row.contractHash ?? EMPTY_CONTRACT_HASH;
385
- if (prevRow?.kind === 'merge-connector' || contractHash === EMPTY_CONTRACT_HASH) {
697
+ if (contractHash === EMPTY_CONTRACT_HASH) {
386
698
  laneSpan = 1;
387
699
  } else {
388
- laneSpan = row.cells.length;
700
+ let lastActiveColumn = -1;
701
+ for (let column = row.cells.length - 1; column >= 0; column--) {
702
+ if (row.cells[column]?.kind !== 'empty') {
703
+ lastActiveColumn = column;
704
+ break;
705
+ }
706
+ }
707
+ laneSpan = lastActiveColumn >= 0 ? lastActiveColumn + 1 : 1;
389
708
  }
390
709
  }
391
710
  const labelColumn =
@@ -402,12 +721,16 @@ export function renderMigrationGraphTree(
402
721
  ) {
403
722
  gutter = row.cells
404
723
  .slice(0, 1)
405
- .map((cell) => renderCellPair(cell, style, palette))
724
+ .map((cell, column) =>
725
+ renderCellPair(cell, column, cellColors, opts.colorize, style, palette),
726
+ )
406
727
  .join('');
407
728
  } else if (row.kind === 'node' && laneSpan < row.cells.length && !nodeHasArcDecoration(row)) {
408
729
  gutter = row.cells
409
730
  .slice(0, laneSpan)
410
- .map((cell) => renderCellPair(cell, style, palette))
731
+ .map((cell, column) =>
732
+ renderCellPair(cell, column, cellColors, opts.colorize, style, palette),
733
+ )
411
734
  .join('');
412
735
  } else if (gutter.length < laneSpan * 2) {
413
736
  gutter = gutter.padEnd(laneSpan * 2, ' ');
@@ -421,16 +744,18 @@ export function renderMigrationGraphTree(
421
744
  if (contractHash === EMPTY_CONTRACT_HASH) {
422
745
  const trailingLanes = row.cells
423
746
  .slice(1)
424
- .map((cell) => renderCellPair(cell, style, palette))
747
+ .map((cell, offset) =>
748
+ renderCellPair(cell, offset + 1, cellColors, opts.colorize, style, palette),
749
+ )
425
750
  .join('');
426
751
  const emptyGutter = palette.emptySource.padEnd(2, ' ') + trailingLanes;
427
752
  const overlayNames = overlayNamesForContract(contractHash, opts);
428
753
  if (overlayNames.length === 0) {
429
- lines.push(emptyGutter.replace(/\s+$/, ''));
754
+ lines.push(trimTrailingWhitespace(emptyGutter));
430
755
  continue;
431
756
  }
432
757
  const overlay = style.refs(overlayNames);
433
- lines.push(`${padVisible(emptyGutter, dataColumn)}${overlay}`.replace(/\s+$/, ''));
758
+ lines.push(trimTrailingWhitespace(`${padVisible(emptyGutter, dataColumn)}${overlay}`));
434
759
  continue;
435
760
  }
436
761
  const hashText = style.sourceHash(
@@ -442,7 +767,7 @@ export function renderMigrationGraphTree(
442
767
  ? ' '.repeat(Math.max(0, dataColumn - labelColumn - stringWidth(hashText)))
443
768
  : '';
444
769
  const overlay = overlayNames.length > 0 ? style.refs(overlayNames) : '';
445
- lines.push(`${gutterPad}${hashText}${overlayPad}${overlay}`.replace(/\s+$/, ''));
770
+ lines.push(trimTrailingWhitespace(`${gutterPad}${hashText}${overlayPad}${overlay}`));
446
771
  continue;
447
772
  }
448
773
 
@@ -450,10 +775,48 @@ export function renderMigrationGraphTree(
450
775
  if (edge === undefined) continue;
451
776
 
452
777
  const dirNamePadding = ' '.repeat(Math.max(0, dirNameWidth - edge.dirName.length));
453
- const dirName = `${style.dirName(edge.dirName)}${dirNamePadding}`;
778
+ const laneIndex = row.laneIndex ?? 0;
779
+ // A branched name keeps its bold (via `style.dirName`) and adds the lane
780
+ // hue, so it reads as one with its lane/arrow; column-0 names stay bold-only.
781
+ const dirNameStyler =
782
+ opts.colorize && laneIndex > NEUTRAL_LANE
783
+ ? (text: string) => forcedBold(laneColorForColumn(laneIndex)(text))
784
+ : style.dirName;
785
+ const dirName = `${dirNameStyler(edge.dirName)}${dirNamePadding}`;
454
786
  const hashColumn = formatEdgeHashColumn(edge, style, hashLength, palette);
455
- lines.push(`${gutterPad}${dirName}${hashColumn}`.replace(/\s+$/, ''));
787
+ lines.push(trimTrailingWhitespace(`${gutterPad}${dirName}${hashColumn}`));
456
788
  }
457
789
 
458
790
  return lines.join('\n');
459
791
  }
792
+
793
+ export interface RenderMigrationGraphLegendOptions {
794
+ readonly colorize: boolean;
795
+ readonly glyphMode?: GlyphMode;
796
+ }
797
+
798
+ /**
799
+ * A compact key for the `--tree` visual language: the contract marker, the
800
+ * in-lane direction arrows, the empty baseline, the `(refs)` overlay (including
801
+ * the reserved `db` live-database and `contract` working-schema markers), and a
802
+ * worked sample of the data-column `from → to` migration hash arrow.
803
+ *
804
+ * Honors the same glyph palette (unicode vs ASCII) and `colorize` gate as the
805
+ * tree renderer, so the key matches whatever the graph itself drew and stays
806
+ * pipe-safe (zero ANSI when color is off). The caller adds the trailing blank
807
+ * line that separates this stderr key from the graph on stdout.
808
+ */
809
+ export function renderMigrationGraphLegend(opts: RenderMigrationGraphLegendOptions): string {
810
+ const palette = paletteFor(opts.glyphMode ?? 'unicode');
811
+ const style = createAnsiMigrationListStyler({ useColor: opts.colorize });
812
+ const node = palette.node.trimEnd();
813
+ const sampleArrow = `${style.sourceHash('aaaaaa')} ${style.glyph(palette.forwardArrow)} ${style.destHash('bbbbbb')}`;
814
+ return [
815
+ 'Legend:',
816
+ ` ${style.kind(node)} ${style.summary('contract')} ${style.kind(palette.edgeArrow.forward)} ${style.summary('forward')} ${style.kind(palette.edgeArrow.rollback)} ${style.summary('rollback')}`,
817
+ ` ${style.kind(palette.edgeArrow.self)} ${style.summary('migration without schema change')}`,
818
+ ` ${style.kind(palette.emptySource)} ${style.summary('empty database (baseline)')}`,
819
+ ` ${style.refs(['refs'])} ${style.summary(`${DB_MARKER_NAME} / ${CONTRACT_MARKER_NAME} markers`)}`,
820
+ ` ${sampleArrow} ${style.summary('migration from contract aaaaaa to bbbbbb')}`,
821
+ ].join('\n');
822
+ }