@prisma-next/cli 0.12.0-dev.67 → 0.12.0-dev.69
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.
- package/dist/cli.mjs +5 -5
- package/dist/commands/migrate.d.mts.map +1 -1
- package/dist/commands/migrate.mjs +17 -11
- package/dist/commands/migrate.mjs.map +1 -1
- package/dist/commands/migration-check.mjs +1 -1
- package/dist/commands/migration-graph.mjs +2 -2
- package/dist/commands/migration-graph.mjs.map +1 -1
- package/dist/commands/migration-list.mjs +1 -1
- package/dist/commands/migration-log.mjs +1 -1
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +1 -1
- package/dist/{migration-check-soB5uZEQ.mjs → migration-check-VwM8xCZV.mjs} +2 -1
- package/dist/{migration-check-soB5uZEQ.mjs.map → migration-check-VwM8xCZV.mjs.map} +1 -1
- package/dist/migration-graph-command-render-BAOzyYF6.mjs +1822 -0
- package/dist/migration-graph-command-render-BAOzyYF6.mjs.map +1 -0
- package/dist/{migration-list-CyLslAtv.mjs → migration-list-CihF6w5z.mjs} +2 -2
- package/dist/migration-list-CihF6w5z.mjs.map +1 -0
- package/dist/{migration-log-BYt18y2H.mjs → migration-log-B75IArji.mjs} +2 -2
- package/dist/{migration-log-BYt18y2H.mjs.map → migration-log-B75IArji.mjs.map} +1 -1
- package/dist/{migration-status-ciYpjhtu.mjs → migration-status-CSVe6ZlD.mjs} +4 -3
- package/dist/migration-status-CSVe6ZlD.mjs.map +1 -0
- package/package.json +19 -18
- package/src/commands/migrate.ts +35 -26
- package/src/commands/migration-graph.ts +1 -1
- package/src/commands/migration-list.ts +1 -1
- package/src/commands/migration-status-overlay.ts +1 -1
- package/src/commands/migration-status.ts +4 -2
- package/src/utils/formatters/migration-graph-command-render.ts +239 -0
- package/src/utils/formatters/migration-graph-grid-layout.ts +857 -0
- package/src/utils/formatters/migration-graph-labels.ts +406 -0
- package/src/utils/formatters/migration-graph-model.ts +94 -0
- package/src/utils/formatters/migration-graph-occlusion-render.ts +245 -0
- package/src/utils/formatters/migration-graph-space-render.ts +73 -33
- package/src/utils/formatters/migration-list-render.ts +1 -1
- package/dist/migration-graph-space-render-Cpg0ql8v.mjs +0 -2370
- package/dist/migration-graph-space-render-Cpg0ql8v.mjs.map +0 -1
- package/dist/migration-list-CyLslAtv.mjs.map +0 -1
- package/dist/migration-status-ciYpjhtu.mjs.map +0 -1
- package/src/utils/formatters/migration-graph-lane-colors.ts +0 -194
- package/src/utils/formatters/migration-graph-layout.ts +0 -1308
- package/src/utils/formatters/migration-graph-tree-render.ts +0 -1337
|
@@ -1,1337 +0,0 @@
|
|
|
1
|
-
import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
|
|
2
|
-
import { bold, createColors, green, yellow } from 'colorette';
|
|
3
|
-
import stringWidth from 'string-width';
|
|
4
|
-
import type { GlyphMode } from '../glyph-mode';
|
|
5
|
-
import {
|
|
6
|
-
laneColorForColumn,
|
|
7
|
-
NEUTRAL_LANE_COLUMN,
|
|
8
|
-
type RowArcLaneColors,
|
|
9
|
-
resolveConnectorLaneColors,
|
|
10
|
-
resolveRowArcLaneColors,
|
|
11
|
-
stylerForLaneColumn,
|
|
12
|
-
} from './migration-graph-lane-colors';
|
|
13
|
-
|
|
14
|
-
export { resolveConnectorLaneColors } from './migration-graph-lane-colors';
|
|
15
|
-
|
|
16
|
-
import type {
|
|
17
|
-
MigrationGraphGridModel,
|
|
18
|
-
MigrationGraphGridRow,
|
|
19
|
-
StructuralCell,
|
|
20
|
-
} from './migration-graph-layout';
|
|
21
|
-
import type { ClassifiedEdge } from './migration-graph-rows';
|
|
22
|
-
import {
|
|
23
|
-
MIGRATION_LIST_HASH_WIDTH,
|
|
24
|
-
migrationListEmptySource,
|
|
25
|
-
migrationListForwardArrow,
|
|
26
|
-
padFromHashColumn,
|
|
27
|
-
} from './migration-list-data-column';
|
|
28
|
-
import type { MigrationEdgeKind } from './migration-list-graph-topology';
|
|
29
|
-
import type { MigrationListStyler } from './migration-list-render';
|
|
30
|
-
import {
|
|
31
|
-
CONTRACT_MARKER_NAME,
|
|
32
|
-
createAnsiMigrationListStyler,
|
|
33
|
-
formatContractNodeOverlays,
|
|
34
|
-
} from './migration-list-styler';
|
|
35
|
-
|
|
36
|
-
const LABEL_GAP = 2;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The live-database overlay marker. Just another ref as far as styling goes —
|
|
40
|
-
* the only emphasized markers are the active ref and the `contract`
|
|
41
|
-
* desired-state marker (see {@link CONTRACT_MARKER_NAME}).
|
|
42
|
-
*/
|
|
43
|
-
const DB_MARKER_NAME = 'db';
|
|
44
|
-
|
|
45
|
-
export interface MigrationEdgeAnnotation {
|
|
46
|
-
readonly status?: 'applied' | 'pending';
|
|
47
|
-
readonly operationCount?: number;
|
|
48
|
-
readonly invariants?: readonly string[];
|
|
49
|
-
/**
|
|
50
|
-
* Path-highlight annotation for `migrate --show` preview.
|
|
51
|
-
* - `'on-path'`: migration is on the chosen path; rendered in bright green (nodes, hashes, names, lane lines).
|
|
52
|
-
* - `'off-path'`: migration is off the chosen path; fully drawn but in uniform dim grey.
|
|
53
|
-
*/
|
|
54
|
-
readonly pathHighlight?: 'on-path' | 'off-path';
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface RenderMigrationGraphTreeOptions {
|
|
58
|
-
readonly refsByHash?: ReadonlyMap<string, readonly string[]>;
|
|
59
|
-
readonly edgeAnnotationsByHash?: ReadonlyMap<string, MigrationEdgeAnnotation>;
|
|
60
|
-
readonly dbHash?: string;
|
|
61
|
-
readonly contractHash?: string;
|
|
62
|
-
/**
|
|
63
|
-
* Whether this render is for the app space. When false, the `@contract`
|
|
64
|
-
* marker is suppressed — `@contract` is an app-space concept and must not
|
|
65
|
-
* appear in extension spaces (e.g. `pgvector:`). Defaults to `true` so
|
|
66
|
-
* single-space callers that do not pass this option are unaffected.
|
|
67
|
-
*/
|
|
68
|
-
readonly isAppSpace?: boolean;
|
|
69
|
-
readonly activeRefName?: string;
|
|
70
|
-
readonly hashLength?: number;
|
|
71
|
-
readonly globalMaxEdgeTreePrefixWidth?: number;
|
|
72
|
-
readonly globalMaxDirNameWidth?: number;
|
|
73
|
-
readonly colorize: boolean;
|
|
74
|
-
readonly glyphMode?: GlyphMode;
|
|
75
|
-
readonly styler?: MigrationListStyler;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
interface MigrationGraphTreeGlyphPalette {
|
|
79
|
-
readonly node: string;
|
|
80
|
-
readonly arcLand: string;
|
|
81
|
-
readonly arcTee: string;
|
|
82
|
-
readonly verticalPass: string;
|
|
83
|
-
readonly branchTee: string;
|
|
84
|
-
readonly mergeTee: string;
|
|
85
|
-
readonly branchCorner: string;
|
|
86
|
-
readonly mergeCorner: string;
|
|
87
|
-
readonly arcBranchCorner: string;
|
|
88
|
-
readonly arcBranchTee: string;
|
|
89
|
-
readonly arcLandCorner: string;
|
|
90
|
-
readonly arcLandTee: string;
|
|
91
|
-
readonly arcCrossing: string;
|
|
92
|
-
readonly arcLandBridge: string;
|
|
93
|
-
readonly horizontalPass: string;
|
|
94
|
-
readonly connectorBranchTee: string;
|
|
95
|
-
readonly connectorBranchTeeCo: string;
|
|
96
|
-
readonly connectorMergeTeeCo: string;
|
|
97
|
-
readonly edgeArrow: Readonly<Record<MigrationEdgeKind, string>>;
|
|
98
|
-
readonly forwardArrow: string;
|
|
99
|
-
readonly emptySource: string;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const UNICODE_PALETTE: MigrationGraphTreeGlyphPalette = {
|
|
103
|
-
node: '○ ',
|
|
104
|
-
arcLand: '○◂',
|
|
105
|
-
arcTee: '○─',
|
|
106
|
-
verticalPass: '│ ',
|
|
107
|
-
branchTee: '├─',
|
|
108
|
-
mergeTee: '├─',
|
|
109
|
-
branchCorner: '╮ ',
|
|
110
|
-
mergeCorner: '╯ ',
|
|
111
|
-
arcBranchCorner: '╮ ',
|
|
112
|
-
arcBranchTee: '┬─',
|
|
113
|
-
arcLandCorner: '╯ ',
|
|
114
|
-
arcLandTee: '┴─',
|
|
115
|
-
arcCrossing: '┼─',
|
|
116
|
-
arcLandBridge: '──',
|
|
117
|
-
horizontalPass: '──',
|
|
118
|
-
connectorBranchTee: '├─',
|
|
119
|
-
connectorBranchTeeCo: '┬─',
|
|
120
|
-
connectorMergeTeeCo: '┴─',
|
|
121
|
-
edgeArrow: { forward: '↑', rollback: '↓', self: '⟲' },
|
|
122
|
-
forwardArrow: migrationListForwardArrow('unicode'),
|
|
123
|
-
emptySource: migrationListEmptySource('unicode'),
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
const ASCII_PALETTE: MigrationGraphTreeGlyphPalette = {
|
|
127
|
-
node: '* ',
|
|
128
|
-
arcLand: '*<',
|
|
129
|
-
arcTee: '*-',
|
|
130
|
-
verticalPass: '| ',
|
|
131
|
-
branchTee: '+-',
|
|
132
|
-
mergeTee: '+-',
|
|
133
|
-
branchCorner: '\\ ',
|
|
134
|
-
mergeCorner: '/ ',
|
|
135
|
-
arcBranchCorner: '\\ ',
|
|
136
|
-
arcBranchTee: '+-',
|
|
137
|
-
arcLandCorner: '/ ',
|
|
138
|
-
arcLandTee: '+-',
|
|
139
|
-
arcCrossing: '+-',
|
|
140
|
-
arcLandBridge: '--',
|
|
141
|
-
horizontalPass: '--',
|
|
142
|
-
connectorBranchTee: '+-',
|
|
143
|
-
connectorBranchTeeCo: '+-',
|
|
144
|
-
connectorMergeTeeCo: '+-',
|
|
145
|
-
edgeArrow: { forward: '^', rollback: 'v', self: '@' },
|
|
146
|
-
forwardArrow: migrationListForwardArrow('ascii'),
|
|
147
|
-
emptySource: migrationListEmptySource('ascii'),
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
function paletteFor(mode: GlyphMode): MigrationGraphTreeGlyphPalette {
|
|
151
|
-
return mode === 'ascii' ? ASCII_PALETTE : UNICODE_PALETTE;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function overlayStatusGlyphs(mode: GlyphMode): {
|
|
155
|
-
readonly applied: string;
|
|
156
|
-
readonly pending: string;
|
|
157
|
-
} {
|
|
158
|
-
return mode === 'ascii' ? { applied: '+', pending: '>' } : { applied: '✓', pending: '⧗' };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function arrowForEdgeKind(
|
|
162
|
-
kind: MigrationEdgeKind,
|
|
163
|
-
palette: MigrationGraphTreeGlyphPalette,
|
|
164
|
-
): string {
|
|
165
|
-
return palette.edgeArrow[kind];
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Forced-color functions that always emit ANSI regardless of the ambient TTY
|
|
170
|
-
* environment (NO_COLOR, piped output). Used for:
|
|
171
|
-
*
|
|
172
|
-
* - `forcedBold`: branch-coloured migration names pair their lane hue with bold;
|
|
173
|
-
* both must emit so the name is deterministically bold + hue.
|
|
174
|
-
* - `forcedDim`: off-path path-highlight override (migrate --show).
|
|
175
|
-
* The renderer gates this behind `opts.colorize`; the forced variant ensures
|
|
176
|
-
* ANSI is emitted in controlled environments (e.g. tests with `NO_COLOR=1`)
|
|
177
|
-
* when the caller explicitly requests colour. Without forcing, `dim()` from
|
|
178
|
-
* the ambient module-level import no-ops under NO_COLOR, making the
|
|
179
|
-
* path-highlight unreachable in tests.
|
|
180
|
-
*/
|
|
181
|
-
const {
|
|
182
|
-
bold: forcedBold,
|
|
183
|
-
dim: forcedDim,
|
|
184
|
-
greenBright: forcedGreen,
|
|
185
|
-
} = createColors({ useColor: true });
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* The two styles used in `migrate --show` path-highlight mode.
|
|
189
|
-
*
|
|
190
|
-
* In path-highlight mode the normal by-branch rotating-colour logic
|
|
191
|
-
* (`LANE_COLOR_CYCLE` / `laneStylerForColumn`) is suppressed entirely.
|
|
192
|
-
* Every glyph, name, and hash is styled by its on-path / off-path role,
|
|
193
|
-
* never by lane column index.
|
|
194
|
-
*
|
|
195
|
-
* - `onPath`: neutral single-path style — exactly how a linear (no-branch)
|
|
196
|
-
* section renders today. Lane glyphs are dim, names are bold, hashes use
|
|
197
|
-
* the default `sourceHash`/`destHash` colours. No rotation hue is applied.
|
|
198
|
-
* This is identical to how the pgvector single-path section renders.
|
|
199
|
-
* - `offPath`: uniform dim grey on every cell (name, hashes, lane glyphs,
|
|
200
|
-
* direction arrows).
|
|
201
|
-
*
|
|
202
|
-
* To change the on-path or off-path colour in future, edit this object only.
|
|
203
|
-
*/
|
|
204
|
-
export const PATH_HIGHLIGHT_STYLES = {
|
|
205
|
-
/**
|
|
206
|
-
* Lane/glyph/arrow stylers for on-path cells.
|
|
207
|
-
*
|
|
208
|
-
* - lane: `forcedGreen` when colour is on — bright green so the on-path
|
|
209
|
-
* branch glyphs (`│ ├ ╯ ↑`) and node markers (`○`/`∅`) are visually
|
|
210
|
-
* distinct from off-path (dim grey). Uses forced ANSI so it survives
|
|
211
|
-
* NO_COLOR in tests. Identity when `colorize` is false.
|
|
212
|
-
* - arrow: identity (plain, no colouring)
|
|
213
|
-
* - dirName: `bold` (ambient bold — name stays white/bold, not green)
|
|
214
|
-
* - hashOverride: undefined — `style.sourceHash`/`style.destHash` apply
|
|
215
|
-
* normally (cyan) so hashes keep their existing neutral colour.
|
|
216
|
-
*
|
|
217
|
-
* `style` is the same `MigrationListStyler` the tree renderer uses.
|
|
218
|
-
* Rotation (`LANE_COLOR_CYCLE`) is never applied to on-path cells.
|
|
219
|
-
*/
|
|
220
|
-
onPath: (_style: MigrationListStyler, colorize: boolean) => ({
|
|
221
|
-
lane: colorize ? forcedGreen : (text: string) => text,
|
|
222
|
-
arrow: (text: string) => text,
|
|
223
|
-
dirName: (text: string) => bold(text),
|
|
224
|
-
hashOverride: undefined,
|
|
225
|
-
}),
|
|
226
|
-
/**
|
|
227
|
-
* Lane/glyph/arrow/hash stylers for off-path cells.
|
|
228
|
-
* Uniform dim grey on everything — uses `forcedDim` so ANSI is emitted even
|
|
229
|
-
* under NO_COLOR (test environments use `colorize:true` + NO_COLOR=1 to verify dim).
|
|
230
|
-
* Returns identity functions when colour is off (`colorize: false`).
|
|
231
|
-
*/
|
|
232
|
-
offPath: (colorize: boolean) => ({
|
|
233
|
-
lane: colorize ? forcedDim : (text: string) => text,
|
|
234
|
-
arrow: colorize ? forcedDim : (text: string) => text,
|
|
235
|
-
dirName: colorize ? forcedDim : (text: string) => text,
|
|
236
|
-
hashOverride: colorize ? forcedDim : undefined,
|
|
237
|
-
}),
|
|
238
|
-
} as const;
|
|
239
|
-
|
|
240
|
-
function laneStylerForColumn(
|
|
241
|
-
colorColumn: number,
|
|
242
|
-
colorize: boolean,
|
|
243
|
-
style: MigrationListStyler,
|
|
244
|
-
): (text: string) => string {
|
|
245
|
-
return stylerForLaneColumn(colorColumn, colorize, style.lane);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Tint a branch-owned token (direction arrow, migration name) by its edge's
|
|
250
|
-
* lane so the whole branch row reads in one colour. Column 0 has nothing to be
|
|
251
|
-
* told apart from in the common linear chain, so it keeps the token's existing
|
|
252
|
-
* default styling (`fallback`) rather than a palette hue; only lanes ≥ 1 take a
|
|
253
|
-
* colour. With colour off, the fallback (also colourless) is used unchanged.
|
|
254
|
-
*/
|
|
255
|
-
function branchStylerOrDefault(
|
|
256
|
-
column: number,
|
|
257
|
-
colorize: boolean,
|
|
258
|
-
fallback: (text: string) => string,
|
|
259
|
-
): (text: string) => string {
|
|
260
|
-
if (!colorize || column <= NEUTRAL_LANE_COLUMN) {
|
|
261
|
-
return fallback;
|
|
262
|
-
}
|
|
263
|
-
return laneColorForColumn(column);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Render a crossing tee (`┼─`): the junction stays dim/neutral so neither arc
|
|
268
|
-
* steals the cell; the trailing dash takes the served lane hue.
|
|
269
|
-
*/
|
|
270
|
-
function renderArcCrossing(
|
|
271
|
-
pair: string,
|
|
272
|
-
dashColumn: number,
|
|
273
|
-
colorize: boolean,
|
|
274
|
-
style: MigrationListStyler,
|
|
275
|
-
): string {
|
|
276
|
-
const junction = colorize ? style.lane : (text: string) => text;
|
|
277
|
-
const dash = laneStylerForColumn(dashColumn, colorize, style);
|
|
278
|
-
return junction(pair.slice(0, 1)) + dash(pair.slice(1));
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Render a connector tee (`├─` / `┬─` / `┴─`) with its junction glyph and its
|
|
283
|
-
* trailing dash coloured independently: the junction anchors its own lane while
|
|
284
|
-
* the dash leads into the branch on its right.
|
|
285
|
-
*/
|
|
286
|
-
function renderConnectorTee(
|
|
287
|
-
pair: string,
|
|
288
|
-
glyphColumn: number,
|
|
289
|
-
dashColumn: number,
|
|
290
|
-
colorize: boolean,
|
|
291
|
-
style: MigrationListStyler,
|
|
292
|
-
): string {
|
|
293
|
-
const glyph = laneStylerForColumn(glyphColumn, colorize, style);
|
|
294
|
-
if (glyphColumn === dashColumn) {
|
|
295
|
-
return glyph(pair);
|
|
296
|
-
}
|
|
297
|
-
return glyph(pair.slice(0, 1)) + laneStylerForColumn(dashColumn, colorize, style)(pair.slice(1));
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* A node-marker glyph pair (`○◂`, `○─`, `*<`, `*-`) is the contract node
|
|
302
|
-
* marker (`○` / `*`) followed by an arc connector (`◂` / `─` / `<` / `-`). The
|
|
303
|
-
* marker takes its own lane's hue (so each node visibly belongs to its branch);
|
|
304
|
-
* the connector follows the arc it belongs to (its owning back-lane hue).
|
|
305
|
-
* Direction arrows are handled elsewhere — they take their edge's lane hue too.
|
|
306
|
-
*
|
|
307
|
-
* When `laneOverride` is provided (for path-highlight rows), it replaces the
|
|
308
|
-
* marker styler. `arcLaneOverride` (if provided) replaces the connector styler
|
|
309
|
-
* independently — this matters when the node is on-path but the arc belongs to
|
|
310
|
-
* an off-path rollback edge, which must render dim rather than green.
|
|
311
|
-
*/
|
|
312
|
-
function renderNodeMarkerPair(
|
|
313
|
-
pair: string,
|
|
314
|
-
nodeColumn: number,
|
|
315
|
-
arcColumn: number,
|
|
316
|
-
colorize: boolean,
|
|
317
|
-
style: MigrationListStyler,
|
|
318
|
-
laneOverride?: (text: string) => string,
|
|
319
|
-
arcLaneOverride?: (text: string) => string,
|
|
320
|
-
): string {
|
|
321
|
-
const marker = laneOverride ?? laneStylerForColumn(nodeColumn, colorize, style);
|
|
322
|
-
const connector =
|
|
323
|
-
arcLaneOverride ?? laneOverride ?? laneStylerForColumn(arcColumn, colorize, style);
|
|
324
|
-
return marker(pair.slice(0, 1)) + connector(pair.slice(1));
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function renderCellPair(
|
|
328
|
-
cell: StructuralCell,
|
|
329
|
-
column: number,
|
|
330
|
-
colors: RowArcLaneColors,
|
|
331
|
-
colorize: boolean,
|
|
332
|
-
style: MigrationListStyler,
|
|
333
|
-
palette: MigrationGraphTreeGlyphPalette,
|
|
334
|
-
laneOverride?: (text: string) => string,
|
|
335
|
-
arrowOverride?: (text: string) => string,
|
|
336
|
-
arcLaneOverride?: (text: string) => string,
|
|
337
|
-
): string {
|
|
338
|
-
const laneColumn = colors.lane[column] ?? column;
|
|
339
|
-
// In path-highlight mode (`laneOverride` present), the rotating lane colour is
|
|
340
|
-
// bypassed entirely — the override applies to every structural glyph. Without an
|
|
341
|
-
// override (normal graph/status/list mode), the existing rotation logic applies.
|
|
342
|
-
const lane = laneOverride ?? laneStylerForColumn(laneColumn, colorize, style);
|
|
343
|
-
// `arrowOverride` is used only for the direction arrow on edge-lane cells.
|
|
344
|
-
// When absent, the normal `branchStylerOrDefault` logic applies (rotation for lanes ≥ 1).
|
|
345
|
-
// In path-highlight mode it is always set alongside `laneOverride`.
|
|
346
|
-
const arrow =
|
|
347
|
-
arrowOverride ?? ((text: string) => branchStylerOrDefault(column, colorize, style.kind)(text));
|
|
348
|
-
switch (cell.kind) {
|
|
349
|
-
case 'node': {
|
|
350
|
-
const arcColumn = colors.connector[column] ?? NEUTRAL_LANE_COLUMN;
|
|
351
|
-
if (cell.arcLand === true) {
|
|
352
|
-
return renderNodeMarkerPair(
|
|
353
|
-
palette.arcLand,
|
|
354
|
-
column,
|
|
355
|
-
arcColumn,
|
|
356
|
-
colorize,
|
|
357
|
-
style,
|
|
358
|
-
laneOverride,
|
|
359
|
-
arcLaneOverride,
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
if (cell.arcTee === true) {
|
|
363
|
-
return renderNodeMarkerPair(
|
|
364
|
-
palette.arcTee,
|
|
365
|
-
column,
|
|
366
|
-
arcColumn,
|
|
367
|
-
colorize,
|
|
368
|
-
style,
|
|
369
|
-
laneOverride,
|
|
370
|
-
arcLaneOverride,
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
return lane(palette.node);
|
|
374
|
-
}
|
|
375
|
-
case 'vertical-pass':
|
|
376
|
-
return lane(palette.verticalPass);
|
|
377
|
-
case 'edge-lane':
|
|
378
|
-
return cell.ownsLabel
|
|
379
|
-
? lane(palette.verticalPass.trimEnd()) + arrow(arrowForEdgeKind(cell.edgeKind, palette))
|
|
380
|
-
: lane(palette.verticalPass);
|
|
381
|
-
case 'branch-tee':
|
|
382
|
-
return lane(palette.branchTee);
|
|
383
|
-
case 'merge-tee':
|
|
384
|
-
return lane(palette.mergeTee);
|
|
385
|
-
case 'branch-corner':
|
|
386
|
-
return lane(palette.branchCorner);
|
|
387
|
-
case 'merge-corner':
|
|
388
|
-
return lane(palette.mergeCorner);
|
|
389
|
-
case 'arc-branch-corner':
|
|
390
|
-
return lane(palette.arcBranchCorner);
|
|
391
|
-
case 'arc-branch-tee':
|
|
392
|
-
return lane(palette.arcBranchTee);
|
|
393
|
-
case 'arc-land-corner':
|
|
394
|
-
return lane(palette.arcLandCorner);
|
|
395
|
-
case 'arc-land-tee':
|
|
396
|
-
// When a lane override is active, apply it uniformly to both glyph and dash parts
|
|
397
|
-
// so neither part emits a rotation hue.
|
|
398
|
-
return laneOverride !== undefined
|
|
399
|
-
? laneOverride(palette.arcLandTee)
|
|
400
|
-
: renderConnectorTee(
|
|
401
|
-
palette.arcLandTee,
|
|
402
|
-
laneColumn,
|
|
403
|
-
colors.dash[column] ?? laneColumn,
|
|
404
|
-
colorize,
|
|
405
|
-
style,
|
|
406
|
-
);
|
|
407
|
-
case 'arc-crossing':
|
|
408
|
-
return lane(palette.arcLandBridge);
|
|
409
|
-
case 'arc-land-bridge':
|
|
410
|
-
return lane(palette.arcLandBridge);
|
|
411
|
-
case 'horizontal-pass':
|
|
412
|
-
return lane(palette.horizontalPass);
|
|
413
|
-
case 'empty':
|
|
414
|
-
return ' ';
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* Render a branch-connector or merge-connector row.
|
|
420
|
-
*
|
|
421
|
-
* `columnLaneOverride` is an optional per-column map populated when path-highlight
|
|
422
|
-
* annotations are active (`migrate --show`). For each column in the connector's
|
|
423
|
-
* lane range, the map supplies the override styler (dim for off-path) that should
|
|
424
|
-
* replace the normal rotating-lane colour for that column. Columns absent from the
|
|
425
|
-
* map (on-path or unannotated) use the standard `laneStylerForColumn` logic unchanged.
|
|
426
|
-
* This ensures off-path branch connectors appear dim rather than in their rotation
|
|
427
|
-
* colour (e.g. magenta).
|
|
428
|
-
*/
|
|
429
|
-
function renderConnectorRow(
|
|
430
|
-
row: MigrationGraphGridRow,
|
|
431
|
-
gridWidth: number,
|
|
432
|
-
colorize: boolean,
|
|
433
|
-
style: MigrationListStyler,
|
|
434
|
-
palette: MigrationGraphTreeGlyphPalette,
|
|
435
|
-
columnLaneOverride?: ReadonlyMap<number, (text: string) => string>,
|
|
436
|
-
): string {
|
|
437
|
-
const resolvedLane = (column: number): ((text: string) => string) =>
|
|
438
|
-
columnLaneOverride?.get(column) ?? laneStylerForColumn(column, colorize, style);
|
|
439
|
-
|
|
440
|
-
const isMerge = row.kind === 'merge-connector';
|
|
441
|
-
if (row.cells.length > 0) {
|
|
442
|
-
const colors = resolveConnectorLaneColors(row.cells, row.startLane ?? 0);
|
|
443
|
-
let seenTee = false;
|
|
444
|
-
let out = '';
|
|
445
|
-
for (let column = 0; column < row.cells.length; column++) {
|
|
446
|
-
const cell = row.cells[column];
|
|
447
|
-
if (cell === undefined) continue;
|
|
448
|
-
const glyphColumn = colors.glyph[column] ?? column;
|
|
449
|
-
const dashColumn = colors.dash[column] ?? glyphColumn;
|
|
450
|
-
const override = columnLaneOverride?.get(glyphColumn);
|
|
451
|
-
// In path-highlight mode, the dash column's override is used for the trailing dash
|
|
452
|
-
// even when the glyph column has no override. This handles branch-tee cells whose
|
|
453
|
-
// migrationHash is undefined (no previous edge occupied that lane) — the tee's dash
|
|
454
|
-
// belongs to the connector run and should follow the corner's annotation.
|
|
455
|
-
const dashOverrideForPathHighlight = columnLaneOverride?.get(dashColumn) ?? override;
|
|
456
|
-
if (
|
|
457
|
-
override !== undefined ||
|
|
458
|
-
(columnLaneOverride !== undefined && dashOverrideForPathHighlight !== undefined)
|
|
459
|
-
) {
|
|
460
|
-
// When an override is active for this column (or when a dash override is available
|
|
461
|
-
// via the connected corner), apply the glyph column's override to the junction glyph
|
|
462
|
-
// (├/┬/┴), and the dash column's override to the trailing dash.
|
|
463
|
-
// This matters for merge/branch connectors: the on-path trunk's tee (├) is green
|
|
464
|
-
// while the dash (─) and corner (╯) bridging to an OFF-path column are dim.
|
|
465
|
-
// For non-tee cells (corner, pass, crossing), the single-column override is fine.
|
|
466
|
-
const effectiveOverride = override ?? dashOverrideForPathHighlight;
|
|
467
|
-
if (effectiveOverride === undefined) {
|
|
468
|
-
out += ' ';
|
|
469
|
-
continue;
|
|
470
|
-
}
|
|
471
|
-
switch (cell.kind) {
|
|
472
|
-
case 'branch-tee':
|
|
473
|
-
case 'merge-tee': {
|
|
474
|
-
const pair = seenTee ? palette.connectorBranchTeeCo : palette.connectorBranchTee;
|
|
475
|
-
// Both the junction glyph and its trailing dash belong to this tee cell's
|
|
476
|
-
// own edge — use effectiveOverride for both so an off-path tee's dash is dim
|
|
477
|
-
// even when the next column (dashColumn) belongs to an on-path edge.
|
|
478
|
-
out += effectiveOverride(pair.slice(0, 1)) + effectiveOverride(pair.slice(1));
|
|
479
|
-
seenTee = true;
|
|
480
|
-
break;
|
|
481
|
-
}
|
|
482
|
-
case 'branch-corner':
|
|
483
|
-
out += effectiveOverride(palette.branchCorner);
|
|
484
|
-
break;
|
|
485
|
-
case 'merge-corner':
|
|
486
|
-
out += effectiveOverride(palette.mergeCorner);
|
|
487
|
-
break;
|
|
488
|
-
case 'vertical-pass':
|
|
489
|
-
out += effectiveOverride(palette.verticalPass);
|
|
490
|
-
break;
|
|
491
|
-
case 'horizontal-pass':
|
|
492
|
-
out += effectiveOverride(palette.horizontalPass);
|
|
493
|
-
break;
|
|
494
|
-
case 'arc-crossing': {
|
|
495
|
-
// The junction glyph (┼) belongs to the vertical lane (effectiveOverride).
|
|
496
|
-
// The trailing dash (─) runs horizontally into the next column — it belongs
|
|
497
|
-
// to that column's owner (dashColumn). Use the dash column's override so an
|
|
498
|
-
// off-path horizontal continuation is dim even when the crossing is on-path.
|
|
499
|
-
const arcCrossingDashOverride =
|
|
500
|
-
columnLaneOverride?.get(dashColumn) ?? effectiveOverride;
|
|
501
|
-
out +=
|
|
502
|
-
effectiveOverride(palette.arcCrossing.slice(0, 1)) +
|
|
503
|
-
arcCrossingDashOverride(palette.arcCrossing.slice(1));
|
|
504
|
-
break;
|
|
505
|
-
}
|
|
506
|
-
default:
|
|
507
|
-
out += ' ';
|
|
508
|
-
}
|
|
509
|
-
continue;
|
|
510
|
-
}
|
|
511
|
-
const lane = laneStylerForColumn(glyphColumn, colorize, style);
|
|
512
|
-
switch (cell.kind) {
|
|
513
|
-
case 'branch-tee':
|
|
514
|
-
out += renderConnectorTee(
|
|
515
|
-
seenTee ? palette.connectorBranchTeeCo : palette.connectorBranchTee,
|
|
516
|
-
glyphColumn,
|
|
517
|
-
dashColumn,
|
|
518
|
-
colorize,
|
|
519
|
-
style,
|
|
520
|
-
);
|
|
521
|
-
seenTee = true;
|
|
522
|
-
break;
|
|
523
|
-
case 'merge-tee':
|
|
524
|
-
out += renderConnectorTee(
|
|
525
|
-
seenTee ? palette.connectorMergeTeeCo : palette.connectorBranchTee,
|
|
526
|
-
glyphColumn,
|
|
527
|
-
dashColumn,
|
|
528
|
-
colorize,
|
|
529
|
-
style,
|
|
530
|
-
);
|
|
531
|
-
seenTee = true;
|
|
532
|
-
break;
|
|
533
|
-
case 'branch-corner':
|
|
534
|
-
out += lane(palette.branchCorner);
|
|
535
|
-
break;
|
|
536
|
-
case 'merge-corner':
|
|
537
|
-
out += lane(palette.mergeCorner);
|
|
538
|
-
break;
|
|
539
|
-
case 'vertical-pass':
|
|
540
|
-
out += lane(palette.verticalPass);
|
|
541
|
-
break;
|
|
542
|
-
case 'horizontal-pass':
|
|
543
|
-
out += lane(palette.horizontalPass);
|
|
544
|
-
break;
|
|
545
|
-
case 'arc-crossing':
|
|
546
|
-
out += renderArcCrossing(palette.arcCrossing, dashColumn, colorize, style);
|
|
547
|
-
break;
|
|
548
|
-
default:
|
|
549
|
-
out += ' ';
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
// The cells array is sized to the grid width at emit time; a back-arc lane
|
|
553
|
-
// allocated by a later row can push the grid wider afterwards, so pad any
|
|
554
|
-
// trailing columns rather than dropping the lanes that pass through here.
|
|
555
|
-
for (let column = row.cells.length; column < gridWidth; column++) {
|
|
556
|
-
out += ' ';
|
|
557
|
-
}
|
|
558
|
-
return out;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
const start = row.startLane ?? 0;
|
|
562
|
-
const end = row.endLane ?? start;
|
|
563
|
-
// The whole fork/merge run reads as one line in the served lane's hue (the
|
|
564
|
-
// corner it reaches); pass-through columns outside the run keep their own.
|
|
565
|
-
const runLane = resolvedLane(end);
|
|
566
|
-
let out = '';
|
|
567
|
-
for (let column = 0; column < gridWidth; column++) {
|
|
568
|
-
if (column < start || column > end) out += ' ';
|
|
569
|
-
else if (column === start) out += runLane(palette.connectorBranchTee);
|
|
570
|
-
else if (column === end) out += runLane(isMerge ? palette.mergeCorner : palette.branchCorner);
|
|
571
|
-
else out += runLane(isMerge ? palette.connectorMergeTeeCo : palette.connectorBranchTeeCo);
|
|
572
|
-
}
|
|
573
|
-
return out;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function abbreviateHash(hash: string, hashLength: number, emptySource: string): string {
|
|
577
|
-
if (hash === EMPTY_CONTRACT_HASH) {
|
|
578
|
-
return emptySource;
|
|
579
|
-
}
|
|
580
|
-
const stripped = hash.startsWith('sha256:') ? hash.slice(7) : hash;
|
|
581
|
-
return stripped.slice(0, hashLength);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
const MIN_HASH_DATA_COLUMN = 25;
|
|
585
|
-
|
|
586
|
-
interface ContractOverlayNames {
|
|
587
|
-
readonly markers: readonly string[];
|
|
588
|
-
readonly refs: readonly string[];
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
function overlayNamesForContract(
|
|
592
|
-
contractHash: string,
|
|
593
|
-
opts: RenderMigrationGraphTreeOptions,
|
|
594
|
-
): ContractOverlayNames {
|
|
595
|
-
const markers: string[] = [];
|
|
596
|
-
const refs: string[] = [];
|
|
597
|
-
const userRefs = opts.refsByHash?.get(contractHash);
|
|
598
|
-
if (userRefs) {
|
|
599
|
-
refs.push(...[...userRefs].sort((a, b) => a.localeCompare(b)));
|
|
600
|
-
}
|
|
601
|
-
if (
|
|
602
|
-
opts.isAppSpace !== false &&
|
|
603
|
-
opts.contractHash === contractHash &&
|
|
604
|
-
contractHash !== EMPTY_CONTRACT_HASH
|
|
605
|
-
) {
|
|
606
|
-
markers.push(CONTRACT_MARKER_NAME);
|
|
607
|
-
}
|
|
608
|
-
if (opts.dbHash === contractHash) {
|
|
609
|
-
markers.push(DB_MARKER_NAME);
|
|
610
|
-
}
|
|
611
|
-
markers.sort((a, b) => {
|
|
612
|
-
if (a === CONTRACT_MARKER_NAME) {
|
|
613
|
-
return -1;
|
|
614
|
-
}
|
|
615
|
-
if (b === CONTRACT_MARKER_NAME) {
|
|
616
|
-
return 1;
|
|
617
|
-
}
|
|
618
|
-
return a.localeCompare(b);
|
|
619
|
-
});
|
|
620
|
-
return { markers, refs };
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
function createTreeStyler(opts: RenderMigrationGraphTreeOptions): MigrationListStyler {
|
|
624
|
-
const base = opts.styler ?? createAnsiMigrationListStyler({ useColor: opts.colorize });
|
|
625
|
-
const activeRefName = opts.activeRefName;
|
|
626
|
-
if (!opts.colorize || activeRefName === undefined) {
|
|
627
|
-
return base;
|
|
628
|
-
}
|
|
629
|
-
return {
|
|
630
|
-
...base,
|
|
631
|
-
refs: (names) => {
|
|
632
|
-
const styledNames = names.map((name) => (name === activeRefName ? bold(name) : name));
|
|
633
|
-
return base.refs(styledNames);
|
|
634
|
-
},
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
function formatEdgeAnnotationSuffix(
|
|
639
|
-
migrationHash: string,
|
|
640
|
-
opts: RenderMigrationGraphTreeOptions,
|
|
641
|
-
style: MigrationListStyler,
|
|
642
|
-
): string {
|
|
643
|
-
const annotation = opts.edgeAnnotationsByHash?.get(migrationHash);
|
|
644
|
-
if (annotation === undefined) {
|
|
645
|
-
return '';
|
|
646
|
-
}
|
|
647
|
-
const isOffPath = annotation.pathHighlight === 'off-path';
|
|
648
|
-
const segments: string[] = [];
|
|
649
|
-
if (annotation.operationCount !== undefined) {
|
|
650
|
-
segments.push(`${annotation.operationCount} ops`);
|
|
651
|
-
}
|
|
652
|
-
if (annotation.invariants !== undefined && annotation.invariants.length > 0) {
|
|
653
|
-
segments.push(style.invariants(annotation.invariants));
|
|
654
|
-
}
|
|
655
|
-
const status = annotation.status;
|
|
656
|
-
if (status !== undefined) {
|
|
657
|
-
const glyphs = overlayStatusGlyphs(opts.glyphMode ?? 'unicode');
|
|
658
|
-
const glyph = status === 'applied' ? glyphs.applied : glyphs.pending;
|
|
659
|
-
const label = status === 'applied' ? 'applied' : 'pending';
|
|
660
|
-
if (!opts.colorize) {
|
|
661
|
-
segments.push(`${glyph} ${label}`);
|
|
662
|
-
} else {
|
|
663
|
-
const styler = status === 'applied' ? green : yellow;
|
|
664
|
-
segments.push(styler(`${glyph} ${label}`));
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
if (annotation.pathHighlight === 'on-path') {
|
|
668
|
-
const glyph = opts.glyphMode === 'ascii' ? '>' : '↑';
|
|
669
|
-
segments.push(`${glyph} will run`);
|
|
670
|
-
}
|
|
671
|
-
if (segments.length === 0) {
|
|
672
|
-
return '';
|
|
673
|
-
}
|
|
674
|
-
const suffix = ` ${segments.join(' ')}`;
|
|
675
|
-
return opts.colorize && isOffPath ? forcedDim(suffix) : suffix;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
/**
|
|
679
|
-
* Format the `from → to` hash data column for an edge row.
|
|
680
|
-
*
|
|
681
|
-
* When `hashOverride` is provided (off-path → `dim`), it replaces ALL sub-stylers
|
|
682
|
-
* (`sourceHash`, `destHash`, arrow `glyph`) so dim reaches every character without
|
|
683
|
-
* inner ANSI codes (e.g. the dim+cyan of `sourceHash`) overriding it. On-path edges
|
|
684
|
-
* carry no override. Without an override, the normal `style` sub-stylers apply.
|
|
685
|
-
*/
|
|
686
|
-
function formatEdgeHashColumn(
|
|
687
|
-
edge: ClassifiedEdge,
|
|
688
|
-
style: MigrationListStyler,
|
|
689
|
-
hashLength: number,
|
|
690
|
-
palette: MigrationGraphTreeGlyphPalette,
|
|
691
|
-
hashOverride?: (text: string) => string,
|
|
692
|
-
): string {
|
|
693
|
-
const src = hashOverride ?? style.sourceHash;
|
|
694
|
-
const dst = hashOverride ?? style.destHash;
|
|
695
|
-
const glyph = hashOverride ?? style.glyph;
|
|
696
|
-
if (edge.kind === 'self') {
|
|
697
|
-
const hash = abbreviateHash(edge.from, hashLength, palette.emptySource);
|
|
698
|
-
const source = padFromHashColumn(src(hash), hashLength);
|
|
699
|
-
return `${source} ${glyph(palette.forwardArrow)} ${dst(hash)}`;
|
|
700
|
-
}
|
|
701
|
-
const source =
|
|
702
|
-
edge.from === EMPTY_CONTRACT_HASH
|
|
703
|
-
? padFromHashColumn(glyph(palette.emptySource), hashLength)
|
|
704
|
-
: padFromHashColumn(
|
|
705
|
-
src(abbreviateHash(edge.from, hashLength, palette.emptySource)),
|
|
706
|
-
hashLength,
|
|
707
|
-
);
|
|
708
|
-
const arrow = glyph(palette.forwardArrow);
|
|
709
|
-
const dest = dst(abbreviateHash(edge.to, hashLength, palette.emptySource));
|
|
710
|
-
return `${source} ${arrow} ${dest}`;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
function padVisible(text: string, targetWidth: number): string {
|
|
714
|
-
const padding = Math.max(0, targetWidth - stringWidth(text));
|
|
715
|
-
return text + ' '.repeat(padding);
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
const ANSI_ESCAPE = '\x1b';
|
|
719
|
-
|
|
720
|
-
function trimTrailingWhitespace(line: string): string {
|
|
721
|
-
const trailingSpaceBeforeReset = new RegExp(`[\\t ]+((?:${ANSI_ESCAPE}\\[[0-9;]*m)+)$`);
|
|
722
|
-
return line.replace(trailingSpaceBeforeReset, '$1').replace(/\s+$/, '');
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
function gridWidthForModel(rows: readonly MigrationGraphGridRow[]): number {
|
|
726
|
-
return rows.reduce(
|
|
727
|
-
(max, row) =>
|
|
728
|
-
row.kind === 'node' || row.kind === 'edge' ? Math.max(max, row.cells.length) : max,
|
|
729
|
-
1,
|
|
730
|
-
);
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
function maxDirNameLength(edges: readonly ClassifiedEdge[]): number {
|
|
734
|
-
if (edges.length === 0) return 0;
|
|
735
|
-
return Math.max(...edges.map((edge) => edge.dirName.length));
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
function rowDirNameWidth(labelColumn: number, maxDirNameLen: number, dirNameGap: number): number {
|
|
739
|
-
return Math.max(maxDirNameLen + dirNameGap, MIN_HASH_DATA_COLUMN - labelColumn);
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
function gridUsesSkipRollbackArcs(rows: readonly MigrationGraphGridRow[]): boolean {
|
|
743
|
-
return rows.some((row) =>
|
|
744
|
-
row.cells.some(
|
|
745
|
-
(cell) => cell.kind === 'edge-lane' && cell.adjacency === 'node-skipping-rollback',
|
|
746
|
-
),
|
|
747
|
-
);
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
function edgeLabelColumn(row: MigrationGraphGridRow, wideLabelColumn: number | undefined): number {
|
|
751
|
-
if (wideLabelColumn !== undefined) {
|
|
752
|
-
return wideLabelColumn;
|
|
753
|
-
}
|
|
754
|
-
const laneIndex = row.laneIndex ?? 0;
|
|
755
|
-
if (row.edge?.from === EMPTY_CONTRACT_HASH && laneIndex === 0) {
|
|
756
|
-
return (laneIndex + 1) * 2 + LABEL_GAP;
|
|
757
|
-
}
|
|
758
|
-
const usesFullRowGutter = row.cells.some(
|
|
759
|
-
(cell, index) => index > laneIndex && cell.kind === 'vertical-pass',
|
|
760
|
-
);
|
|
761
|
-
return usesFullRowGutter ? row.cells.length * 2 + LABEL_GAP : (laneIndex + 1) * 2 + LABEL_GAP;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
function maxEdgeTreePrefixWidth(
|
|
765
|
-
rows: readonly MigrationGraphGridRow[],
|
|
766
|
-
wideLabelColumn: number | undefined,
|
|
767
|
-
): number {
|
|
768
|
-
let max = 0;
|
|
769
|
-
for (const row of rows) {
|
|
770
|
-
if (row.kind !== 'edge' || row.edge === undefined) continue;
|
|
771
|
-
max = Math.max(max, edgeLabelColumn(row, wideLabelColumn));
|
|
772
|
-
}
|
|
773
|
-
return max;
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
export function computeMaxEdgeTreePrefixWidthForLayout(model: MigrationGraphGridModel): number {
|
|
777
|
-
const wideLabelColumn = gridUsesSkipRollbackArcs(model.rows)
|
|
778
|
-
? gridWidthForModel(model.rows) * 2 + 4
|
|
779
|
-
: undefined;
|
|
780
|
-
return maxEdgeTreePrefixWidth(model.rows, wideLabelColumn);
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
export function computeMaxDirNameLengthForLayout(model: MigrationGraphGridModel): number {
|
|
784
|
-
const allEdges = model.rows
|
|
785
|
-
.filter(
|
|
786
|
-
(row): row is MigrationGraphGridRow & { edge: ClassifiedEdge } =>
|
|
787
|
-
row.kind === 'edge' && row.edge !== undefined,
|
|
788
|
-
)
|
|
789
|
-
.map((row) => row.edge);
|
|
790
|
-
return maxDirNameLength(allEdges);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
function nodeHasArcDecoration(row: MigrationGraphGridRow): boolean {
|
|
794
|
-
return row.cells.some(
|
|
795
|
-
(cell) => cell.kind === 'node' && (cell.arcTee === true || cell.arcLand === true),
|
|
796
|
-
);
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
export function renderMigrationGraphTree(
|
|
800
|
-
model: MigrationGraphGridModel,
|
|
801
|
-
opts: RenderMigrationGraphTreeOptions,
|
|
802
|
-
): string {
|
|
803
|
-
const glyphMode = opts.glyphMode ?? 'unicode';
|
|
804
|
-
const palette = paletteFor(glyphMode);
|
|
805
|
-
const style = createTreeStyler(opts);
|
|
806
|
-
const hashLength = opts.hashLength ?? MIGRATION_LIST_HASH_WIDTH;
|
|
807
|
-
const gridWidth = gridWidthForModel(model.rows);
|
|
808
|
-
const wideLabelColumn = gridUsesSkipRollbackArcs(model.rows) ? gridWidth * 2 + 4 : undefined;
|
|
809
|
-
const dirNameGap = wideLabelColumn !== undefined ? 3 : LABEL_GAP;
|
|
810
|
-
const allEdges = model.rows
|
|
811
|
-
.filter(
|
|
812
|
-
(row): row is MigrationGraphGridRow & { edge: ClassifiedEdge } =>
|
|
813
|
-
row.kind === 'edge' && row.edge !== undefined,
|
|
814
|
-
)
|
|
815
|
-
.map((row) => row.edge);
|
|
816
|
-
const maxDirNameLen = maxDirNameLength(allEdges);
|
|
817
|
-
const effectiveMaxDirNameLen = opts.globalMaxDirNameWidth ?? maxDirNameLen;
|
|
818
|
-
const maxEdgePrefixWidth =
|
|
819
|
-
opts.globalMaxEdgeTreePrefixWidth ?? maxEdgeTreePrefixWidth(model.rows, wideLabelColumn);
|
|
820
|
-
const edgeDirNameWidth = rowDirNameWidth(maxEdgePrefixWidth, effectiveMaxDirNameLen, dirNameGap);
|
|
821
|
-
|
|
822
|
-
// Build a contract-hash → path-highlight map so node rows can be coloured correctly.
|
|
823
|
-
// On-path wins: if a contract is both `from` of an on-path edge and `to` of an off-path
|
|
824
|
-
// edge (or vice-versa), it is treated as on-path.
|
|
825
|
-
// This map is only populated when edgeAnnotationsByHash is provided (migrate --show);
|
|
826
|
-
// for every other command (graph/status/list) it is empty and the code below is a no-op.
|
|
827
|
-
// NOTE: this is ONLY used for node-marker (○/∅) classification. Connector rows and
|
|
828
|
-
// structural cells (tees, corners, arcs) use their per-cell migrationHash directly —
|
|
829
|
-
// not this map and not any column-level aggregate.
|
|
830
|
-
const contractHighlights = new Map<string, 'on-path' | 'off-path'>();
|
|
831
|
-
if (opts.edgeAnnotationsByHash) {
|
|
832
|
-
for (const row of model.rows) {
|
|
833
|
-
if (row.kind !== 'edge' || row.edge === undefined) continue;
|
|
834
|
-
const annotation = opts.edgeAnnotationsByHash.get(row.edge.migrationHash);
|
|
835
|
-
if (annotation?.pathHighlight === undefined) continue;
|
|
836
|
-
const highlight = annotation.pathHighlight;
|
|
837
|
-
for (const hash of [row.edge.from, row.edge.to]) {
|
|
838
|
-
if (hash === EMPTY_CONTRACT_HASH) continue;
|
|
839
|
-
const existing = contractHighlights.get(hash);
|
|
840
|
-
// On-path wins over off-path when a contract hash appears in both.
|
|
841
|
-
if (existing !== 'on-path') {
|
|
842
|
-
contractHighlights.set(hash, highlight);
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
// In path-highlight mode (`opts.edgeAnnotationsByHash` present), the by-branch rotating
|
|
849
|
-
// colour logic is suppressed entirely. Every glyph is styled by on-path / off-path role
|
|
850
|
-
// via PATH_HIGHLIGHT_STYLES — never by lane column index. In normal mode (no annotations)
|
|
851
|
-
// `pathHighlightActive` is false and the code below is a complete no-op; rotation applies.
|
|
852
|
-
const pathHighlightActive = opts.edgeAnnotationsByHash !== undefined;
|
|
853
|
-
|
|
854
|
-
/**
|
|
855
|
-
* Resolve the lane and arrow overrides for a row in path-highlight mode.
|
|
856
|
-
* - on-path → neutral single-path style (style.lane for glyphs, plain arrow, bold name).
|
|
857
|
-
* Rotation colour is suppressed; `style.sourceHash`/`style.destHash` apply for hashes.
|
|
858
|
-
* - off-path → uniform dim grey (forcedDim) on every glyph, arrow, name, and hash.
|
|
859
|
-
* - undefined → `undefined` (no override). Unannotated rows use normal rotation. This covers
|
|
860
|
-
* both non-path-highlight commands (graph/status/list) and any annotation without pathHighlight.
|
|
861
|
-
* - When pathHighlightActive is false: always returns undefined, preserving normal rotation.
|
|
862
|
-
*/
|
|
863
|
-
function pathStyleForHighlight(highlight: 'on-path' | 'off-path' | undefined):
|
|
864
|
-
| {
|
|
865
|
-
lane: ((text: string) => string) | undefined;
|
|
866
|
-
arrow: ((text: string) => string) | undefined;
|
|
867
|
-
dirName: ((text: string) => string) | undefined;
|
|
868
|
-
hashOverride: ((text: string) => string) | undefined;
|
|
869
|
-
}
|
|
870
|
-
| undefined {
|
|
871
|
-
if (!pathHighlightActive || highlight === undefined) return undefined;
|
|
872
|
-
if (highlight === 'off-path') {
|
|
873
|
-
const s = PATH_HIGHLIGHT_STYLES.offPath(opts.colorize);
|
|
874
|
-
return { lane: s.lane, arrow: s.arrow, dirName: s.dirName, hashOverride: s.hashOverride };
|
|
875
|
-
}
|
|
876
|
-
// on-path → green lane glyphs, bold name, neutral hashes
|
|
877
|
-
const s = PATH_HIGHLIGHT_STYLES.onPath(style, opts.colorize);
|
|
878
|
-
return { lane: s.lane, arrow: s.arrow, dirName: s.dirName, hashOverride: s.hashOverride };
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
/**
|
|
882
|
-
* Lane override for a given highlight in path-highlight mode.
|
|
883
|
-
* Returns the `lane` part only — used for per-cell overrides.
|
|
884
|
-
*/
|
|
885
|
-
function pathLaneFor(
|
|
886
|
-
highlight: 'on-path' | 'off-path' | undefined,
|
|
887
|
-
): ((text: string) => string) | undefined {
|
|
888
|
-
return pathStyleForHighlight(highlight)?.lane;
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
/**
|
|
892
|
-
* Arrow override for a given highlight in path-highlight mode.
|
|
893
|
-
* Returns the `arrow` part only — used for edge-lane cell arrow rendering.
|
|
894
|
-
*/
|
|
895
|
-
function pathArrowFor(
|
|
896
|
-
highlight: 'on-path' | 'off-path' | undefined,
|
|
897
|
-
): ((text: string) => string) | undefined {
|
|
898
|
-
return pathStyleForHighlight(highlight)?.arrow;
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
const lines: string[] = [];
|
|
902
|
-
|
|
903
|
-
for (let rowIndex = 0; rowIndex < model.rows.length; rowIndex++) {
|
|
904
|
-
const row = model.rows[rowIndex];
|
|
905
|
-
if (row === undefined) continue;
|
|
906
|
-
|
|
907
|
-
if (row.kind === 'component-separator') {
|
|
908
|
-
lines.push('');
|
|
909
|
-
continue;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
if (row.kind === 'branch-connector' || row.kind === 'merge-connector') {
|
|
913
|
-
// In path-highlight mode, build a per-column lane override from each cell's own
|
|
914
|
-
// migrationHash. Each structural cell (branch-tee, branch-corner, merge-tee,
|
|
915
|
-
// merge-corner, vertical-pass, arc-crossing) carries the migrationHash of the
|
|
916
|
-
// edge it visually belongs to (set by Stage 2). We look up that edge's annotation
|
|
917
|
-
// directly — no column-level aggregate, no "on-path wins" across columns.
|
|
918
|
-
let connectorColumnOverride: Map<number, (text: string) => string> | undefined;
|
|
919
|
-
if (pathHighlightActive && opts.colorize) {
|
|
920
|
-
connectorColumnOverride = new Map();
|
|
921
|
-
for (let col = 0; col < row.cells.length; col++) {
|
|
922
|
-
const cell = row.cells[col];
|
|
923
|
-
if (cell === undefined || cell.kind === 'empty') continue;
|
|
924
|
-
// arc-crossing: colour by the vertical lane's owner (migrationHash), not the arc.
|
|
925
|
-
const hashForCell =
|
|
926
|
-
'migrationHash' in cell && cell.migrationHash !== undefined
|
|
927
|
-
? cell.migrationHash
|
|
928
|
-
: undefined;
|
|
929
|
-
if (hashForCell === undefined) continue;
|
|
930
|
-
const highlight = opts.edgeAnnotationsByHash?.get(hashForCell)?.pathHighlight;
|
|
931
|
-
const override = pathLaneFor(highlight);
|
|
932
|
-
if (override !== undefined) {
|
|
933
|
-
connectorColumnOverride.set(col, override);
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
if (connectorColumnOverride.size === 0) {
|
|
937
|
-
connectorColumnOverride = undefined;
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
lines.push(
|
|
941
|
-
trimTrailingWhitespace(
|
|
942
|
-
renderConnectorRow(
|
|
943
|
-
row,
|
|
944
|
-
gridWidth,
|
|
945
|
-
opts.colorize,
|
|
946
|
-
style,
|
|
947
|
-
palette,
|
|
948
|
-
connectorColumnOverride,
|
|
949
|
-
),
|
|
950
|
-
),
|
|
951
|
-
);
|
|
952
|
-
continue;
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
// Determine the per-row path-highlight style for path-highlight rendering.
|
|
956
|
-
// For edge rows: derived from the edge's annotation.
|
|
957
|
-
// For node rows: derived from the contract hash's membership in on/off-path edges.
|
|
958
|
-
// When pathHighlightActive is false, pathStyleForHighlight returns undefined and
|
|
959
|
-
// the normal rotating-colour lane styler applies everywhere (no-op for non-show commands).
|
|
960
|
-
let rowPathHighlight: 'on-path' | 'off-path' | undefined;
|
|
961
|
-
if (row.kind === 'edge' && row.edge !== undefined) {
|
|
962
|
-
rowPathHighlight = opts.edgeAnnotationsByHash?.get(row.edge.migrationHash)?.pathHighlight;
|
|
963
|
-
} else if (row.kind === 'node' && row.contractHash !== undefined) {
|
|
964
|
-
rowPathHighlight = contractHighlights.get(row.contractHash);
|
|
965
|
-
}
|
|
966
|
-
const rowStyle = pathStyleForHighlight(rowPathHighlight);
|
|
967
|
-
const rowLaneOverride = rowStyle?.lane;
|
|
968
|
-
const rowArrowOverride = rowStyle?.arrow;
|
|
969
|
-
|
|
970
|
-
// Classify every cell by its own edge's annotation (migrationHash → edgeAnnotationsByHash).
|
|
971
|
-
// Each structural cell (vertical-pass, branch-tee, arc-land-corner, etc.) carries the
|
|
972
|
-
// migrationHash of the edge it visually belongs to (set by the layout builder, Stage 2).
|
|
973
|
-
// We read that hash directly — no column-level aggregate, no "on-path wins" across columns.
|
|
974
|
-
//
|
|
975
|
-
// - vertical-pass: classifies by cell.migrationHash (the edge passing through), NOT by column.
|
|
976
|
-
// - edge-lane: classifies by cell.migrationHash (the edge's own row).
|
|
977
|
-
// - branch-tee/corner, merge-tee/corner, arc-*: classifies by cell.migrationHash.
|
|
978
|
-
// - arc-crossing: classifies by cell.migrationHash (the vertical lane's owner), so the
|
|
979
|
-
// crossing reads as the lane passing THROUGH, not the arc skipping over.
|
|
980
|
-
// - node (○/∅): classifies by rowPathHighlight derived from contractHighlights (the
|
|
981
|
-
// node's incident edges); falls through to rowLaneOverride.
|
|
982
|
-
//
|
|
983
|
-
// When pathHighlightActive is false (normal graph/status/list mode), all overrides are
|
|
984
|
-
// undefined and the normal rotating-colour lane styler applies unchanged.
|
|
985
|
-
const cellColors = resolveRowArcLaneColors(row.cells);
|
|
986
|
-
let gutter = row.cells
|
|
987
|
-
.map((cell, column) => {
|
|
988
|
-
let laneOverride = rowLaneOverride;
|
|
989
|
-
let arrowOverride = rowArrowOverride;
|
|
990
|
-
let arcLaneOverride: ((text: string) => string) | undefined;
|
|
991
|
-
if (pathHighlightActive) {
|
|
992
|
-
if (cell.kind === 'edge-lane') {
|
|
993
|
-
// Own cell: colour comes from this cell's own edge annotation.
|
|
994
|
-
const cellHighlight = opts.edgeAnnotationsByHash?.get(
|
|
995
|
-
cell.migrationHash,
|
|
996
|
-
)?.pathHighlight;
|
|
997
|
-
laneOverride = pathLaneFor(cellHighlight);
|
|
998
|
-
arrowOverride = pathArrowFor(cellHighlight);
|
|
999
|
-
} else if (cell.kind === 'node' && (cell.arcTee === true || cell.arcLand === true)) {
|
|
1000
|
-
// Node with arc decoration: the node marker takes the node's own row highlight
|
|
1001
|
-
// (rowLaneOverride), but the arc connector belongs to the back-arc edge which may
|
|
1002
|
-
// have a different annotation. Look up the arc cell's migrationHash to derive the
|
|
1003
|
-
// arc connector's colour independently.
|
|
1004
|
-
const arcColumn = cellColors.connector[column] ?? NEUTRAL_LANE_COLUMN;
|
|
1005
|
-
const arcCell = row.cells[arcColumn];
|
|
1006
|
-
const arcHash =
|
|
1007
|
-
arcCell !== undefined && 'migrationHash' in arcCell
|
|
1008
|
-
? arcCell.migrationHash
|
|
1009
|
-
: undefined;
|
|
1010
|
-
if (arcHash !== undefined) {
|
|
1011
|
-
const arcHighlight = opts.edgeAnnotationsByHash?.get(arcHash)?.pathHighlight;
|
|
1012
|
-
arcLaneOverride = pathLaneFor(arcHighlight);
|
|
1013
|
-
}
|
|
1014
|
-
// laneOverride stays as rowLaneOverride (the node marker colour)
|
|
1015
|
-
} else if (cell.kind !== 'node' && cell.kind !== 'empty') {
|
|
1016
|
-
// Routing cells (vertical-pass, branch-tee, merge-corner, arc-*, horizontal-pass):
|
|
1017
|
-
// each carries a migrationHash for the edge it belongs to. Classify by that hash.
|
|
1018
|
-
//
|
|
1019
|
-
// arc-crossing in node/edge rows renders as '──' (the arc bridge over the crossing),
|
|
1020
|
-
// not '┼─'. Colour by the arc edge (arcMigrationHash) so an off-path arc bridge is
|
|
1021
|
-
// dim even when the crossed vertical lane (migrationHash) is on-path.
|
|
1022
|
-
// In connector rows, arc-crossing renders '┼─' where the junction belongs to the
|
|
1023
|
-
// vertical lane — handled separately in renderConnectorRow.
|
|
1024
|
-
const hashForCell =
|
|
1025
|
-
cell.kind === 'arc-crossing' &&
|
|
1026
|
-
'arcMigrationHash' in cell &&
|
|
1027
|
-
cell.arcMigrationHash !== undefined
|
|
1028
|
-
? cell.arcMigrationHash
|
|
1029
|
-
: 'migrationHash' in cell && cell.migrationHash !== undefined
|
|
1030
|
-
? cell.migrationHash
|
|
1031
|
-
: undefined;
|
|
1032
|
-
if (hashForCell !== undefined) {
|
|
1033
|
-
const cellHighlight = opts.edgeAnnotationsByHash?.get(hashForCell)?.pathHighlight;
|
|
1034
|
-
laneOverride = pathLaneFor(cellHighlight);
|
|
1035
|
-
arrowOverride = pathArrowFor(cellHighlight);
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
// plain node cells (no arcTee/arcLand) fall through to rowLaneOverride
|
|
1039
|
-
}
|
|
1040
|
-
return renderCellPair(
|
|
1041
|
-
cell,
|
|
1042
|
-
column,
|
|
1043
|
-
cellColors,
|
|
1044
|
-
opts.colorize,
|
|
1045
|
-
style,
|
|
1046
|
-
palette,
|
|
1047
|
-
laneOverride,
|
|
1048
|
-
arrowOverride,
|
|
1049
|
-
arcLaneOverride,
|
|
1050
|
-
);
|
|
1051
|
-
})
|
|
1052
|
-
.join('');
|
|
1053
|
-
let laneSpan = row.cells.length;
|
|
1054
|
-
if (row.kind === 'node') {
|
|
1055
|
-
const contractHash = row.contractHash ?? EMPTY_CONTRACT_HASH;
|
|
1056
|
-
if (contractHash === EMPTY_CONTRACT_HASH) {
|
|
1057
|
-
laneSpan = 1;
|
|
1058
|
-
} else {
|
|
1059
|
-
let lastActiveColumn = -1;
|
|
1060
|
-
for (let column = row.cells.length - 1; column >= 0; column--) {
|
|
1061
|
-
if (row.cells[column]?.kind !== 'empty') {
|
|
1062
|
-
lastActiveColumn = column;
|
|
1063
|
-
break;
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
laneSpan = lastActiveColumn >= 0 ? lastActiveColumn + 1 : 1;
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
const labelColumn =
|
|
1070
|
-
row.kind === 'edge'
|
|
1071
|
-
? maxEdgePrefixWidth
|
|
1072
|
-
: wideLabelColumn !== undefined &&
|
|
1073
|
-
(nodeHasArcDecoration(row) || row.contractHash !== undefined)
|
|
1074
|
-
? wideLabelColumn
|
|
1075
|
-
: laneSpan * 2 + LABEL_GAP;
|
|
1076
|
-
if (
|
|
1077
|
-
row.kind === 'edge' &&
|
|
1078
|
-
row.edge?.from === EMPTY_CONTRACT_HASH &&
|
|
1079
|
-
(row.laneIndex ?? 0) === 0
|
|
1080
|
-
) {
|
|
1081
|
-
// Init edge (∅ → first): only the first cell is rendered (the edge-lane cell).
|
|
1082
|
-
// rowLaneOverride is correct here — it comes from the edge's own annotation.
|
|
1083
|
-
gutter = row.cells
|
|
1084
|
-
.slice(0, 1)
|
|
1085
|
-
.map((cell, column) =>
|
|
1086
|
-
renderCellPair(
|
|
1087
|
-
cell,
|
|
1088
|
-
column,
|
|
1089
|
-
cellColors,
|
|
1090
|
-
opts.colorize,
|
|
1091
|
-
style,
|
|
1092
|
-
palette,
|
|
1093
|
-
rowLaneOverride,
|
|
1094
|
-
rowArrowOverride,
|
|
1095
|
-
),
|
|
1096
|
-
)
|
|
1097
|
-
.join('');
|
|
1098
|
-
} else if (row.kind === 'node' && laneSpan < row.cells.length && !nodeHasArcDecoration(row)) {
|
|
1099
|
-
// Node gutter slice: may contain vertical-pass cells belonging to other edges.
|
|
1100
|
-
// Classify each cell by its own migrationHash so pass-through lanes carry the
|
|
1101
|
-
// correct colour, not the node's highlight.
|
|
1102
|
-
gutter = row.cells
|
|
1103
|
-
.slice(0, laneSpan)
|
|
1104
|
-
.map((cell, column) => {
|
|
1105
|
-
let cellLaneOverride = rowLaneOverride;
|
|
1106
|
-
let cellArrowOverride = rowArrowOverride;
|
|
1107
|
-
if (pathHighlightActive && cell.kind !== 'node' && cell.kind !== 'empty') {
|
|
1108
|
-
const hashForCell =
|
|
1109
|
-
cell.kind === 'arc-crossing' &&
|
|
1110
|
-
'arcMigrationHash' in cell &&
|
|
1111
|
-
cell.arcMigrationHash !== undefined
|
|
1112
|
-
? cell.arcMigrationHash
|
|
1113
|
-
: 'migrationHash' in cell && cell.migrationHash !== undefined
|
|
1114
|
-
? cell.migrationHash
|
|
1115
|
-
: undefined;
|
|
1116
|
-
if (hashForCell !== undefined) {
|
|
1117
|
-
const cellHighlight = opts.edgeAnnotationsByHash?.get(hashForCell)?.pathHighlight;
|
|
1118
|
-
cellLaneOverride = pathLaneFor(cellHighlight);
|
|
1119
|
-
cellArrowOverride = pathArrowFor(cellHighlight);
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
return renderCellPair(
|
|
1123
|
-
cell,
|
|
1124
|
-
column,
|
|
1125
|
-
cellColors,
|
|
1126
|
-
opts.colorize,
|
|
1127
|
-
style,
|
|
1128
|
-
palette,
|
|
1129
|
-
cellLaneOverride,
|
|
1130
|
-
cellArrowOverride,
|
|
1131
|
-
);
|
|
1132
|
-
})
|
|
1133
|
-
.join('');
|
|
1134
|
-
} else if (gutter.length < laneSpan * 2) {
|
|
1135
|
-
gutter = gutter.padEnd(laneSpan * 2, ' ');
|
|
1136
|
-
}
|
|
1137
|
-
const dirNameWidth =
|
|
1138
|
-
row.kind === 'edge'
|
|
1139
|
-
? edgeDirNameWidth
|
|
1140
|
-
: rowDirNameWidth(labelColumn, maxDirNameLen, dirNameGap);
|
|
1141
|
-
const gutterPad = padVisible(gutter, labelColumn);
|
|
1142
|
-
|
|
1143
|
-
if (row.kind === 'node') {
|
|
1144
|
-
const contractHash = row.contractHash ?? EMPTY_CONTRACT_HASH;
|
|
1145
|
-
if (contractHash === EMPTY_CONTRACT_HASH) {
|
|
1146
|
-
// The ∅ node row's trailing cells are vertical-pass lanes belonging to arc edges.
|
|
1147
|
-
// Classify each by its own migrationHash so they carry the correct path-highlight
|
|
1148
|
-
// colour rather than the rotation code that falls out of the ambient lane styler.
|
|
1149
|
-
const trailingLanes = row.cells
|
|
1150
|
-
.slice(1)
|
|
1151
|
-
.map((cell, offset) => {
|
|
1152
|
-
let cellLaneOverride = rowLaneOverride;
|
|
1153
|
-
let cellArrowOverride = rowArrowOverride;
|
|
1154
|
-
if (pathHighlightActive && cell.kind !== 'node' && cell.kind !== 'empty') {
|
|
1155
|
-
const hashForCell =
|
|
1156
|
-
cell.kind === 'arc-crossing' &&
|
|
1157
|
-
'arcMigrationHash' in cell &&
|
|
1158
|
-
cell.arcMigrationHash !== undefined
|
|
1159
|
-
? cell.arcMigrationHash
|
|
1160
|
-
: 'migrationHash' in cell && cell.migrationHash !== undefined
|
|
1161
|
-
? cell.migrationHash
|
|
1162
|
-
: undefined;
|
|
1163
|
-
if (hashForCell !== undefined) {
|
|
1164
|
-
const cellHighlight = opts.edgeAnnotationsByHash?.get(hashForCell)?.pathHighlight;
|
|
1165
|
-
cellLaneOverride = pathLaneFor(cellHighlight);
|
|
1166
|
-
cellArrowOverride = pathArrowFor(cellHighlight);
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
return renderCellPair(
|
|
1170
|
-
cell,
|
|
1171
|
-
offset + 1,
|
|
1172
|
-
cellColors,
|
|
1173
|
-
opts.colorize,
|
|
1174
|
-
style,
|
|
1175
|
-
palette,
|
|
1176
|
-
cellLaneOverride,
|
|
1177
|
-
cellArrowOverride,
|
|
1178
|
-
);
|
|
1179
|
-
})
|
|
1180
|
-
.join('');
|
|
1181
|
-
const emptyGutter = palette.emptySource.padEnd(2, ' ') + trailingLanes;
|
|
1182
|
-
const overlays = overlayNamesForContract(contractHash, opts);
|
|
1183
|
-
if (overlays.markers.length === 0 && overlays.refs.length === 0) {
|
|
1184
|
-
lines.push(trimTrailingWhitespace(emptyGutter));
|
|
1185
|
-
continue;
|
|
1186
|
-
}
|
|
1187
|
-
const overlay = formatContractNodeOverlays(style, overlays.markers, overlays.refs);
|
|
1188
|
-
lines.push(trimTrailingWhitespace(`${emptyGutter}${' '.repeat(LABEL_GAP)}${overlay}`));
|
|
1189
|
-
continue;
|
|
1190
|
-
}
|
|
1191
|
-
// In path-highlight mode, off-path nodes use `rowStyle.hashOverride` (uniform dim) so
|
|
1192
|
-
// inner ANSI codes (e.g. dim+cyan of `style.sourceHash`) cannot override the outer dim.
|
|
1193
|
-
// On-path nodes use `style.sourceHash` as normal (neutral purple-ish hash colour).
|
|
1194
|
-
const hashTextStyler = rowStyle?.hashOverride ?? style.sourceHash;
|
|
1195
|
-
const hashText = hashTextStyler(
|
|
1196
|
-
abbreviateHash(contractHash, hashLength, palette.emptySource),
|
|
1197
|
-
);
|
|
1198
|
-
const overlays = overlayNamesForContract(contractHash, opts);
|
|
1199
|
-
const hasOverlays = overlays.markers.length > 0 || overlays.refs.length > 0;
|
|
1200
|
-
const overlayPad = hasOverlays ? ' '.repeat(LABEL_GAP) : '';
|
|
1201
|
-
const overlay = hasOverlays
|
|
1202
|
-
? formatContractNodeOverlays(style, overlays.markers, overlays.refs)
|
|
1203
|
-
: '';
|
|
1204
|
-
lines.push(trimTrailingWhitespace(`${gutterPad}${hashText}${overlayPad}${overlay}`));
|
|
1205
|
-
continue;
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
const edge = row.edge;
|
|
1209
|
-
if (edge === undefined) continue;
|
|
1210
|
-
|
|
1211
|
-
const dirNamePadding = ' '.repeat(Math.max(0, dirNameWidth - edge.dirName.length));
|
|
1212
|
-
const laneIndex = row.laneIndex ?? 0;
|
|
1213
|
-
|
|
1214
|
-
// The gutter is already coloured via the per-cell overrides threaded into renderCellPair.
|
|
1215
|
-
const edgeGutterPad = padVisible(gutter, labelColumn);
|
|
1216
|
-
|
|
1217
|
-
let dirName: string;
|
|
1218
|
-
if (rowStyle !== undefined) {
|
|
1219
|
-
// Path-highlight mode (on-path or off-path annotation present):
|
|
1220
|
-
// `rowStyle.dirName` is set by PATH_HIGHLIGHT_STYLES — bold for on-path, forcedDim for off-path.
|
|
1221
|
-
// Rotation is suppressed entirely for both roles.
|
|
1222
|
-
// When rowStyle is undefined (unannotated row or non-show command), this branch is not entered.
|
|
1223
|
-
const dirNameStyler = rowStyle.dirName ?? style.dirName;
|
|
1224
|
-
dirName = `${dirNameStyler(edge.dirName)}${dirNamePadding}`;
|
|
1225
|
-
} else {
|
|
1226
|
-
// Normal mode: lane hue for branched lanes (column ≥ 1), bold-only for column 0.
|
|
1227
|
-
const dirNameStyler =
|
|
1228
|
-
opts.colorize && laneIndex > NEUTRAL_LANE_COLUMN
|
|
1229
|
-
? (text: string) => forcedBold(laneColorForColumn(laneIndex)(text))
|
|
1230
|
-
: style.dirName;
|
|
1231
|
-
dirName = `${dirNameStyler(edge.dirName)}${dirNamePadding}`;
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
// Pass hashOverride from path-highlight styles so formatEdgeHashColumn applies it to ALL
|
|
1235
|
-
// sub-stylers (sourceHash, destHash, arrow glyph). Wrapping already-styled text in an outer
|
|
1236
|
-
// colour does not work — inner ANSI codes override the outer at the terminal level.
|
|
1237
|
-
const hashColumnOverride = rowStyle?.hashOverride;
|
|
1238
|
-
const hashColumn = formatEdgeHashColumn(edge, style, hashLength, palette, hashColumnOverride);
|
|
1239
|
-
const annotationSuffix = formatEdgeAnnotationSuffix(edge.migrationHash, opts, style);
|
|
1240
|
-
lines.push(
|
|
1241
|
-
trimTrailingWhitespace(`${edgeGutterPad}${dirName}${hashColumn}${annotationSuffix}`),
|
|
1242
|
-
);
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
return lines.join('\n');
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
/**
|
|
1249
|
-
* Format a single on-path migration row for the `migrate --show` run-list.
|
|
1250
|
-
*
|
|
1251
|
-
* Uses the SAME styling as the tree renderer's on-path rows (PATH_HIGHLIGHT_STYLES.onPath)
|
|
1252
|
-
* so the run-list and graph tree are byte-for-byte identical in their name/hash columns.
|
|
1253
|
-
* The gutter is omitted — the list has no graph structure.
|
|
1254
|
-
*
|
|
1255
|
-
* This is the SINGLE code path for on-path row styling shared by both the graph tree
|
|
1256
|
-
* and the "Will run, in order:" list. To change the on-path colour, edit PATH_HIGHLIGHT_STYLES.
|
|
1257
|
-
*/
|
|
1258
|
-
export function formatOnPathMigrationRow(
|
|
1259
|
-
dirName: string,
|
|
1260
|
-
from: string,
|
|
1261
|
-
to: string,
|
|
1262
|
-
dirNameWidth: number,
|
|
1263
|
-
colorize: boolean,
|
|
1264
|
-
glyphMode: GlyphMode,
|
|
1265
|
-
): string {
|
|
1266
|
-
const palette = paletteFor(glyphMode);
|
|
1267
|
-
const style = createAnsiMigrationListStyler({ useColor: colorize });
|
|
1268
|
-
// Use PATH_HIGHLIGHT_STYLES.onPath as the single seam for on-path colour.
|
|
1269
|
-
// Pass `style` and `colorize` so the lane/glyph stylers respect the colour gate.
|
|
1270
|
-
const s = PATH_HIGHLIGHT_STYLES.onPath(style, colorize);
|
|
1271
|
-
const styledDirName = `${s.dirName(dirName)}${' '.repeat(Math.max(0, dirNameWidth - dirName.length))}`;
|
|
1272
|
-
const hashLength = MIGRATION_LIST_HASH_WIDTH;
|
|
1273
|
-
const emptySource = palette.emptySource;
|
|
1274
|
-
const fromAbbr =
|
|
1275
|
-
from === EMPTY_CONTRACT_HASH
|
|
1276
|
-
? padFromHashColumn(style.glyph(emptySource), hashLength)
|
|
1277
|
-
: padFromHashColumn(style.sourceHash(abbreviateHashShort(from, hashLength)), hashLength);
|
|
1278
|
-
const toAbbr =
|
|
1279
|
-
to === EMPTY_CONTRACT_HASH
|
|
1280
|
-
? style.glyph(emptySource)
|
|
1281
|
-
: style.destHash(abbreviateHashShort(to, hashLength));
|
|
1282
|
-
const arrow = style.glyph(palette.forwardArrow);
|
|
1283
|
-
return `${styledDirName} ${fromAbbr} ${arrow} ${toAbbr}`;
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
function abbreviateHashShort(hash: string, length: number): string {
|
|
1287
|
-
const stripped = hash.startsWith('sha256:') ? hash.slice(7) : hash;
|
|
1288
|
-
return stripped.slice(0, length);
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
export interface RenderMigrationGraphLegendOptions {
|
|
1292
|
-
readonly colorize: boolean;
|
|
1293
|
-
readonly glyphMode?: GlyphMode;
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
function formatLegendExampleMarkers(colorize: boolean): string {
|
|
1297
|
-
if (!colorize) {
|
|
1298
|
-
return '@contract @db';
|
|
1299
|
-
}
|
|
1300
|
-
const sigil = green('@');
|
|
1301
|
-
return `${sigil + bold(green('contract'))} ${sigil}${green('db')}`;
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
/**
|
|
1305
|
-
* A compact key for the tree visual language: the contract node glyph, the
|
|
1306
|
-
* in-lane direction arrows, the empty baseline, the system-marker `<…>` and
|
|
1307
|
-
* user-ref `(…)` bracket conventions (two illustrative example lines), and a
|
|
1308
|
-
* worked sample of the data-column `from → to` migration hash arrow.
|
|
1309
|
-
*
|
|
1310
|
-
* Honors the same glyph palette (unicode vs ASCII) and `colorize` gate as the
|
|
1311
|
-
* tree renderer, so the key matches whatever the graph itself drew and stays
|
|
1312
|
-
* pipe-safe (zero ANSI when color is off). The caller adds the trailing blank
|
|
1313
|
-
* line that separates this stderr key from the tree on stdout.
|
|
1314
|
-
*/
|
|
1315
|
-
export function renderMigrationGraphLegend(opts: RenderMigrationGraphLegendOptions): string {
|
|
1316
|
-
const palette = paletteFor(opts.glyphMode ?? 'unicode');
|
|
1317
|
-
const style = createAnsiMigrationListStyler({ useColor: opts.colorize });
|
|
1318
|
-
const node = palette.node.trimEnd();
|
|
1319
|
-
const sampleArrow = `${style.sourceHash('aaaaaa')} ${style.glyph(palette.forwardArrow)} ${style.destHash('bbbbbb')}`;
|
|
1320
|
-
const statusGlyphs = overlayStatusGlyphs(opts.glyphMode ?? 'unicode');
|
|
1321
|
-
const appliedPending = opts.colorize
|
|
1322
|
-
? ` ${green(statusGlyphs.applied)} ${style.summary('applied')} ${yellow(statusGlyphs.pending)} ${style.summary('pending')}`
|
|
1323
|
-
: ` ${statusGlyphs.applied} ${style.summary('applied')} ${statusGlyphs.pending} ${style.summary('pending')}`;
|
|
1324
|
-
const exampleMarkers = formatLegendExampleMarkers(opts.colorize);
|
|
1325
|
-
const exampleRefs = opts.colorize ? style.refs(['prod', 'staging']) : '(prod, staging)';
|
|
1326
|
-
const lines = [
|
|
1327
|
-
'Legend:',
|
|
1328
|
-
` ${style.kind(node)} ${style.summary('contract')} ${style.kind(palette.edgeArrow.forward)} ${style.summary('forward')} ${style.kind(palette.edgeArrow.rollback)} ${style.summary('rollback')}`,
|
|
1329
|
-
` ${style.kind(palette.edgeArrow.self)} ${style.summary('migration without schema change')}`,
|
|
1330
|
-
appliedPending,
|
|
1331
|
-
` ${style.kind(palette.emptySource)} ${style.summary('empty database (baseline)')}`,
|
|
1332
|
-
` ${exampleMarkers} ${style.summary('reserved markers — also typeable as --from/--to tokens')}`,
|
|
1333
|
-
` ${exampleRefs} ${style.summary('user-defined refs')}`,
|
|
1334
|
-
` ${sampleArrow} ${style.summary('migration from contract aaaaaa to bbbbbb')}`,
|
|
1335
|
-
];
|
|
1336
|
-
return lines.join('\n');
|
|
1337
|
-
}
|