@prisma-next/cli 0.12.0 → 0.13.0-dev.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cli.mjs +180 -163
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-KgJorIvG.mjs → client-CJzuo5wX.mjs} +222 -107
- package/dist/client-CJzuo5wX.mjs.map +1 -0
- package/dist/{command-helpers-Bbw1GbwL.mjs → command-helpers-DGMvGBeX.mjs} +318 -25
- package/dist/command-helpers-DGMvGBeX.mjs.map +1 -0
- package/dist/commands/contract-emit.d.mts.map +1 -1
- package/dist/commands/contract-emit.mjs +1 -1
- package/dist/commands/contract-infer.d.mts.map +1 -1
- package/dist/commands/contract-infer.mjs +1 -1
- package/dist/commands/db-init.d.mts.map +1 -1
- package/dist/commands/db-init.mjs +4 -5
- package/dist/commands/db-init.mjs.map +1 -1
- package/dist/commands/db-schema.d.mts.map +1 -1
- package/dist/commands/db-schema.mjs +3 -3
- package/dist/commands/db-schema.mjs.map +1 -1
- package/dist/commands/db-sign.d.mts.map +1 -1
- package/dist/commands/db-sign.mjs +6 -6
- package/dist/commands/db-sign.mjs.map +1 -1
- package/dist/commands/db-update.d.mts.map +1 -1
- package/dist/commands/db-update.mjs +10 -7
- package/dist/commands/db-update.mjs.map +1 -1
- package/dist/commands/db-verify.d.mts.map +1 -1
- package/dist/commands/db-verify.mjs +1 -1
- package/dist/commands/migrate.d.mts +37 -3
- package/dist/commands/migrate.d.mts.map +1 -1
- package/dist/commands/migrate.mjs +298 -12
- package/dist/commands/migrate.mjs.map +1 -1
- package/dist/commands/migration-check.d.mts +55 -13
- package/dist/commands/migration-check.d.mts.map +1 -1
- package/dist/commands/migration-check.mjs +3 -2
- package/dist/commands/migration-graph.d.mts +17 -8
- package/dist/commands/migration-graph.d.mts.map +1 -1
- package/dist/commands/migration-graph.mjs +185 -2
- package/dist/commands/migration-graph.mjs.map +1 -0
- package/dist/commands/migration-list.d.mts +26 -27
- package/dist/commands/migration-list.d.mts.map +1 -1
- package/dist/commands/migration-list.mjs +2 -190
- package/dist/commands/migration-log.d.mts +9 -19
- package/dist/commands/migration-log.d.mts.map +1 -1
- package/dist/commands/migration-log.mjs +1 -137
- package/dist/commands/migration-new.d.mts.map +1 -1
- package/dist/commands/migration-new.mjs +6 -5
- package/dist/commands/migration-new.mjs.map +1 -1
- package/dist/commands/migration-plan.d.mts +1 -1
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +1 -1
- package/dist/commands/migration-show.d.mts +17 -21
- package/dist/commands/migration-show.d.mts.map +1 -1
- package/dist/commands/migration-show.mjs +24 -36
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.d.mts +42 -144
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +3 -759
- package/dist/commands/ref.d.mts +1 -1
- package/dist/commands/ref.d.mts.map +1 -1
- package/dist/commands/ref.mjs +4 -4
- package/dist/commands/ref.mjs.map +1 -1
- package/dist/commands/telemetry/index.d.mts +7 -0
- package/dist/commands/telemetry/index.d.mts.map +1 -0
- package/dist/commands/telemetry/index.mjs +2 -0
- package/dist/{config-loader-B6sJjXTv.mjs → config-loader-p9JMrekQ.mjs} +1 -1
- package/dist/{config-loader-B6sJjXTv.mjs.map → config-loader-p9JMrekQ.mjs.map} +1 -1
- package/dist/config-loader.mjs +1 -1
- package/dist/{contract-at-errors-BxP-TOMl.mjs → contract-at-errors-CFXsstzm.mjs} +2 -2
- package/dist/{contract-at-errors-BxP-TOMl.mjs.map → contract-at-errors-CFXsstzm.mjs.map} +1 -1
- package/dist/{contract-emit-DxcGl4Uq.mjs → contract-emit-B_qriF8B.mjs} +5 -5
- package/dist/{contract-emit-DxcGl4Uq.mjs.map → contract-emit-B_qriF8B.mjs.map} +1 -1
- package/dist/{contract-emit-D-4jrNve.mjs → contract-emit-C8HmtboH.mjs} +12 -7
- package/dist/contract-emit-C8HmtboH.mjs.map +1 -0
- package/dist/{contract-enrichment-a0V5Y_mL.mjs → contract-enrichment-gn9sWbPw.mjs} +1 -1
- package/dist/{contract-enrichment-a0V5Y_mL.mjs.map → contract-enrichment-gn9sWbPw.mjs.map} +1 -1
- package/dist/{contract-infer-D8uEbJuu.mjs → contract-infer-BYT_ra_U.mjs} +5 -5
- package/dist/contract-infer-BYT_ra_U.mjs.map +1 -0
- package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs → contract-space-aggregate-loader-ClI1KN6d.mjs} +5 -5
- package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs.map → contract-space-aggregate-loader-ClI1KN6d.mjs.map} +1 -1
- package/dist/{db-verify-v_vUKXTU.mjs → db-verify-C24FKhb7.mjs} +6 -6
- package/dist/{db-verify-v_vUKXTU.mjs.map → db-verify-C24FKhb7.mjs.map} +1 -1
- package/dist/exports/control-api.d.mts +5 -3
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +3 -3
- package/dist/exports/index.mjs +1 -1
- package/dist/exports/index.mjs.map +1 -1
- package/dist/exports/init-output.d.mts +1 -3
- package/dist/exports/init-output.d.mts.map +1 -1
- package/dist/exports/init-output.mjs +1 -1
- package/dist/{extension-pack-inputs-IDvjRCi3.mjs → extension-pack-inputs-1ySHqxKG.mjs} +1 -1
- package/dist/{extension-pack-inputs-IDvjRCi3.mjs.map → extension-pack-inputs-1ySHqxKG.mjs.map} +1 -1
- package/dist/{framework-components-fYXjz_in.mjs → framework-components-YVQHhPH7.mjs} +2 -2
- package/dist/{framework-components-fYXjz_in.mjs.map → framework-components-YVQHhPH7.mjs.map} +1 -1
- package/dist/{global-flags-DEHjV8_s.d.mts → global-flags-BpoOYtNZ.d.mts} +1 -1
- package/dist/{global-flags-DEHjV8_s.d.mts.map → global-flags-BpoOYtNZ.d.mts.map} +1 -1
- package/dist/{init-Cv9UzWL5.mjs → init-0HwB-Vh8.mjs} +5 -58
- package/dist/init-0HwB-Vh8.mjs.map +1 -0
- package/dist/{inspect-live-schema-C6ohV_oQ.mjs → inspect-live-schema-DF6IwcDl.mjs} +7 -5
- package/dist/inspect-live-schema-DF6IwcDl.mjs.map +1 -0
- package/dist/migration-check-VwM8xCZV.mjs +574 -0
- package/dist/migration-check-VwM8xCZV.mjs.map +1 -0
- package/dist/migration-cli.mjs +1 -1
- package/dist/migration-cli.mjs.map +1 -1
- package/dist/{migration-command-scaffold-CjvwO6at.mjs → migration-command-scaffold-DA-Lhx6o.mjs} +5 -5
- package/dist/{migration-command-scaffold-CjvwO6at.mjs.map → migration-command-scaffold-DA-Lhx6o.mjs.map} +1 -1
- package/dist/migration-graph-command-render-CEez7YUK.mjs +1960 -0
- package/dist/migration-graph-command-render-CEez7YUK.mjs.map +1 -0
- package/dist/migration-list-DlJJ_38Z.mjs +230 -0
- package/dist/migration-list-DlJJ_38Z.mjs.map +1 -0
- package/dist/migration-log-CG0qQAFm.mjs +222 -0
- package/dist/migration-log-CG0qQAFm.mjs.map +1 -0
- package/dist/migration-path-target-Ce6OZImp.mjs +38 -0
- package/dist/migration-path-target-Ce6OZImp.mjs.map +1 -0
- package/dist/{migration-plan-9DJ7q7_z.mjs → migration-plan-z5Ing-TD.mjs} +9 -8
- package/dist/migration-plan-z5Ing-TD.mjs.map +1 -0
- package/dist/migration-status-CD-LC2Ip.mjs +447 -0
- package/dist/migration-status-CD-LC2Ip.mjs.map +1 -0
- package/dist/{output-B60Gw5fu.mjs → output-mEQ74_nd.mjs} +1 -1
- package/dist/{output-B60Gw5fu.mjs.map → output-mEQ74_nd.mjs.map} +1 -1
- package/dist/{progress-adapter-C644QK8l.mjs → progress-adapter-CjAeTxY_.mjs} +1 -1
- package/dist/{progress-adapter-C644QK8l.mjs.map → progress-adapter-CjAeTxY_.mjs.map} +1 -1
- package/dist/{ref-advancement-DUZqsue6.mjs → ref-advancement-BkXlikCA.mjs} +1 -1
- package/dist/{ref-advancement-DUZqsue6.mjs.map → ref-advancement-BkXlikCA.mjs.map} +1 -1
- package/dist/schemas-CeGMYFYX.d.mts +191 -0
- package/dist/schemas-CeGMYFYX.d.mts.map +1 -0
- package/dist/schemas-KhXMzNA_.mjs +112 -0
- package/dist/schemas-KhXMzNA_.mjs.map +1 -0
- package/dist/telemetry-BIM4beEO.mjs +122 -0
- package/dist/telemetry-BIM4beEO.mjs.map +1 -0
- package/dist/{terminal-ui-5Y6mrg93.d.mts → terminal-ui-DGRNFWna.d.mts} +1 -1
- package/dist/terminal-ui-DGRNFWna.d.mts.map +1 -0
- package/dist/{types-Dt_SfqFm.d.mts → types-C_tYiJYx.d.mts} +53 -31
- package/dist/types-C_tYiJYx.d.mts.map +1 -0
- package/dist/{verify-DCA9Sldu.mjs → verify-DcOYZ1tH.mjs} +2 -2
- package/dist/{verify-DCA9Sldu.mjs.map → verify-DcOYZ1tH.mjs.map} +1 -1
- package/package.json +26 -22
- package/src/cli.ts +5 -0
- package/src/commands/contract-infer.ts +2 -2
- package/src/commands/db-update.ts +7 -1
- package/src/commands/init/index.ts +6 -35
- package/src/commands/init/init.ts +1 -14
- package/src/commands/init/inputs.ts +0 -75
- package/src/commands/inspect-live-schema.ts +10 -0
- package/src/commands/json/schemas.ts +195 -0
- package/src/commands/migrate.ts +527 -8
- package/src/commands/migration-check.ts +469 -134
- package/src/commands/migration-graph.ts +164 -91
- package/src/commands/migration-list.ts +72 -39
- package/src/commands/migration-log.ts +52 -102
- package/src/commands/migration-new.ts +2 -1
- package/src/commands/migration-plan.ts +2 -1
- package/src/commands/migration-show.ts +31 -66
- package/src/commands/migration-status-overlay.ts +61 -0
- package/src/commands/migration-status.ts +458 -1066
- package/src/commands/telemetry/index.ts +107 -0
- package/src/commands/telemetry/status.ts +67 -0
- package/src/control-api/client.ts +70 -9
- package/src/control-api/operations/contract-emit.ts +22 -2
- package/src/control-api/operations/db-init.ts +6 -3
- package/src/control-api/operations/{db-apply.ts → db-run.ts} +55 -14
- package/src/control-api/operations/db-update.ts +7 -4
- package/src/control-api/operations/db-verify.ts +15 -5
- package/src/control-api/operations/{migration-apply.ts → migrate.ts} +181 -80
- package/src/control-api/operations/{apply.ts → run-migration.ts} +33 -27
- package/src/control-api/types.ts +56 -29
- package/src/utils/cli-errors.ts +70 -2
- package/src/utils/formatters/errors.ts +11 -0
- package/src/utils/formatters/migration-graph-command-render.ts +239 -0
- package/src/utils/formatters/migration-graph-grid-layout.ts +1134 -0
- package/src/utils/formatters/migration-graph-labels.ts +408 -0
- package/src/utils/formatters/migration-graph-model.ts +103 -0
- package/src/utils/formatters/migration-graph-occlusion-render.ts +258 -0
- package/src/utils/formatters/migration-graph-rows.ts +128 -15
- package/src/utils/formatters/migration-graph-space-render.ts +188 -0
- package/src/utils/formatters/migration-list-data-column.ts +4 -91
- package/src/utils/formatters/migration-list-graph-topology.ts +72 -94
- package/src/utils/formatters/migration-list-render.ts +135 -71
- package/src/utils/formatters/migration-list-styler.ts +46 -5
- package/src/utils/formatters/migration-list-types.ts +5 -21
- package/src/utils/formatters/migration-log-table.ts +205 -0
- package/src/utils/formatters/migrations.ts +33 -11
- package/src/utils/global-flags.ts +35 -0
- package/src/utils/integrity-violation-to-check-failure.ts +28 -19
- package/src/utils/legend.ts +38 -0
- package/src/utils/migration-path-target.ts +60 -0
- package/src/utils/telemetry.ts +68 -32
- package/dist/client-KgJorIvG.mjs.map +0 -1
- package/dist/command-helpers-Bbw1GbwL.mjs.map +0 -1
- package/dist/commands/migration-list.mjs.map +0 -1
- package/dist/commands/migration-log.mjs.map +0 -1
- package/dist/commands/migration-status.mjs.map +0 -1
- package/dist/contract-emit-D-4jrNve.mjs.map +0 -1
- package/dist/contract-infer-D8uEbJuu.mjs.map +0 -1
- package/dist/graph-render-rFAqZujX.mjs +0 -1081
- package/dist/graph-render-rFAqZujX.mjs.map +0 -1
- package/dist/init-Cv9UzWL5.mjs.map +0 -1
- package/dist/inspect-live-schema-C6ohV_oQ.mjs.map +0 -1
- package/dist/migration-check-BiBJoYYW.mjs +0 -341
- package/dist/migration-check-BiBJoYYW.mjs.map +0 -1
- package/dist/migration-graph-D7DVUElV.mjs +0 -1232
- package/dist/migration-graph-D7DVUElV.mjs.map +0 -1
- package/dist/migration-list-styler-BRwF4-gy.mjs +0 -399
- package/dist/migration-list-styler-BRwF4-gy.mjs.map +0 -1
- package/dist/migration-plan-9DJ7q7_z.mjs.map +0 -1
- package/dist/migration-types-D2FW63pr.d.mts +0 -15
- package/dist/migration-types-D2FW63pr.d.mts.map +0 -1
- package/dist/migrations-Cv2jxNNK.mjs +0 -228
- package/dist/migrations-Cv2jxNNK.mjs.map +0 -1
- package/dist/terminal-ui-5Y6mrg93.d.mts.map +0 -1
- package/dist/types-Dt_SfqFm.d.mts.map +0 -1
- package/src/utils/formatters/graph-migration-mapper.ts +0 -235
- package/src/utils/formatters/graph-render.ts +0 -1323
- package/src/utils/formatters/graph-types.ts +0 -120
- package/src/utils/formatters/migration-graph-layout.ts +0 -1119
- package/src/utils/formatters/migration-graph-tree-render.ts +0 -459
|
@@ -0,0 +1,1960 @@
|
|
|
1
|
+
import { ifDefined } from "@prisma-next/utils/defined";
|
|
2
|
+
import { bold, createColors, cyan, cyanBright, dim, green, yellow } from "colorette";
|
|
3
|
+
import stringWidth from "string-width";
|
|
4
|
+
import { EMPTY_CONTRACT_HASH } from "@prisma-next/migration-tools/constants";
|
|
5
|
+
//#region src/utils/formatters/migration-graph-occlusion-render.ts
|
|
6
|
+
/**
|
|
7
|
+
* Occlusion renderer for the line/plane/occlusion migration-graph.
|
|
8
|
+
*
|
|
9
|
+
* Per cell: pick the topmost-plane line (lowest plane number = drawn on top),
|
|
10
|
+
* look up its glyph, apply colour from the line's lane or role. Lower-plane
|
|
11
|
+
* lines are occluded (not drawn).
|
|
12
|
+
*
|
|
13
|
+
* Colour is forced via createColors({ useColor: true }) regardless of NO_COLOR.
|
|
14
|
+
*/
|
|
15
|
+
const palette = createColors({ useColor: true });
|
|
16
|
+
const LANE_COLORIZERS = [
|
|
17
|
+
palette.white,
|
|
18
|
+
palette.cyan,
|
|
19
|
+
palette.yellow,
|
|
20
|
+
palette.blueBright,
|
|
21
|
+
palette.magenta,
|
|
22
|
+
palette.green
|
|
23
|
+
];
|
|
24
|
+
function laneColor(lane) {
|
|
25
|
+
return LANE_COLORIZERS[lane % LANE_COLORIZERS.length] ?? ((t) => t);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The colourizer for a lane's hue (lane0 = white, lane1 = cyan, …). Exported
|
|
29
|
+
* so the per-row LABEL renderer can tint a migration name in its lane's colour,
|
|
30
|
+
* matching the node `○`, the edges, and the arrows drawn in the gutter — one
|
|
31
|
+
* colour per lane across glyph and text.
|
|
32
|
+
*/
|
|
33
|
+
function laneColorizer(lane) {
|
|
34
|
+
return laneColor(lane);
|
|
35
|
+
}
|
|
36
|
+
function roleColor(role) {
|
|
37
|
+
return role === "on-path" ? palette.greenBright : palette.dim;
|
|
38
|
+
}
|
|
39
|
+
const UNICODE_ALPHABET = {
|
|
40
|
+
vertical: "│",
|
|
41
|
+
horizontal: "─",
|
|
42
|
+
cornerUpRight: "╰",
|
|
43
|
+
cornerDownRight: "╭",
|
|
44
|
+
cornerUpLeft: "╯",
|
|
45
|
+
cornerDownLeft: "╮",
|
|
46
|
+
arrowUp: "↑",
|
|
47
|
+
arrowDown: "↓",
|
|
48
|
+
node: "○",
|
|
49
|
+
selfLoop: "⟲",
|
|
50
|
+
landingArrow: "◂",
|
|
51
|
+
fallback: "?"
|
|
52
|
+
};
|
|
53
|
+
const ASCII_ALPHABET = {
|
|
54
|
+
vertical: "|",
|
|
55
|
+
horizontal: "-",
|
|
56
|
+
cornerUpRight: "\\",
|
|
57
|
+
cornerDownRight: "/",
|
|
58
|
+
cornerUpLeft: "/",
|
|
59
|
+
cornerDownLeft: "\\",
|
|
60
|
+
arrowUp: "^",
|
|
61
|
+
arrowDown: "v",
|
|
62
|
+
node: "*",
|
|
63
|
+
selfLoop: "@",
|
|
64
|
+
landingArrow: "<",
|
|
65
|
+
fallback: "?"
|
|
66
|
+
};
|
|
67
|
+
function alphabetFor(mode) {
|
|
68
|
+
return mode === "ascii" ? ASCII_ALPHABET : UNICODE_ALPHABET;
|
|
69
|
+
}
|
|
70
|
+
function glyphFor(dirs, alphabet) {
|
|
71
|
+
const has = (d) => dirs.has(d);
|
|
72
|
+
if (has("up") && has("down") && !has("left") && !has("right")) return alphabet.vertical;
|
|
73
|
+
if (has("left") && has("right") && !has("up") && !has("down")) return alphabet.horizontal;
|
|
74
|
+
if (has("up") && has("right") && !has("down") && !has("left")) return alphabet.cornerUpRight;
|
|
75
|
+
if (has("down") && has("right") && !has("up") && !has("left")) return alphabet.cornerDownRight;
|
|
76
|
+
if (has("up") && has("left") && !has("down") && !has("right")) return alphabet.cornerUpLeft;
|
|
77
|
+
if (has("down") && has("left") && !has("up") && !has("right")) return alphabet.cornerDownLeft;
|
|
78
|
+
if (has("up") && !has("down") && !has("left") && !has("right")) return alphabet.arrowUp;
|
|
79
|
+
if (has("down") && !has("up") && !has("left") && !has("right")) return alphabet.arrowDown;
|
|
80
|
+
return alphabet.fallback;
|
|
81
|
+
}
|
|
82
|
+
const NO_COLOR = (t) => t;
|
|
83
|
+
function renderCell(cell, colorEnabled, alphabet) {
|
|
84
|
+
if (cell.node !== void 0) return (!colorEnabled ? NO_COLOR : cell.node.role !== void 0 ? roleColor(cell.node.role) : laneColor(cell.node.lane))(alphabet.node);
|
|
85
|
+
if (cell.lines.length === 0) return " ";
|
|
86
|
+
const topLine = cell.lines.reduce((best, current) => {
|
|
87
|
+
if (current.plane < best.plane) return current;
|
|
88
|
+
if (current.plane > best.plane) return best;
|
|
89
|
+
if (current.line.role === "on-path" && best.line.role !== "on-path") return current;
|
|
90
|
+
return best;
|
|
91
|
+
}, cell.lines[0]);
|
|
92
|
+
const glyph = topLine.selfLoop === true ? alphabet.selfLoop : topLine.landingArrow === true ? alphabet.landingArrow : glyphFor(topLine.directions, alphabet);
|
|
93
|
+
return (!colorEnabled ? NO_COLOR : topLine.line.role !== void 0 ? roleColor(topLine.line.role) : laneColor(topLine.line.lane))(glyph);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Render a single grid row to a coloured string. A completely empty row returns
|
|
97
|
+
* the empty string (the row is NOT dropped) so callers that pair grid rows with
|
|
98
|
+
* an external per-row label list keep a 1:1 index correspondence. `renderGrid`
|
|
99
|
+
* itself drops empty rows for its standalone output (but preserves separator rows).
|
|
100
|
+
*/
|
|
101
|
+
function renderGridRow(row, opts = {}) {
|
|
102
|
+
if (row[0]?.separator === true) return "";
|
|
103
|
+
let lastNonEmpty = -1;
|
|
104
|
+
for (let i = row.length - 1; i >= 0; i--) {
|
|
105
|
+
const cell = row[i];
|
|
106
|
+
if (cell !== void 0 && (cell.lines.length > 0 || cell.node !== void 0)) {
|
|
107
|
+
lastNonEmpty = i;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (lastNonEmpty < 0) return "";
|
|
112
|
+
const colsPerLane = opts.colsPerLane ?? 2;
|
|
113
|
+
const colorEnabled = opts.colorize ?? true;
|
|
114
|
+
const alphabet = alphabetFor(opts.glyphMode ?? "unicode");
|
|
115
|
+
const lastConnectorCol = Math.floor(lastNonEmpty / colsPerLane) * colsPerLane + (colsPerLane - 1);
|
|
116
|
+
const renderThrough = Math.max(lastNonEmpty, lastConnectorCol);
|
|
117
|
+
let line = "";
|
|
118
|
+
for (let col = 0; col <= Math.min(renderThrough, row.length - 1); col++) {
|
|
119
|
+
const cell = row[col];
|
|
120
|
+
line += cell === void 0 ? " " : renderCell(cell, colorEnabled, alphabet);
|
|
121
|
+
}
|
|
122
|
+
return line;
|
|
123
|
+
}
|
|
124
|
+
function migrationListForwardArrow(glyphMode) {
|
|
125
|
+
return glyphMode === "ascii" ? "->" : "→";
|
|
126
|
+
}
|
|
127
|
+
function migrationListEmptySource(glyphMode) {
|
|
128
|
+
return glyphMode === "ascii" ? "-" : "∅";
|
|
129
|
+
}
|
|
130
|
+
function abbreviateContractHash(hash) {
|
|
131
|
+
return (hash.startsWith("sha256:") ? hash.slice(7) : hash).slice(0, 7);
|
|
132
|
+
}
|
|
133
|
+
function padFromHashColumn(text, width) {
|
|
134
|
+
const padding = Math.max(0, width - stringWidth(text));
|
|
135
|
+
return `${" ".repeat(padding)}${text}`;
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/utils/formatters/migration-graph-grid-layout.ts
|
|
139
|
+
/**
|
|
140
|
+
* Grid layout for the line/plane/occlusion migration-graph renderer.
|
|
141
|
+
*
|
|
142
|
+
* Produces a Grid (rows × cells) from a MigrationGraphRowModel. Each node
|
|
143
|
+
* emits: fork connector, self-loop rows, node row, merge connector, and
|
|
144
|
+
* inbound migration rows — in display order (tips first, then roots).
|
|
145
|
+
*/
|
|
146
|
+
function buildLaneAssignment(nodes, edges) {
|
|
147
|
+
const fwdEdges = edges.filter((e) => e.kind === "forward" && e.from !== e.to);
|
|
148
|
+
const outbound = /* @__PURE__ */ new Map();
|
|
149
|
+
const inbound = /* @__PURE__ */ new Map();
|
|
150
|
+
for (const edge of fwdEdges) {
|
|
151
|
+
const ob = outbound.get(edge.from);
|
|
152
|
+
if (ob) ob.push(edge);
|
|
153
|
+
else outbound.set(edge.from, [edge]);
|
|
154
|
+
const ib = inbound.get(edge.to);
|
|
155
|
+
if (ib) ib.push(edge);
|
|
156
|
+
else inbound.set(edge.to, [edge]);
|
|
157
|
+
}
|
|
158
|
+
for (const list of outbound.values()) list.sort((a, b) => a.dirName.localeCompare(b.dirName));
|
|
159
|
+
for (const list of inbound.values()) list.sort((a, b) => a.dirName.localeCompare(b.dirName));
|
|
160
|
+
const components = [];
|
|
161
|
+
let current = [];
|
|
162
|
+
for (const n of nodes) if (n === null) {
|
|
163
|
+
if (current.length > 0) components.push(current);
|
|
164
|
+
current = [];
|
|
165
|
+
} else current.push(n);
|
|
166
|
+
if (current.length > 0) components.push(current);
|
|
167
|
+
const allNodes = /* @__PURE__ */ new Set();
|
|
168
|
+
for (const n of nodes) if (n !== null) allNodes.add(n);
|
|
169
|
+
const nodeRank = /* @__PURE__ */ new Map();
|
|
170
|
+
for (const n of allNodes) nodeRank.set(n, 0);
|
|
171
|
+
for (let pass = 0; pass < allNodes.size; pass++) {
|
|
172
|
+
let changed = false;
|
|
173
|
+
for (const [from, es] of outbound) {
|
|
174
|
+
const base = nodeRank.get(from) ?? 0;
|
|
175
|
+
for (const e of es) {
|
|
176
|
+
const next = base + 1;
|
|
177
|
+
if (next > (nodeRank.get(e.to) ?? 0)) {
|
|
178
|
+
nodeRank.set(e.to, next);
|
|
179
|
+
changed = true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (!changed) break;
|
|
184
|
+
}
|
|
185
|
+
const nodeLane = /* @__PURE__ */ new Map();
|
|
186
|
+
const edgeLane = /* @__PURE__ */ new Map();
|
|
187
|
+
let totalLanes = 0;
|
|
188
|
+
for (const componentNodes of components) {
|
|
189
|
+
const componentSet = new Set(componentNodes);
|
|
190
|
+
let nextLane = 0;
|
|
191
|
+
const roots = [];
|
|
192
|
+
for (const n of componentNodes) if ((inbound.get(n) ?? []).length === 0) roots.push(n);
|
|
193
|
+
roots.sort((a, b) => {
|
|
194
|
+
if (a === EMPTY_CONTRACT_HASH) return -1;
|
|
195
|
+
if (b === EMPTY_CONTRACT_HASH) return 1;
|
|
196
|
+
return a.localeCompare(b);
|
|
197
|
+
});
|
|
198
|
+
const bfsQueue = [];
|
|
199
|
+
for (const root of roots) if (!nodeLane.has(root)) {
|
|
200
|
+
nodeLane.set(root, nextLane++);
|
|
201
|
+
bfsQueue.push({
|
|
202
|
+
node: root,
|
|
203
|
+
lane: nodeLane.get(root)
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
let head = 0;
|
|
207
|
+
while (head < bfsQueue.length) {
|
|
208
|
+
const { node, lane } = bfsQueue[head++];
|
|
209
|
+
const children = outbound.get(node) ?? [];
|
|
210
|
+
let first = true;
|
|
211
|
+
for (const childEdge of children) {
|
|
212
|
+
const child = childEdge.to;
|
|
213
|
+
if (!componentSet.has(child)) continue;
|
|
214
|
+
if (!nodeLane.has(child)) {
|
|
215
|
+
const childLane = first ? lane : nextLane++;
|
|
216
|
+
nodeLane.set(child, childLane);
|
|
217
|
+
bfsQueue.push({
|
|
218
|
+
node: child,
|
|
219
|
+
lane: childLane
|
|
220
|
+
});
|
|
221
|
+
edgeLane.set(childEdge.migrationHash, childLane);
|
|
222
|
+
} else edgeLane.set(childEdge.migrationHash, Math.max(lane, nodeLane.get(child)));
|
|
223
|
+
first = false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
for (const n of componentNodes) if (!nodeLane.has(n)) nodeLane.set(n, nextLane++);
|
|
227
|
+
for (const n of componentNodes) {
|
|
228
|
+
const parents = inbound.get(n);
|
|
229
|
+
if (!parents || parents.length <= 1) continue;
|
|
230
|
+
let trunkParent = parents[0].from;
|
|
231
|
+
let trunkRank = nodeRank.get(trunkParent) ?? 0;
|
|
232
|
+
let trunkLane = nodeLane.get(trunkParent) ?? 0;
|
|
233
|
+
for (let i = 1; i < parents.length; i++) {
|
|
234
|
+
const parent = parents[i].from;
|
|
235
|
+
const rank = nodeRank.get(parent) ?? 0;
|
|
236
|
+
const lane = nodeLane.get(parent) ?? 0;
|
|
237
|
+
if (rank > trunkRank || rank === trunkRank && lane < trunkLane) {
|
|
238
|
+
trunkParent = parent;
|
|
239
|
+
trunkRank = rank;
|
|
240
|
+
trunkLane = lane;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const trunkParentLane = nodeLane.get(trunkParent) ?? 0;
|
|
244
|
+
const currentNodeLane = nodeLane.get(n) ?? 0;
|
|
245
|
+
if (currentNodeLane === trunkParentLane) continue;
|
|
246
|
+
nodeLane.set(n, trunkParentLane);
|
|
247
|
+
for (const parentEdge of parents) if (parentEdge.from === trunkParent) edgeLane.set(parentEdge.migrationHash, trunkParentLane);
|
|
248
|
+
const bfsDescendants = [n];
|
|
249
|
+
let descHead = 0;
|
|
250
|
+
while (descHead < bfsDescendants.length) {
|
|
251
|
+
const current = bfsDescendants[descHead++];
|
|
252
|
+
const children = outbound.get(current) ?? [];
|
|
253
|
+
for (const childEdge of children) {
|
|
254
|
+
const child = childEdge.to;
|
|
255
|
+
if (!componentSet.has(child)) continue;
|
|
256
|
+
if ((nodeLane.get(child) ?? 0) !== currentNodeLane) continue;
|
|
257
|
+
nodeLane.set(child, trunkParentLane);
|
|
258
|
+
const existingEdgeLane = edgeLane.get(childEdge.migrationHash);
|
|
259
|
+
if (existingEdgeLane !== void 0 && existingEdgeLane === currentNodeLane) edgeLane.set(childEdge.migrationHash, trunkParentLane);
|
|
260
|
+
bfsDescendants.push(child);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (nextLane > totalLanes) totalLanes = nextLane;
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
nodeLane,
|
|
268
|
+
nodeRank,
|
|
269
|
+
edgeLane,
|
|
270
|
+
numLanes: totalLanes
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function computeDisplayOrder(nodes, nodeLane, nodeRank) {
|
|
274
|
+
const seen = /* @__PURE__ */ new Set();
|
|
275
|
+
const result = [];
|
|
276
|
+
let componentBuffer = [];
|
|
277
|
+
function flushComponent() {
|
|
278
|
+
componentBuffer.sort((a, b) => b.rank - a.rank || a.lane - b.lane);
|
|
279
|
+
for (const d of componentBuffer) result.push(d);
|
|
280
|
+
componentBuffer = [];
|
|
281
|
+
}
|
|
282
|
+
for (const n of nodes) {
|
|
283
|
+
if (n === null) {
|
|
284
|
+
flushComponent();
|
|
285
|
+
result.push(null);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (seen.has(n)) continue;
|
|
289
|
+
seen.add(n);
|
|
290
|
+
componentBuffer.push({
|
|
291
|
+
hash: n,
|
|
292
|
+
lane: nodeLane.get(n) ?? 0,
|
|
293
|
+
rank: nodeRank.get(n) ?? 0
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
flushComponent();
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
/** Create an empty cell. */
|
|
300
|
+
function emptyCell() {
|
|
301
|
+
return { lines: [] };
|
|
302
|
+
}
|
|
303
|
+
function buildGrid(rowModel, opts = {}, highlight = {
|
|
304
|
+
mode: "flat",
|
|
305
|
+
onPath: /* @__PURE__ */ new Set()
|
|
306
|
+
}) {
|
|
307
|
+
const colsPerLane = opts.colsPerLane ?? 2;
|
|
308
|
+
const isFocus = highlight.mode === "focus";
|
|
309
|
+
const { nodeLane, nodeRank, edgeLane, numLanes } = buildLaneAssignment(rowModel.nodes, rowModel.edges);
|
|
310
|
+
const displayOrder = computeDisplayOrder(rowModel.nodes, nodeLane, nodeRank);
|
|
311
|
+
const displayIndex = /* @__PURE__ */ new Map();
|
|
312
|
+
let nodeIdx = 0;
|
|
313
|
+
for (const d of displayOrder) if (d !== null) displayIndex.set(d.hash, nodeIdx++);
|
|
314
|
+
const rollbackEdges = rowModel.edges.filter((e) => e.kind === "rollback" && e.from !== e.to);
|
|
315
|
+
const adjacentRollbacks = [];
|
|
316
|
+
const skippingRollbacks = [];
|
|
317
|
+
for (const e of rollbackEdges) {
|
|
318
|
+
const si = displayIndex.get(e.from);
|
|
319
|
+
const ti = displayIndex.get(e.to);
|
|
320
|
+
if (si === void 0 || ti === void 0) continue;
|
|
321
|
+
if (ti === si + 1) adjacentRollbacks.push(e);
|
|
322
|
+
else skippingRollbacks.push(e);
|
|
323
|
+
}
|
|
324
|
+
const targetGroups = /* @__PURE__ */ new Map();
|
|
325
|
+
for (const e of skippingRollbacks) {
|
|
326
|
+
const group = targetGroups.get(e.to);
|
|
327
|
+
if (group) group.push(e);
|
|
328
|
+
else targetGroups.set(e.to, [e]);
|
|
329
|
+
}
|
|
330
|
+
const sortedTargetKeys = [...targetGroups.keys()].sort((a, b) => {
|
|
331
|
+
const ta = displayIndex.get(a) ?? 0;
|
|
332
|
+
return (displayIndex.get(b) ?? 0) - ta;
|
|
333
|
+
});
|
|
334
|
+
const numTargetGroups = sortedTargetKeys.length;
|
|
335
|
+
const geomLaneOf = /* @__PURE__ */ new Map();
|
|
336
|
+
const outermostGroup = numLanes + numTargetGroups - 1;
|
|
337
|
+
sortedTargetKeys.forEach((targetHash, i) => {
|
|
338
|
+
const groupGeomLane = outermostGroup - i;
|
|
339
|
+
for (const e of targetGroups.get(targetHash)) geomLaneOf.set(e.migrationHash, groupGeomLane);
|
|
340
|
+
});
|
|
341
|
+
const totalDisplayNodes = displayOrder.filter((d) => d !== null).length;
|
|
342
|
+
const planeLaneOf = /* @__PURE__ */ new Map();
|
|
343
|
+
for (const e of skippingRollbacks) {
|
|
344
|
+
const si = displayIndex.get(e.from) ?? 0;
|
|
345
|
+
planeLaneOf.set(e.migrationHash, totalDisplayNodes - si);
|
|
346
|
+
}
|
|
347
|
+
const PALETTE_SIZE = 6;
|
|
348
|
+
const GREEN_PALETTE_IDX = 5;
|
|
349
|
+
const arcSourceIndex = /* @__PURE__ */ new Map();
|
|
350
|
+
const arcTargetIndex = /* @__PURE__ */ new Map();
|
|
351
|
+
for (const e of skippingRollbacks) {
|
|
352
|
+
arcSourceIndex.set(e.migrationHash, displayIndex.get(e.from) ?? 0);
|
|
353
|
+
arcTargetIndex.set(e.migrationHash, displayIndex.get(e.to) ?? 0);
|
|
354
|
+
}
|
|
355
|
+
const arcsByTarget = /* @__PURE__ */ new Map();
|
|
356
|
+
const arcsBySource = /* @__PURE__ */ new Map();
|
|
357
|
+
for (const e of skippingRollbacks) {
|
|
358
|
+
const tb = arcsByTarget.get(e.to);
|
|
359
|
+
if (tb) tb.push(e);
|
|
360
|
+
else arcsByTarget.set(e.to, [e]);
|
|
361
|
+
const sb = arcsBySource.get(e.from);
|
|
362
|
+
if (sb) sb.push(e);
|
|
363
|
+
else arcsBySource.set(e.from, [e]);
|
|
364
|
+
}
|
|
365
|
+
const colourLaneOf = /* @__PURE__ */ new Map();
|
|
366
|
+
const activeArcColours = /* @__PURE__ */ new Map();
|
|
367
|
+
const activeFwdLaneColours = /* @__PURE__ */ new Set();
|
|
368
|
+
for (let i = displayOrder.length - 1; i >= 0; i--) {
|
|
369
|
+
const nd = displayOrder[i];
|
|
370
|
+
if (nd === null || nd === void 0) continue;
|
|
371
|
+
const { hash: nodeHash } = nd;
|
|
372
|
+
const nodeFwdLane = nodeLane.get(nodeHash) ?? 0;
|
|
373
|
+
activeFwdLaneColours.add(nodeFwdLane % PALETTE_SIZE);
|
|
374
|
+
const sortedIncoming = [...arcsByTarget.get(nodeHash) ?? []].sort((a, b) => a.dirName.localeCompare(b.dirName));
|
|
375
|
+
for (const arc of sortedIncoming) {
|
|
376
|
+
const originLaneColour = (nodeLane.get(arc.from) ?? 0) % PALETTE_SIZE;
|
|
377
|
+
const occupied = new Set(activeFwdLaneColours);
|
|
378
|
+
for (const c of activeArcColours.values()) occupied.add(c);
|
|
379
|
+
occupied.add(GREEN_PALETTE_IDX);
|
|
380
|
+
occupied.add(originLaneColour);
|
|
381
|
+
let chosen = -1;
|
|
382
|
+
for (let ci = 0; ci < PALETTE_SIZE; ci++) if (!occupied.has(ci)) {
|
|
383
|
+
chosen = ci;
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
if (chosen === -1) {
|
|
387
|
+
for (let ci = 0; ci < PALETTE_SIZE; ci++) if (ci !== GREEN_PALETTE_IDX) {
|
|
388
|
+
chosen = ci;
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
colourLaneOf.set(arc.migrationHash, chosen === -1 ? 0 : chosen);
|
|
393
|
+
activeArcColours.set(arc.migrationHash, chosen === -1 ? 0 : chosen);
|
|
394
|
+
}
|
|
395
|
+
for (const arc of arcsBySource.get(nodeHash) ?? []) activeArcColours.delete(arc.migrationHash);
|
|
396
|
+
}
|
|
397
|
+
const routedBackArcs = skippingRollbacks.map((e) => ({
|
|
398
|
+
edge: e,
|
|
399
|
+
sourceIndex: displayIndex.get(e.from) ?? 0,
|
|
400
|
+
targetIndex: displayIndex.get(e.to) ?? 0,
|
|
401
|
+
geomLane: geomLaneOf.get(e.migrationHash) ?? numLanes,
|
|
402
|
+
colourLane: colourLaneOf.get(e.migrationHash) ?? 0,
|
|
403
|
+
planeLane: planeLaneOf.get(e.migrationHash) ?? numLanes
|
|
404
|
+
}));
|
|
405
|
+
const backArcsBySource = /* @__PURE__ */ new Map();
|
|
406
|
+
const backArcsByTarget = /* @__PURE__ */ new Map();
|
|
407
|
+
for (const arc of routedBackArcs) {
|
|
408
|
+
const sb = backArcsBySource.get(arc.edge.from);
|
|
409
|
+
if (sb) sb.push(arc);
|
|
410
|
+
else backArcsBySource.set(arc.edge.from, [arc]);
|
|
411
|
+
const tb = backArcsByTarget.get(arc.edge.to);
|
|
412
|
+
if (tb) tb.push(arc);
|
|
413
|
+
else backArcsByTarget.set(arc.edge.to, [arc]);
|
|
414
|
+
}
|
|
415
|
+
const adjacentBySource = /* @__PURE__ */ new Map();
|
|
416
|
+
const adjacentByTarget = /* @__PURE__ */ new Map();
|
|
417
|
+
for (const e of adjacentRollbacks) {
|
|
418
|
+
const b = adjacentBySource.get(e.from);
|
|
419
|
+
if (b) b.push(e);
|
|
420
|
+
else adjacentBySource.set(e.from, [e]);
|
|
421
|
+
const t = adjacentByTarget.get(e.to);
|
|
422
|
+
if (t) t.push(e);
|
|
423
|
+
else adjacentByTarget.set(e.to, [e]);
|
|
424
|
+
}
|
|
425
|
+
for (const list of adjacentBySource.values()) list.sort((a, b) => a.dirName.localeCompare(b.dirName));
|
|
426
|
+
const totalCols = (numLanes + numTargetGroups) * colsPerLane;
|
|
427
|
+
const fwdEdges = rowModel.edges.filter((e) => e.kind === "forward" && e.from !== e.to);
|
|
428
|
+
const selfEdges = rowModel.edges.filter((e) => e.kind === "self");
|
|
429
|
+
const outboundFwd = /* @__PURE__ */ new Map();
|
|
430
|
+
const inboundFwd = /* @__PURE__ */ new Map();
|
|
431
|
+
for (const e of fwdEdges) {
|
|
432
|
+
const ob = outboundFwd.get(e.from);
|
|
433
|
+
if (ob) ob.push(e);
|
|
434
|
+
else outboundFwd.set(e.from, [e]);
|
|
435
|
+
const ib = inboundFwd.get(e.to);
|
|
436
|
+
if (ib) ib.push(e);
|
|
437
|
+
else inboundFwd.set(e.to, [e]);
|
|
438
|
+
}
|
|
439
|
+
for (const list of outboundFwd.values()) list.sort((a, b) => a.dirName.localeCompare(b.dirName));
|
|
440
|
+
for (const list of inboundFwd.values()) list.sort((a, b) => a.dirName.localeCompare(b.dirName));
|
|
441
|
+
const selfEdgesByNode = /* @__PURE__ */ new Map();
|
|
442
|
+
for (const e of selfEdges) {
|
|
443
|
+
const bucket = selfEdgesByNode.get(e.from);
|
|
444
|
+
if (bucket) bucket.push(e);
|
|
445
|
+
else selfEdgesByNode.set(e.from, [e]);
|
|
446
|
+
}
|
|
447
|
+
for (const list of selfEdgesByNode.values()) list.sort((a, b) => a.dirName.localeCompare(b.dirName));
|
|
448
|
+
function roleOf(migrationHash) {
|
|
449
|
+
if (!isFocus) return void 0;
|
|
450
|
+
return highlight.onPath.has(migrationHash) ? "on-path" : "off-path";
|
|
451
|
+
}
|
|
452
|
+
const onPathNodes = /* @__PURE__ */ new Set();
|
|
453
|
+
if (isFocus) {
|
|
454
|
+
for (const e of [
|
|
455
|
+
...fwdEdges,
|
|
456
|
+
...selfEdges,
|
|
457
|
+
...rollbackEdges
|
|
458
|
+
]) if (highlight.onPath.has(e.migrationHash)) {
|
|
459
|
+
onPathNodes.add(e.from);
|
|
460
|
+
onPathNodes.add(e.to);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function nodeRoleOf(hash) {
|
|
464
|
+
if (!isFocus) return void 0;
|
|
465
|
+
return onPathNodes.has(hash) ? "on-path" : "off-path";
|
|
466
|
+
}
|
|
467
|
+
function planeOf(lane, role) {
|
|
468
|
+
if (!isFocus) return lane;
|
|
469
|
+
return role === "on-path" ? 0 : lane + 1;
|
|
470
|
+
}
|
|
471
|
+
function lineRefFor(edge, lane) {
|
|
472
|
+
return {
|
|
473
|
+
migrationHash: edge.migrationHash,
|
|
474
|
+
dirName: edge.dirName,
|
|
475
|
+
lane,
|
|
476
|
+
role: roleOf(edge.migrationHash)
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
/** Synthetic LineRef for a lane carrying a representative edge's role (pass-through). */
|
|
480
|
+
function passLineRef(lane, dirName, migHash) {
|
|
481
|
+
return {
|
|
482
|
+
migrationHash: migHash,
|
|
483
|
+
dirName,
|
|
484
|
+
lane,
|
|
485
|
+
role: roleOf(migHash)
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function vertCell(line) {
|
|
489
|
+
return { lines: [{
|
|
490
|
+
line,
|
|
491
|
+
directions: new Set(["up", "down"]),
|
|
492
|
+
plane: planeOf(line.lane, line.role)
|
|
493
|
+
}] };
|
|
494
|
+
}
|
|
495
|
+
function dirCell(line, dirs) {
|
|
496
|
+
return { lines: [{
|
|
497
|
+
line,
|
|
498
|
+
directions: dirs,
|
|
499
|
+
plane: planeOf(line.lane, line.role)
|
|
500
|
+
}] };
|
|
501
|
+
}
|
|
502
|
+
function nodeCell(nodeRef) {
|
|
503
|
+
return {
|
|
504
|
+
node: nodeRef,
|
|
505
|
+
lines: []
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
const laneCurrentEdge = /* @__PURE__ */ new Map();
|
|
509
|
+
function getRepLine(lane) {
|
|
510
|
+
const e = laneCurrentEdge.get(lane);
|
|
511
|
+
if (e) return lineRefFor(e, lane);
|
|
512
|
+
return passLineRef(lane, `lane${lane}`, `lane${lane}`);
|
|
513
|
+
}
|
|
514
|
+
const activeLanes = /* @__PURE__ */ new Set();
|
|
515
|
+
const grid = [];
|
|
516
|
+
function makeRow() {
|
|
517
|
+
return Array.from({ length: totalCols }, () => emptyCell());
|
|
518
|
+
}
|
|
519
|
+
function placeVerticals(row, skip) {
|
|
520
|
+
for (const lane of activeLanes) {
|
|
521
|
+
if (skip.has(lane)) continue;
|
|
522
|
+
const railCol = lane * colsPerLane;
|
|
523
|
+
const cell = row[railCol];
|
|
524
|
+
if (cell !== void 0 && cell.lines.length === 0 && !cell.node) row[railCol] = vertCell(getRepLine(lane));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const activeBackArcs = /* @__PURE__ */ new Set();
|
|
528
|
+
function backArcLine(arc) {
|
|
529
|
+
return {
|
|
530
|
+
migrationHash: arc.edge.migrationHash,
|
|
531
|
+
dirName: arc.edge.dirName,
|
|
532
|
+
lane: arc.colourLane,
|
|
533
|
+
role: roleOf(arc.edge.migrationHash)
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function backArcPlane(arc) {
|
|
537
|
+
const role = roleOf(arc.edge.migrationHash);
|
|
538
|
+
if (!isFocus) return arc.planeLane;
|
|
539
|
+
return role === "on-path" ? 0 : arc.planeLane + 1;
|
|
540
|
+
}
|
|
541
|
+
function composeLine(row, col, line, dirs, plane, extra) {
|
|
542
|
+
const existing = row[col];
|
|
543
|
+
const cellLine = {
|
|
544
|
+
line,
|
|
545
|
+
directions: dirs,
|
|
546
|
+
plane,
|
|
547
|
+
...extra?.landingArrow ? { landingArrow: true } : {}
|
|
548
|
+
};
|
|
549
|
+
if (existing && (existing.lines.length > 0 || existing.node)) row[col] = {
|
|
550
|
+
...existing,
|
|
551
|
+
lines: [...existing.lines, cellLine]
|
|
552
|
+
};
|
|
553
|
+
else row[col] = { lines: [cellLine] };
|
|
554
|
+
}
|
|
555
|
+
function placeBackVerticals(row) {
|
|
556
|
+
for (const arc of activeBackArcs) composeLine(row, arc.geomLane * colsPerLane, backArcLine(arc), new Set(["up", "down"]), backArcPlane(arc));
|
|
557
|
+
placeAdjacentOverlays(row);
|
|
558
|
+
}
|
|
559
|
+
const activeAdjacent = /* @__PURE__ */ new Set();
|
|
560
|
+
function placeAdjacentOverlays(row) {
|
|
561
|
+
for (const adj of activeAdjacent) {
|
|
562
|
+
const railCol = adj.lane * colsPerLane;
|
|
563
|
+
if (row[railCol]?.node) continue;
|
|
564
|
+
const line = lineRefFor(adj.edge, adj.lane);
|
|
565
|
+
composeLine(row, railCol, line, new Set(["up", "down"]), planeOf(adj.lane, line.role));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
function emitBackArcTee(row, nodeLaneNum, arc) {
|
|
569
|
+
const nodeRail = nodeLaneNum * colsPerLane;
|
|
570
|
+
const geomRail = arc.geomLane * colsPerLane;
|
|
571
|
+
const line = backArcLine(arc);
|
|
572
|
+
const plane = backArcPlane(arc);
|
|
573
|
+
for (let col = nodeRail + 1; col < geomRail; col++) composeLine(row, col, line, new Set(["left", "right"]), plane);
|
|
574
|
+
composeLine(row, geomRail, line, new Set(["down", "left"]), plane);
|
|
575
|
+
}
|
|
576
|
+
function emitBackArcLanding(row, nodeLaneNum, arc) {
|
|
577
|
+
const nodeRail = nodeLaneNum * colsPerLane;
|
|
578
|
+
const geomRail = arc.geomLane * colsPerLane;
|
|
579
|
+
const line = backArcLine(arc);
|
|
580
|
+
const plane = backArcPlane(arc);
|
|
581
|
+
composeLine(row, nodeRail + 1, line, new Set(["left", "right"]), plane, { landingArrow: true });
|
|
582
|
+
for (let col = nodeRail + 2; col < geomRail; col++) composeLine(row, col, line, new Set(["left", "right"]), plane);
|
|
583
|
+
composeLine(row, geomRail, line, new Set(["up", "left"]), plane);
|
|
584
|
+
}
|
|
585
|
+
function emitConnectorRow(trunkLane, branchEntries, connectorType, trunkEdge) {
|
|
586
|
+
const row = makeRow();
|
|
587
|
+
const sorted = [...branchEntries].sort((a, b) => a.lane - b.lane);
|
|
588
|
+
if (sorted.length === 0) return row;
|
|
589
|
+
const branchByLane = /* @__PURE__ */ new Map();
|
|
590
|
+
for (const b of sorted) branchByLane.set(b.lane, b.edge);
|
|
591
|
+
let continuousLane = trunkLane;
|
|
592
|
+
if (isFocus) if (trunkEdge && highlight.onPath.has(trunkEdge.migrationHash)) continuousLane = trunkLane;
|
|
593
|
+
else {
|
|
594
|
+
const onPathBranch = sorted.find((b) => highlight.onPath.has(b.edge.migrationHash));
|
|
595
|
+
if (onPathBranch) continuousLane = onPathBranch.lane;
|
|
596
|
+
}
|
|
597
|
+
const trunkRailCol = trunkLane * colsPerLane;
|
|
598
|
+
const continuousRailCol = continuousLane * colsPerLane;
|
|
599
|
+
function addLine(col, line, dirs) {
|
|
600
|
+
const existing = row[col];
|
|
601
|
+
const cellLine = {
|
|
602
|
+
line,
|
|
603
|
+
directions: dirs,
|
|
604
|
+
plane: planeOf(line.lane, line.role)
|
|
605
|
+
};
|
|
606
|
+
row[col] = existing && existing.lines.length > 0 ? {
|
|
607
|
+
...existing,
|
|
608
|
+
lines: [...existing.lines, cellLine]
|
|
609
|
+
} : { lines: [cellLine] };
|
|
610
|
+
}
|
|
611
|
+
const cornerLeftDown = connectorType === "merge" ? new Set(["left", "down"]) : new Set(["left", "up"]);
|
|
612
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
613
|
+
const b = sorted[i];
|
|
614
|
+
if (b.lane === continuousLane) continue;
|
|
615
|
+
const branchLine = lineRefFor(b.edge, b.lane);
|
|
616
|
+
const railCol = b.lane * colsPerLane;
|
|
617
|
+
addLine(railCol, branchLine, cornerLeftDown);
|
|
618
|
+
const leftBound = i === 0 ? trunkRailCol + 1 : sorted[i - 1].lane * colsPerLane + 1;
|
|
619
|
+
for (let col = leftBound; col < railCol; col++) addLine(col, branchLine, new Set(["left", "right"]));
|
|
620
|
+
}
|
|
621
|
+
const continuousLine = continuousLane === trunkLane ? trunkEdge ? lineRefFor(trunkEdge, trunkLane) : getRepLine(trunkLane) : lineRefFor(branchByLane.get(continuousLane), continuousLane);
|
|
622
|
+
if (continuousLane === trunkLane) addLine(trunkRailCol, continuousLine, new Set(["up", "down"]));
|
|
623
|
+
else {
|
|
624
|
+
addLine(trunkRailCol, continuousLine, connectorType === "merge" ? new Set(["up", "right"]) : new Set(["down", "right"]));
|
|
625
|
+
for (let col = trunkRailCol + 1; col < continuousRailCol; col++) addLine(col, continuousLine, new Set(["left", "right"]));
|
|
626
|
+
addLine(continuousRailCol, continuousLine, cornerLeftDown);
|
|
627
|
+
}
|
|
628
|
+
placeVerticals(row, new Set([trunkLane, ...sorted.map((b) => b.lane)]));
|
|
629
|
+
placeBackVerticals(row);
|
|
630
|
+
return row;
|
|
631
|
+
}
|
|
632
|
+
for (const nodeDisplay of displayOrder) {
|
|
633
|
+
if (nodeDisplay === null) {
|
|
634
|
+
const sepRow = makeRow();
|
|
635
|
+
sepRow[0] = {
|
|
636
|
+
lines: [],
|
|
637
|
+
separator: true
|
|
638
|
+
};
|
|
639
|
+
grid.push(sepRow);
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
const { hash: nodeHash } = nodeDisplay;
|
|
643
|
+
const nodeLaneNum = nodeLane.get(nodeHash) ?? 0;
|
|
644
|
+
activeLanes.add(nodeLaneNum);
|
|
645
|
+
const outEdges = outboundFwd.get(nodeHash) ?? [];
|
|
646
|
+
if (outEdges.length > 1) {
|
|
647
|
+
const trunkEdgeForFork = outEdges[0];
|
|
648
|
+
const trunkChildLane = edgeLane.get(trunkEdgeForFork.migrationHash) ?? nodeLane.get(trunkEdgeForFork.to) ?? nodeLaneNum;
|
|
649
|
+
const branchEntries = outEdges.slice(1).map((e) => ({
|
|
650
|
+
lane: edgeLane.get(e.migrationHash) ?? nodeLane.get(e.to) ?? 0,
|
|
651
|
+
edge: e
|
|
652
|
+
})).filter((b) => b.lane !== trunkChildLane && activeLanes.has(b.lane));
|
|
653
|
+
if (branchEntries.length > 0) {
|
|
654
|
+
const trunkEdge = outEdges[0];
|
|
655
|
+
const connRow = emitConnectorRow(nodeLaneNum, branchEntries, "fork", trunkEdge);
|
|
656
|
+
grid.push(connRow);
|
|
657
|
+
assertSingleOwner(connRow, isFocus);
|
|
658
|
+
for (const b of branchEntries) activeLanes.delete(b.lane);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const selfMigrations = selfEdgesByNode.get(nodeHash) ?? [];
|
|
662
|
+
for (const selfEdge of selfMigrations) {
|
|
663
|
+
const row = makeRow();
|
|
664
|
+
const railCol = nodeLaneNum * colsPerLane;
|
|
665
|
+
const connCol = nodeLaneNum * colsPerLane + 1;
|
|
666
|
+
const line = lineRefFor(selfEdge, nodeLaneNum);
|
|
667
|
+
row[railCol] = vertCell(line);
|
|
668
|
+
row[connCol] = { lines: [{
|
|
669
|
+
line,
|
|
670
|
+
directions: /* @__PURE__ */ new Set(),
|
|
671
|
+
plane: planeOf(nodeLaneNum, line.role),
|
|
672
|
+
selfLoop: true
|
|
673
|
+
}] };
|
|
674
|
+
placeVerticals(row, new Set([nodeLaneNum]));
|
|
675
|
+
placeBackVerticals(row);
|
|
676
|
+
grid.push(row);
|
|
677
|
+
}
|
|
678
|
+
{
|
|
679
|
+
const row = makeRow();
|
|
680
|
+
const railCol = nodeLaneNum * colsPerLane;
|
|
681
|
+
row[railCol] = nodeCell({
|
|
682
|
+
contractHash: nodeHash,
|
|
683
|
+
isEmpty: nodeHash === EMPTY_CONTRACT_HASH,
|
|
684
|
+
lane: nodeLaneNum,
|
|
685
|
+
role: nodeRoleOf(nodeHash)
|
|
686
|
+
});
|
|
687
|
+
placeVerticals(row, new Set([nodeLaneNum]));
|
|
688
|
+
const landingArcs = backArcsByTarget.get(nodeHash) ?? [];
|
|
689
|
+
for (const arc of landingArcs) activeBackArcs.delete(arc);
|
|
690
|
+
for (const adj of [...activeAdjacent]) if (adj.edge.to === nodeHash) activeAdjacent.delete(adj);
|
|
691
|
+
placeBackVerticals(row);
|
|
692
|
+
for (const arc of landingArcs) emitBackArcLanding(row, nodeLaneNum, arc);
|
|
693
|
+
const teeArcs = backArcsBySource.get(nodeHash) ?? [];
|
|
694
|
+
for (const arc of teeArcs) emitBackArcTee(row, nodeLaneNum, arc);
|
|
695
|
+
grid.push(row);
|
|
696
|
+
for (const arc of teeArcs) activeBackArcs.add(arc);
|
|
697
|
+
for (const adj of adjacentBySource.get(nodeHash) ?? []) activeAdjacent.add({
|
|
698
|
+
lane: nodeLaneNum,
|
|
699
|
+
edge: adj
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
function edgeLaneFor(edge) {
|
|
703
|
+
const override = edgeLane.get(edge.migrationHash);
|
|
704
|
+
if (override !== void 0) return override;
|
|
705
|
+
return Math.max(nodeLane.get(edge.from) ?? 0, nodeLane.get(edge.to) ?? 0);
|
|
706
|
+
}
|
|
707
|
+
const inEdges = inboundFwd.get(nodeHash) ?? [];
|
|
708
|
+
inEdges.sort((a, b) => {
|
|
709
|
+
const aLane = edgeLaneFor(a);
|
|
710
|
+
const bLane = edgeLaneFor(b);
|
|
711
|
+
if (aLane !== bLane) return aLane - bLane;
|
|
712
|
+
return a.dirName.localeCompare(b.dirName);
|
|
713
|
+
});
|
|
714
|
+
for (const edge of inEdges) laneCurrentEdge.set(edgeLaneFor(edge), edge);
|
|
715
|
+
{
|
|
716
|
+
const teeArcs = backArcsBySource.get(nodeHash) ?? [];
|
|
717
|
+
for (const arc of teeArcs) {
|
|
718
|
+
const row = makeRow();
|
|
719
|
+
const railCol = arc.geomLane * colsPerLane;
|
|
720
|
+
const connCol = railCol + 1;
|
|
721
|
+
const line = backArcLine(arc);
|
|
722
|
+
const plane = backArcPlane(arc);
|
|
723
|
+
composeLine(row, railCol, line, new Set(["up", "down"]), plane);
|
|
724
|
+
composeLine(row, connCol, line, new Set(["down"]), plane);
|
|
725
|
+
placeVerticals(row, /* @__PURE__ */ new Set());
|
|
726
|
+
placeBackVerticals(row);
|
|
727
|
+
grid.push(row);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
if (inEdges.length > 1) {
|
|
731
|
+
const branchEntries = inEdges.slice(1).map((e) => ({
|
|
732
|
+
lane: edgeLaneFor(e),
|
|
733
|
+
edge: e
|
|
734
|
+
}));
|
|
735
|
+
const trunkEdge = inEdges[0];
|
|
736
|
+
const connRow = emitConnectorRow(nodeLaneNum, branchEntries, "merge", trunkEdge);
|
|
737
|
+
grid.push(connRow);
|
|
738
|
+
assertSingleOwner(connRow, isFocus);
|
|
739
|
+
for (const b of branchEntries) activeLanes.add(b.lane);
|
|
740
|
+
}
|
|
741
|
+
for (const edge of inEdges) {
|
|
742
|
+
const eLane = edgeLaneFor(edge);
|
|
743
|
+
const row = makeRow();
|
|
744
|
+
const railCol = eLane * colsPerLane;
|
|
745
|
+
const connCol = eLane * colsPerLane + 1;
|
|
746
|
+
const line = lineRefFor(edge, eLane);
|
|
747
|
+
row[railCol] = vertCell(line);
|
|
748
|
+
row[connCol] = dirCell(line, new Set(["up"]));
|
|
749
|
+
placeVerticals(row, new Set([eLane]));
|
|
750
|
+
placeBackVerticals(row);
|
|
751
|
+
grid.push(row);
|
|
752
|
+
}
|
|
753
|
+
{
|
|
754
|
+
const adjacents = adjacentBySource.get(nodeHash) ?? [];
|
|
755
|
+
for (const adj of adjacents) {
|
|
756
|
+
const row = makeRow();
|
|
757
|
+
const connCol = nodeLaneNum * colsPerLane + 1;
|
|
758
|
+
const line = lineRefFor(adj, nodeLaneNum);
|
|
759
|
+
const plane = planeOf(nodeLaneNum, line.role);
|
|
760
|
+
composeLine(row, connCol, line, new Set(["down"]), plane);
|
|
761
|
+
placeVerticals(row, /* @__PURE__ */ new Set());
|
|
762
|
+
placeBackVerticals(row);
|
|
763
|
+
grid.push(row);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (inEdges.length === 0) activeLanes.delete(nodeLaneNum);
|
|
767
|
+
}
|
|
768
|
+
return grid;
|
|
769
|
+
}
|
|
770
|
+
function assertSingleOwner(row, isFocus) {
|
|
771
|
+
for (const cell of row) {
|
|
772
|
+
if (cell.lines.length <= 1) continue;
|
|
773
|
+
let topPlane = Number.POSITIVE_INFINITY;
|
|
774
|
+
for (const cl of cell.lines) if (cl.plane < topPlane) topPlane = cl.plane;
|
|
775
|
+
const top = cell.lines.filter((cl) => cl.plane === topPlane);
|
|
776
|
+
if (top.length > 1) {
|
|
777
|
+
if (isFocus) {
|
|
778
|
+
if (new Set(top.map((cl) => cl.line.role)).size > 1) throw new Error("migration-graph layout: single-owner invariant violated — two differently-roled lines share the top plane in one cell");
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
//#endregion
|
|
784
|
+
//#region src/utils/formatters/migration-list-graph-topology.ts
|
|
785
|
+
function compareDirNameDesc(a, b) {
|
|
786
|
+
return b.dirName.localeCompare(a.dirName);
|
|
787
|
+
}
|
|
788
|
+
function bumpDegree(map, key) {
|
|
789
|
+
map.set(key, (map.get(key) ?? 0) + 1);
|
|
790
|
+
}
|
|
791
|
+
function compareNodesRootFirst(a, b) {
|
|
792
|
+
if (a === EMPTY_CONTRACT_HASH) return -1;
|
|
793
|
+
if (b === EMPTY_CONTRACT_HASH) return 1;
|
|
794
|
+
return a.localeCompare(b);
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Shortest-path distance of each node from the forward roots, over the given
|
|
798
|
+
* candidate edges. Roots are the in-degree-0 nodes (baseline first, then lex);
|
|
799
|
+
* a rooted component therefore distances every node by how many forward steps
|
|
800
|
+
* it sits from a root. A component with no root (a pure cycle) is seeded from
|
|
801
|
+
* its single lexically-smallest node so the cycle still gets a stable layering.
|
|
802
|
+
*
|
|
803
|
+
* Crucially this is *shortest* path, not longest: a backward (rollback) edge
|
|
804
|
+
* `deep → shallow` never offers a shorter route to the already-shallower
|
|
805
|
+
* target, so it is inert here. Distances are thus stable whether or not the
|
|
806
|
+
* rollbacks are still in the candidate set — which is what lets the peel below
|
|
807
|
+
* tell a genuine back-edge (target strictly shallower than source) apart from a
|
|
808
|
+
* forward edge that merely happens to share the back-edge's cycle.
|
|
809
|
+
*/
|
|
810
|
+
function forwardDistances(nodes, candidates) {
|
|
811
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
812
|
+
for (const node of nodes) inDegree.set(node, 0);
|
|
813
|
+
for (const edge of candidates) bumpDegree(inDegree, edge.to);
|
|
814
|
+
const roots = [...nodes].filter((node) => (inDegree.get(node) ?? 0) === 0);
|
|
815
|
+
roots.sort(compareNodesRootFirst);
|
|
816
|
+
const seeds = roots.length > 0 ? roots : [...nodes].sort(compareNodesRootFirst).slice(0, 1);
|
|
817
|
+
const dist = /* @__PURE__ */ new Map();
|
|
818
|
+
for (const seed of seeds) dist.set(seed, 0);
|
|
819
|
+
const maxPasses = nodes.size;
|
|
820
|
+
for (let pass = 0; pass < maxPasses; pass++) {
|
|
821
|
+
let changed = false;
|
|
822
|
+
for (const edge of candidates) {
|
|
823
|
+
const base = dist.get(edge.from);
|
|
824
|
+
if (base === void 0) continue;
|
|
825
|
+
const next = base + 1;
|
|
826
|
+
if (next < (dist.get(edge.to) ?? Number.POSITIVE_INFINITY)) {
|
|
827
|
+
dist.set(edge.to, next);
|
|
828
|
+
changed = true;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (!changed) break;
|
|
832
|
+
}
|
|
833
|
+
for (const node of nodes) if (!dist.has(node)) dist.set(node, 0);
|
|
834
|
+
return dist;
|
|
835
|
+
}
|
|
836
|
+
function canReachForward(start, goal, candidates) {
|
|
837
|
+
if (start === goal) return true;
|
|
838
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
839
|
+
for (const edge of candidates) {
|
|
840
|
+
const bucket = outgoing.get(edge.from);
|
|
841
|
+
if (bucket) bucket.push(edge.to);
|
|
842
|
+
else outgoing.set(edge.from, [edge.to]);
|
|
843
|
+
}
|
|
844
|
+
const visited = new Set([start]);
|
|
845
|
+
const queue = [start];
|
|
846
|
+
while (queue.length > 0) {
|
|
847
|
+
const node = queue.shift();
|
|
848
|
+
if (node === void 0) continue;
|
|
849
|
+
for (const next of outgoing.get(node) ?? []) {
|
|
850
|
+
if (next === goal) return true;
|
|
851
|
+
if (!visited.has(next)) {
|
|
852
|
+
visited.add(next);
|
|
853
|
+
queue.push(next);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return false;
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Demote node-skipping rollbacks left forward by the DFS. An edge `from → to`
|
|
861
|
+
* is a rollback exactly when both hold:
|
|
862
|
+
* 1. `to` is a forward-ancestor of `from` — `to` can still reach `from` over
|
|
863
|
+
* the other forward edges, so the edge closes a cycle; and
|
|
864
|
+
* 2. `to` is strictly shallower than `from` (smaller forward distance) — the
|
|
865
|
+
* edge points back toward the root rather than advancing history.
|
|
866
|
+
*
|
|
867
|
+
* Condition 2 is the discriminator: in a cycle created by a rollback every edge
|
|
868
|
+
* satisfies condition 1, but only the rollback itself runs deep → shallow. The
|
|
869
|
+
* forward chain edges run shallow → deep and are never peeled, however many
|
|
870
|
+
* rollbacks converge on the same target. Tight back-edges whose source and
|
|
871
|
+
* target sit at the same distance (mutual two-node cycles) are already resolved
|
|
872
|
+
* by the DFS immediate-parent rule, so they never reach this pass. One edge is
|
|
873
|
+
* peeled per iteration (dirName-descending tie-break) and distances/reachability
|
|
874
|
+
* are recomputed, making the outcome independent of edge input order.
|
|
875
|
+
*/
|
|
876
|
+
function peelNodeSkippingRollbacks(nodes, kindByMigrationHash, nonSelf) {
|
|
877
|
+
let candidates = nonSelf.filter((edge) => kindByMigrationHash.get(edge.hash) === "forward");
|
|
878
|
+
while (candidates.length > 0) {
|
|
879
|
+
const dist = forwardDistances(nodes, candidates);
|
|
880
|
+
const backEdges = candidates.filter((edge) => {
|
|
881
|
+
if ((dist.get(edge.to) ?? 0) >= (dist.get(edge.from) ?? 0)) return false;
|
|
882
|
+
const without = candidates.filter((candidate) => candidate !== edge);
|
|
883
|
+
return canReachForward(edge.to, edge.from, without);
|
|
884
|
+
});
|
|
885
|
+
if (backEdges.length === 0) break;
|
|
886
|
+
backEdges.sort(compareDirNameDesc);
|
|
887
|
+
const rollback = backEdges[0];
|
|
888
|
+
if (rollback === void 0) break;
|
|
889
|
+
kindByMigrationHash.set(rollback.hash, "rollback");
|
|
890
|
+
candidates = candidates.filter((edge) => edge !== rollback);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* DFS with dirName-descending traversal. A GRAY target is a rollback only when it
|
|
895
|
+
* is the immediate DFS parent of the source — cross-links to other GRAY nodes
|
|
896
|
+
* stay forward. A follow-up peel pass demotes node-skipping rollbacks (target is
|
|
897
|
+
* a forward-ancestor of the source and sits strictly shallower than it).
|
|
898
|
+
*/
|
|
899
|
+
function classifyNormalizedEdges(edges) {
|
|
900
|
+
const nodes = /* @__PURE__ */ new Set();
|
|
901
|
+
const kindByMigrationHash = /* @__PURE__ */ new Map();
|
|
902
|
+
const outgoingByFrom = /* @__PURE__ */ new Map();
|
|
903
|
+
const nonSelf = [];
|
|
904
|
+
for (const edge of edges) {
|
|
905
|
+
nodes.add(edge.from);
|
|
906
|
+
nodes.add(edge.to);
|
|
907
|
+
if (edge.from === edge.to) {
|
|
908
|
+
kindByMigrationHash.set(edge.hash, "self");
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
nonSelf.push(edge);
|
|
912
|
+
const bucket = outgoingByFrom.get(edge.from);
|
|
913
|
+
if (bucket) bucket.push(edge);
|
|
914
|
+
else outgoingByFrom.set(edge.from, [edge]);
|
|
915
|
+
}
|
|
916
|
+
for (const bucket of outgoingByFrom.values()) bucket.sort(compareDirNameDesc);
|
|
917
|
+
const nonSelfInDegree = /* @__PURE__ */ new Map();
|
|
918
|
+
for (const node of nodes) nonSelfInDegree.set(node, 0);
|
|
919
|
+
for (const bucket of outgoingByFrom.values()) for (const edge of bucket) bumpDegree(nonSelfInDegree, edge.to);
|
|
920
|
+
const dfsRoots = [];
|
|
921
|
+
for (const node of nodes) if ((nonSelfInDegree.get(node) ?? 0) === 0) dfsRoots.push(node);
|
|
922
|
+
dfsRoots.sort((a, b) => {
|
|
923
|
+
if (a === EMPTY_CONTRACT_HASH) return -1;
|
|
924
|
+
if (b === EMPTY_CONTRACT_HASH) return 1;
|
|
925
|
+
return a.localeCompare(b);
|
|
926
|
+
});
|
|
927
|
+
if (dfsRoots.length === 0) dfsRoots.push(...[...nodes].sort((a, b) => a.localeCompare(b)));
|
|
928
|
+
const WHITE = 0;
|
|
929
|
+
const GRAY = 1;
|
|
930
|
+
const BLACK = 2;
|
|
931
|
+
const color = /* @__PURE__ */ new Map();
|
|
932
|
+
const dfsParent = /* @__PURE__ */ new Map();
|
|
933
|
+
for (const node of nodes) color.set(node, WHITE);
|
|
934
|
+
const stack = [];
|
|
935
|
+
function isImmediateDfsParent(ancestor, node) {
|
|
936
|
+
return dfsParent.get(node) === ancestor;
|
|
937
|
+
}
|
|
938
|
+
function pushFrame(node, parent) {
|
|
939
|
+
color.set(node, GRAY);
|
|
940
|
+
dfsParent.set(node, parent);
|
|
941
|
+
stack.push({
|
|
942
|
+
node,
|
|
943
|
+
outgoing: outgoingByFrom.get(node) ?? [],
|
|
944
|
+
index: 0
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
function runDfsFrom(root) {
|
|
948
|
+
if (color.get(root) !== WHITE) return;
|
|
949
|
+
pushFrame(root, void 0);
|
|
950
|
+
while (stack.length > 0) {
|
|
951
|
+
const frame = stack[stack.length - 1];
|
|
952
|
+
if (frame === void 0) break;
|
|
953
|
+
if (frame.index >= frame.outgoing.length) {
|
|
954
|
+
color.set(frame.node, BLACK);
|
|
955
|
+
stack.pop();
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
const edge = frame.outgoing[frame.index];
|
|
959
|
+
frame.index += 1;
|
|
960
|
+
if (edge === void 0) continue;
|
|
961
|
+
const v = edge.to;
|
|
962
|
+
const vColor = color.get(v);
|
|
963
|
+
if (vColor === GRAY && isImmediateDfsParent(v, frame.node)) kindByMigrationHash.set(edge.hash, "rollback");
|
|
964
|
+
else {
|
|
965
|
+
kindByMigrationHash.set(edge.hash, "forward");
|
|
966
|
+
if (vColor === WHITE) pushFrame(v, frame.node);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
for (const root of dfsRoots) runDfsFrom(root);
|
|
971
|
+
const remainingWhite = [...nodes].filter((node) => color.get(node) === WHITE);
|
|
972
|
+
remainingWhite.sort((a, b) => a.localeCompare(b));
|
|
973
|
+
for (const root of remainingWhite) runDfsFrom(root);
|
|
974
|
+
peelNodeSkippingRollbacks(nodes, kindByMigrationHash, nonSelf);
|
|
975
|
+
const forwardInDegree = /* @__PURE__ */ new Map();
|
|
976
|
+
const forwardOutDegree = /* @__PURE__ */ new Map();
|
|
977
|
+
for (const edge of edges) {
|
|
978
|
+
if (kindByMigrationHash.get(edge.hash) !== "forward") continue;
|
|
979
|
+
bumpDegree(forwardOutDegree, edge.from);
|
|
980
|
+
bumpDegree(forwardInDegree, edge.to);
|
|
981
|
+
}
|
|
982
|
+
return {
|
|
983
|
+
kindByMigrationHash,
|
|
984
|
+
forwardInDegree,
|
|
985
|
+
forwardOutDegree
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Classify forward/rollback/self for a `MigrationGraph` edge set (Tier-3).
|
|
990
|
+
*/
|
|
991
|
+
function classifyMigrationGraphTopology(graph) {
|
|
992
|
+
const normalized = [];
|
|
993
|
+
for (const edges of graph.forwardChain.values()) for (const edge of edges) normalized.push({
|
|
994
|
+
hash: edge.migrationHash,
|
|
995
|
+
from: edge.from,
|
|
996
|
+
to: edge.to,
|
|
997
|
+
dirName: edge.dirName
|
|
998
|
+
});
|
|
999
|
+
return classifyNormalizedEdges(normalized);
|
|
1000
|
+
}
|
|
1001
|
+
//#endregion
|
|
1002
|
+
//#region src/utils/formatters/migration-graph-rows.ts
|
|
1003
|
+
/**
|
|
1004
|
+
* Return the weakly-connected components of `graph` as an array of node sets,
|
|
1005
|
+
* ordered so the component containing EMPTY_CONTRACT_HASH comes first (if
|
|
1006
|
+
* present), with remaining components sorted by their lex-smallest node hash.
|
|
1007
|
+
*/
|
|
1008
|
+
function weaklyConnectedComponents(graph) {
|
|
1009
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1010
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
1011
|
+
function addAdjacent(a, b) {
|
|
1012
|
+
const aList = adjacency.get(a);
|
|
1013
|
+
if (aList) aList.push(b);
|
|
1014
|
+
else adjacency.set(a, [b]);
|
|
1015
|
+
const bList = adjacency.get(b);
|
|
1016
|
+
if (bList) bList.push(a);
|
|
1017
|
+
else adjacency.set(b, [a]);
|
|
1018
|
+
}
|
|
1019
|
+
for (const edges of graph.forwardChain.values()) for (const edge of edges) if (edge.from !== edge.to) addAdjacent(edge.from, edge.to);
|
|
1020
|
+
for (const node of graph.nodes) if (!adjacency.has(node)) adjacency.set(node, []);
|
|
1021
|
+
const components = [];
|
|
1022
|
+
function bfsComponent(start) {
|
|
1023
|
+
const component = /* @__PURE__ */ new Set();
|
|
1024
|
+
const queue = [start];
|
|
1025
|
+
while (queue.length > 0) {
|
|
1026
|
+
const node = queue.shift();
|
|
1027
|
+
if (node === void 0 || visited.has(node)) continue;
|
|
1028
|
+
visited.add(node);
|
|
1029
|
+
component.add(node);
|
|
1030
|
+
for (const neighbor of adjacency.get(node) ?? []) if (!visited.has(neighbor)) queue.push(neighbor);
|
|
1031
|
+
}
|
|
1032
|
+
return component;
|
|
1033
|
+
}
|
|
1034
|
+
const allNodes = [...graph.nodes].sort((a, b) => {
|
|
1035
|
+
if (a === EMPTY_CONTRACT_HASH) return -1;
|
|
1036
|
+
if (b === EMPTY_CONTRACT_HASH) return 1;
|
|
1037
|
+
return a.localeCompare(b);
|
|
1038
|
+
});
|
|
1039
|
+
for (const node of allNodes) if (!visited.has(node)) components.push(bfsComponent(node));
|
|
1040
|
+
components.sort((a, b) => {
|
|
1041
|
+
const aHasEmpty = a.has(EMPTY_CONTRACT_HASH);
|
|
1042
|
+
const bHasEmpty = b.has(EMPTY_CONTRACT_HASH);
|
|
1043
|
+
if (aHasEmpty && !bHasEmpty) return -1;
|
|
1044
|
+
if (!aHasEmpty && bHasEmpty) return 1;
|
|
1045
|
+
const aMin = [...a].sort((x, y) => x.localeCompare(y))[0] ?? "";
|
|
1046
|
+
const bMin = [...b].sort((x, y) => x.localeCompare(y))[0] ?? "";
|
|
1047
|
+
return aMin.localeCompare(bMin);
|
|
1048
|
+
});
|
|
1049
|
+
return components;
|
|
1050
|
+
}
|
|
1051
|
+
function forwardRootsInComponent(componentNodes, topology) {
|
|
1052
|
+
const roots = [];
|
|
1053
|
+
for (const node of componentNodes) if ((topology.forwardInDegree.get(node) ?? 0) === 0) roots.push(node);
|
|
1054
|
+
roots.sort((a, b) => {
|
|
1055
|
+
if (a === EMPTY_CONTRACT_HASH) return -1;
|
|
1056
|
+
if (b === EMPTY_CONTRACT_HASH) return 1;
|
|
1057
|
+
return a.localeCompare(b);
|
|
1058
|
+
});
|
|
1059
|
+
if (roots.length > 0) return roots;
|
|
1060
|
+
return [...componentNodes].sort((a, b) => {
|
|
1061
|
+
if (a === EMPTY_CONTRACT_HASH) return -1;
|
|
1062
|
+
if (b === EMPTY_CONTRACT_HASH) return 1;
|
|
1063
|
+
return a.localeCompare(b);
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
function compareNodesTipsFirst(a, b, rank) {
|
|
1067
|
+
const rankA = rank.get(a) ?? 0;
|
|
1068
|
+
const rankB = rank.get(b) ?? 0;
|
|
1069
|
+
if (rankA !== rankB) return rankB - rankA;
|
|
1070
|
+
if (a === EMPTY_CONTRACT_HASH) return 1;
|
|
1071
|
+
if (b === EMPTY_CONTRACT_HASH) return -1;
|
|
1072
|
+
return a.localeCompare(b);
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Layer nodes by longest forward-path rank from forward roots within the
|
|
1076
|
+
* component. Rank 0 is the root (bottom row); the maximum rank is the tip
|
|
1077
|
+
* (top row). Emits rank-descending with lex-ascending tie-break among siblings
|
|
1078
|
+
* at the same rank — stable across edge-insertion order and correct under
|
|
1079
|
+
* diamonds, cross-links, and rollbacks.
|
|
1080
|
+
*/
|
|
1081
|
+
function maxRank(rank) {
|
|
1082
|
+
let max = 0;
|
|
1083
|
+
for (const value of rank.values()) if (value > max) max = value;
|
|
1084
|
+
return max;
|
|
1085
|
+
}
|
|
1086
|
+
function layerNodesByLongestForwardPath(componentNodes, topology, graph, contractHash) {
|
|
1087
|
+
const forwardOut = /* @__PURE__ */ new Map();
|
|
1088
|
+
for (const node of componentNodes) forwardOut.set(node, []);
|
|
1089
|
+
for (const edges of graph.forwardChain.values()) for (const edge of edges) {
|
|
1090
|
+
if (!componentNodes.has(edge.from) || !componentNodes.has(edge.to)) continue;
|
|
1091
|
+
if (edge.from === edge.to) continue;
|
|
1092
|
+
if (topology.kindByMigrationHash.get(edge.migrationHash) !== "forward") continue;
|
|
1093
|
+
const bucket = forwardOut.get(edge.from);
|
|
1094
|
+
if (bucket) bucket.push(edge.to);
|
|
1095
|
+
}
|
|
1096
|
+
const roots = forwardRootsInComponent(componentNodes, topology);
|
|
1097
|
+
const rank = /* @__PURE__ */ new Map();
|
|
1098
|
+
for (const root of roots) rank.set(root, 0);
|
|
1099
|
+
const maxPasses = componentNodes.size;
|
|
1100
|
+
for (let pass = 0; pass < maxPasses; pass++) {
|
|
1101
|
+
let changed = false;
|
|
1102
|
+
for (const node of componentNodes) {
|
|
1103
|
+
const base = rank.get(node);
|
|
1104
|
+
if (base === void 0) continue;
|
|
1105
|
+
for (const to of forwardOut.get(node) ?? []) {
|
|
1106
|
+
const next = base + 1;
|
|
1107
|
+
if (next > (rank.get(to) ?? -1)) {
|
|
1108
|
+
rank.set(to, next);
|
|
1109
|
+
changed = true;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
if (!changed) break;
|
|
1114
|
+
}
|
|
1115
|
+
for (const node of componentNodes) if (!rank.has(node)) rank.set(node, 0);
|
|
1116
|
+
if (contractHash !== void 0 && contractHash !== EMPTY_CONTRACT_HASH && componentNodes.has(contractHash) && (forwardOut.get(contractHash) ?? []).length === 0) rank.set(contractHash, maxRank(rank) + 1);
|
|
1117
|
+
return [...componentNodes].sort((a, b) => compareNodesTipsFirst(a, b, rank));
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Build the row model from a tolerant `MigrationGraph`.
|
|
1121
|
+
*
|
|
1122
|
+
* The row model is the first pure-data stage of the `migration graph` render
|
|
1123
|
+
* pipeline. It:
|
|
1124
|
+
* - classifies every edge as `forward`, `rollback`, or `self`;
|
|
1125
|
+
* - produces a deterministic vertical node ordering (tips at index 0, roots
|
|
1126
|
+
* at the end) within each weakly-connected component;
|
|
1127
|
+
* - separates disjoint components with `null` sentinels;
|
|
1128
|
+
* - optionally prepends a detached current contract as its own single-node
|
|
1129
|
+
* component when `contractHash` is not already in the graph.
|
|
1130
|
+
*
|
|
1131
|
+
* No columns, no lane allocation, no glyphs, no rendering.
|
|
1132
|
+
*/
|
|
1133
|
+
/**
|
|
1134
|
+
* Resolve the detached current contract, if any: a real contract (not the
|
|
1135
|
+
* empty baseline) that no migration on disk produces, so it is absent from
|
|
1136
|
+
* the graph. Such a contract renders as a floating node rather than
|
|
1137
|
+
* decorating an existing one. Returns the hash when detached, else undefined.
|
|
1138
|
+
*/
|
|
1139
|
+
function detachedContractHash(graph, contractHash) {
|
|
1140
|
+
return contractHash !== void 0 && contractHash !== EMPTY_CONTRACT_HASH && !graph.nodes.has(contractHash) ? contractHash : void 0;
|
|
1141
|
+
}
|
|
1142
|
+
function isForwardLeaf(node, edges) {
|
|
1143
|
+
return !edges.some((e) => e.kind === "forward" && e.from === node && e.from !== e.to);
|
|
1144
|
+
}
|
|
1145
|
+
function forwardReachableFrom(start, forwardTo) {
|
|
1146
|
+
const reachable = new Set([start]);
|
|
1147
|
+
const queue = [start];
|
|
1148
|
+
while (queue.length > 0) {
|
|
1149
|
+
const node = queue.shift();
|
|
1150
|
+
if (node === void 0) continue;
|
|
1151
|
+
for (const next of forwardTo.get(node) ?? []) if (!reachable.has(next)) {
|
|
1152
|
+
reachable.add(next);
|
|
1153
|
+
queue.push(next);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
return reachable;
|
|
1157
|
+
}
|
|
1158
|
+
function buildForwardToMap(edges) {
|
|
1159
|
+
const forwardTo = /* @__PURE__ */ new Map();
|
|
1160
|
+
for (const edge of edges) {
|
|
1161
|
+
if (edge.kind !== "forward" || edge.from === edge.to) continue;
|
|
1162
|
+
const bucket = forwardTo.get(edge.from);
|
|
1163
|
+
if (bucket) bucket.push(edge.to);
|
|
1164
|
+
else forwardTo.set(edge.from, [edge.to]);
|
|
1165
|
+
}
|
|
1166
|
+
return forwardTo;
|
|
1167
|
+
}
|
|
1168
|
+
function sortEdgesForContractHashTrunk(edges, contractHash) {
|
|
1169
|
+
if (contractHash === void 0 || contractHash === EMPTY_CONTRACT_HASH || !isForwardLeaf(contractHash, edges)) return edges;
|
|
1170
|
+
const preferredLeaf = contractHash;
|
|
1171
|
+
const forwardTo = buildForwardToMap(edges);
|
|
1172
|
+
const reachability = /* @__PURE__ */ new Map();
|
|
1173
|
+
function canReachContractHash(from) {
|
|
1174
|
+
let cached = reachability.get(from);
|
|
1175
|
+
if (cached === void 0) {
|
|
1176
|
+
cached = forwardReachableFrom(from, forwardTo);
|
|
1177
|
+
reachability.set(from, cached);
|
|
1178
|
+
}
|
|
1179
|
+
return cached.has(preferredLeaf);
|
|
1180
|
+
}
|
|
1181
|
+
function trunkBias(edge) {
|
|
1182
|
+
if (edge.kind !== "forward" || edge.from === edge.to) return 0;
|
|
1183
|
+
if (edge.to === preferredLeaf) return 2;
|
|
1184
|
+
if (canReachContractHash(edge.to)) return 1;
|
|
1185
|
+
return 0;
|
|
1186
|
+
}
|
|
1187
|
+
return edges.map((edge, index) => ({
|
|
1188
|
+
edge,
|
|
1189
|
+
index,
|
|
1190
|
+
bias: trunkBias(edge)
|
|
1191
|
+
})).sort((a, b) => {
|
|
1192
|
+
if (a.edge.from !== b.edge.from) return a.index - b.index;
|
|
1193
|
+
if (a.bias !== b.bias) return b.bias - a.bias;
|
|
1194
|
+
return a.index - b.index;
|
|
1195
|
+
}).map(({ edge }) => edge);
|
|
1196
|
+
}
|
|
1197
|
+
function rebuildEdgeLookupMaps(edges) {
|
|
1198
|
+
const edgesByFrom = /* @__PURE__ */ new Map();
|
|
1199
|
+
const edgesByTo = /* @__PURE__ */ new Map();
|
|
1200
|
+
for (const classified of edges) {
|
|
1201
|
+
const fromBucket = edgesByFrom.get(classified.from);
|
|
1202
|
+
if (fromBucket) fromBucket.push(classified);
|
|
1203
|
+
else edgesByFrom.set(classified.from, [classified]);
|
|
1204
|
+
const toBucket = edgesByTo.get(classified.to);
|
|
1205
|
+
if (toBucket) toBucket.push(classified);
|
|
1206
|
+
else edgesByTo.set(classified.to, [classified]);
|
|
1207
|
+
}
|
|
1208
|
+
return {
|
|
1209
|
+
edgesByFrom,
|
|
1210
|
+
edgesByTo
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
function buildMigrationGraphRows(graph, options = {}) {
|
|
1214
|
+
const emptyModel = {
|
|
1215
|
+
nodes: [],
|
|
1216
|
+
edges: [],
|
|
1217
|
+
edgesByFrom: /* @__PURE__ */ new Map(),
|
|
1218
|
+
edgesByTo: /* @__PURE__ */ new Map()
|
|
1219
|
+
};
|
|
1220
|
+
if (graph.nodes.size === 0) {
|
|
1221
|
+
const detached = detachedContractHash(graph, options.contractHash);
|
|
1222
|
+
return detached !== void 0 ? {
|
|
1223
|
+
...emptyModel,
|
|
1224
|
+
nodes: [detached]
|
|
1225
|
+
} : emptyModel;
|
|
1226
|
+
}
|
|
1227
|
+
const topology = classifyMigrationGraphTopology(graph);
|
|
1228
|
+
const edges = [];
|
|
1229
|
+
for (const edgeList of graph.forwardChain.values()) for (const edge of edgeList) {
|
|
1230
|
+
const kind = topology.kindByMigrationHash.get(edge.migrationHash) ?? "forward";
|
|
1231
|
+
edges.push({
|
|
1232
|
+
migrationHash: edge.migrationHash,
|
|
1233
|
+
from: edge.from,
|
|
1234
|
+
to: edge.to,
|
|
1235
|
+
dirName: edge.dirName,
|
|
1236
|
+
kind
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
const sortedEdges = sortEdgesForContractHashTrunk(edges, options.contractHash);
|
|
1240
|
+
const { edgesByFrom, edgesByTo } = rebuildEdgeLookupMaps(sortedEdges);
|
|
1241
|
+
const components = weaklyConnectedComponents(graph);
|
|
1242
|
+
const nodes = [];
|
|
1243
|
+
for (let i = 0; i < components.length; i++) {
|
|
1244
|
+
if (i > 0) nodes.push(null);
|
|
1245
|
+
const component = components[i];
|
|
1246
|
+
if (component === void 0) continue;
|
|
1247
|
+
const ordered = layerNodesByLongestForwardPath(component, topology, graph, options.contractHash);
|
|
1248
|
+
for (const node of ordered) nodes.push(node);
|
|
1249
|
+
}
|
|
1250
|
+
const detached = detachedContractHash(graph, options.contractHash);
|
|
1251
|
+
if (detached !== void 0) {
|
|
1252
|
+
if (nodes.length > 0) nodes.unshift(null);
|
|
1253
|
+
nodes.unshift(detached);
|
|
1254
|
+
}
|
|
1255
|
+
return {
|
|
1256
|
+
nodes,
|
|
1257
|
+
edges: sortedEdges,
|
|
1258
|
+
edgesByFrom,
|
|
1259
|
+
edgesByTo
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
//#endregion
|
|
1263
|
+
//#region src/utils/formatters/migration-graph-space-render.ts
|
|
1264
|
+
function mergeMigrationEdgeAnnotations(listOverlay, statusOverlay) {
|
|
1265
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1266
|
+
for (const [migrationHash, listAnnotation] of listOverlay) {
|
|
1267
|
+
const statusAnnotation = statusOverlay.get(migrationHash);
|
|
1268
|
+
merged.set(migrationHash, {
|
|
1269
|
+
...listAnnotation,
|
|
1270
|
+
...statusAnnotation?.status !== void 0 ? { status: statusAnnotation.status } : {}
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
return merged;
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Translate `migrate --show` per-edge path-highlight annotations into a
|
|
1277
|
+
* {@link Highlight}. With any `pathHighlight` present the result is focus mode
|
|
1278
|
+
* (on-path lifted green, off-path dim); otherwise flat (lane-rotation colour).
|
|
1279
|
+
*/
|
|
1280
|
+
function highlightFromEdgeAnnotations(edgeAnnotationsByHash) {
|
|
1281
|
+
const onPath = /* @__PURE__ */ new Set();
|
|
1282
|
+
let anyPathHighlight = false;
|
|
1283
|
+
for (const [migrationHash, annotation] of edgeAnnotationsByHash) {
|
|
1284
|
+
if (annotation.pathHighlight === void 0) continue;
|
|
1285
|
+
anyPathHighlight = true;
|
|
1286
|
+
if (annotation.pathHighlight === "on-path") onPath.add(migrationHash);
|
|
1287
|
+
}
|
|
1288
|
+
return anyPathHighlight ? {
|
|
1289
|
+
mode: "focus",
|
|
1290
|
+
onPath
|
|
1291
|
+
} : {
|
|
1292
|
+
mode: "flat",
|
|
1293
|
+
onPath: /* @__PURE__ */ new Set()
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
function buildGridForInput(input) {
|
|
1297
|
+
const rowModel = buildMigrationGraphRows(input.graph, { contractHash: input.liveContractHash });
|
|
1298
|
+
return {
|
|
1299
|
+
grid: buildGrid(rowModel, {}, {
|
|
1300
|
+
mode: "flat",
|
|
1301
|
+
onPath: /* @__PURE__ */ new Set()
|
|
1302
|
+
}),
|
|
1303
|
+
rowModel
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* The widest gutter→label column across the given space layouts. Cross-space
|
|
1308
|
+
* callers pass this back in so every section's labels share one column.
|
|
1309
|
+
*/
|
|
1310
|
+
function computeGlobalMaxEdgeTreePrefixWidth(inputs, glyphMode = "unicode") {
|
|
1311
|
+
let globalMax = 0;
|
|
1312
|
+
for (const input of inputs) {
|
|
1313
|
+
const { grid } = buildGridForInput(input);
|
|
1314
|
+
globalMax = Math.max(globalMax, computeLabelColumn(grid, glyphMode));
|
|
1315
|
+
}
|
|
1316
|
+
return globalMax;
|
|
1317
|
+
}
|
|
1318
|
+
function computeGlobalMaxDirNameWidth(inputs) {
|
|
1319
|
+
let globalMax = 0;
|
|
1320
|
+
for (const input of inputs) {
|
|
1321
|
+
const { rowModel } = buildGridForInput(input);
|
|
1322
|
+
globalMax = Math.max(globalMax, computeMaxDirNameWidth(rowModel));
|
|
1323
|
+
}
|
|
1324
|
+
return globalMax;
|
|
1325
|
+
}
|
|
1326
|
+
function renderMigrationGraphSpaceTreeInternal(input) {
|
|
1327
|
+
const appSpace = input.isAppSpace !== false;
|
|
1328
|
+
const rowModel = buildMigrationGraphRows(input.graph, { ...appSpace ? { contractHash: input.liveContractHash } : {} });
|
|
1329
|
+
const listOverlay = buildEdgeAnnotationsByHashFromListEntries(input.migrations);
|
|
1330
|
+
const edgeAnnotationsByHash = input.statusOverlayByHash === void 0 ? listOverlay : mergeMigrationEdgeAnnotations(listOverlay, input.statusOverlayByHash);
|
|
1331
|
+
return renderMigrationGraphCommand({
|
|
1332
|
+
grid: buildGrid(rowModel, {}, highlightFromEdgeAnnotations(edgeAnnotationsByHash)),
|
|
1333
|
+
rowModel,
|
|
1334
|
+
colorize: input.colorize,
|
|
1335
|
+
glyphMode: input.glyphMode,
|
|
1336
|
+
contractHash: input.liveContractHash,
|
|
1337
|
+
isAppSpace: appSpace,
|
|
1338
|
+
edgeAnnotationsByHash,
|
|
1339
|
+
refsByHash: input.refsByHash ?? buildRefsByHashFromListEntries(input.migrations),
|
|
1340
|
+
...input.dbHash !== void 0 ? { dbHash: input.dbHash } : {},
|
|
1341
|
+
...input.styler !== void 0 ? { styler: input.styler } : {},
|
|
1342
|
+
...input.globalMaxEdgeTreePrefixWidth !== void 0 ? { globalLabelColumn: input.globalMaxEdgeTreePrefixWidth } : {},
|
|
1343
|
+
...input.globalMaxDirNameWidth !== void 0 ? { globalMaxDirNameWidth: input.globalMaxDirNameWidth } : {}
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
function renderMigrationGraphSpaceTree(input) {
|
|
1347
|
+
return renderMigrationGraphSpaceTreeInternal(input);
|
|
1348
|
+
}
|
|
1349
|
+
function indentMigrationGraphTreeBlock(treeOutput, indent) {
|
|
1350
|
+
if (treeOutput.length === 0) return treeOutput;
|
|
1351
|
+
return treeOutput.split("\n").map((line) => line.length === 0 ? line : `${indent}${line}`).join("\n");
|
|
1352
|
+
}
|
|
1353
|
+
//#endregion
|
|
1354
|
+
//#region src/utils/formatters/migration-list-render.ts
|
|
1355
|
+
const IDENTITY_MIGRATION_LIST_STYLER = {
|
|
1356
|
+
kind: (text) => text,
|
|
1357
|
+
dirName: (text) => text,
|
|
1358
|
+
sourceHash: (text) => text,
|
|
1359
|
+
destHash: (text) => text,
|
|
1360
|
+
glyph: (text) => text,
|
|
1361
|
+
lane: (text) => text,
|
|
1362
|
+
invariants: (ids) => `{${ids.join(", ")}}`,
|
|
1363
|
+
refs: (names) => `(${names.join(", ")})`,
|
|
1364
|
+
spaceHeading: (text) => text,
|
|
1365
|
+
summary: (text) => text,
|
|
1366
|
+
emptyState: (text) => text
|
|
1367
|
+
};
|
|
1368
|
+
function canonicalFrom(from) {
|
|
1369
|
+
return from ?? EMPTY_CONTRACT_HASH;
|
|
1370
|
+
}
|
|
1371
|
+
function migrationGraphFromListEntries(entries) {
|
|
1372
|
+
const nodes = /* @__PURE__ */ new Set();
|
|
1373
|
+
const forwardChain = /* @__PURE__ */ new Map();
|
|
1374
|
+
const reverseChain = /* @__PURE__ */ new Map();
|
|
1375
|
+
const migrationByHash = /* @__PURE__ */ new Map();
|
|
1376
|
+
for (const entry of entries) {
|
|
1377
|
+
const from = canonicalFrom(entry.fromContract);
|
|
1378
|
+
const edge = {
|
|
1379
|
+
from,
|
|
1380
|
+
to: entry.toContract,
|
|
1381
|
+
migrationHash: entry.hash,
|
|
1382
|
+
dirName: entry.name,
|
|
1383
|
+
createdAt: entry.createdAt,
|
|
1384
|
+
invariants: entry.providedInvariants
|
|
1385
|
+
};
|
|
1386
|
+
nodes.add(from);
|
|
1387
|
+
nodes.add(entry.toContract);
|
|
1388
|
+
const forward = forwardChain.get(from);
|
|
1389
|
+
if (forward) forward.push(edge);
|
|
1390
|
+
else forwardChain.set(from, [edge]);
|
|
1391
|
+
const reverse = reverseChain.get(entry.toContract);
|
|
1392
|
+
if (reverse) reverse.push(edge);
|
|
1393
|
+
else reverseChain.set(entry.toContract, [edge]);
|
|
1394
|
+
migrationByHash.set(entry.hash, edge);
|
|
1395
|
+
}
|
|
1396
|
+
return {
|
|
1397
|
+
nodes,
|
|
1398
|
+
forwardChain,
|
|
1399
|
+
reverseChain,
|
|
1400
|
+
migrationByHash
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
function buildEdgeAnnotationsByHashFromListEntries(entries) {
|
|
1404
|
+
const annotations = /* @__PURE__ */ new Map();
|
|
1405
|
+
for (const entry of entries) annotations.set(entry.hash, {
|
|
1406
|
+
operationCount: entry.operationCount,
|
|
1407
|
+
invariants: entry.providedInvariants
|
|
1408
|
+
});
|
|
1409
|
+
return annotations;
|
|
1410
|
+
}
|
|
1411
|
+
function buildRefsByHashFromListEntries(entries) {
|
|
1412
|
+
const refsByHash = /* @__PURE__ */ new Map();
|
|
1413
|
+
for (const entry of entries) if (entry.refs.length > 0) refsByHash.set(entry.toContract, entry.refs);
|
|
1414
|
+
return refsByHash;
|
|
1415
|
+
}
|
|
1416
|
+
function formatEmptyStateLine(spaceId, style) {
|
|
1417
|
+
return style.emptyState(`There are no migrations in migrations/${spaceId}/ yet`);
|
|
1418
|
+
}
|
|
1419
|
+
function renderSpaceTreeBlock(spaceId, migrations, multiSpace, glyphMode, style, colorize, liveContractHash, graphForSpace, appSpaceId, globalMaxEdgeTreePrefixWidth, globalMaxDirNameWidth) {
|
|
1420
|
+
if (migrations.length === 0) {
|
|
1421
|
+
const emptyLine = formatEmptyStateLine(spaceId, style);
|
|
1422
|
+
if (!multiSpace) return [emptyLine];
|
|
1423
|
+
return [style.spaceHeading(`${spaceId}:`), ` ${emptyLine}`];
|
|
1424
|
+
}
|
|
1425
|
+
const graph = graphForSpace(spaceId) ?? migrationGraphFromListEntries(migrations);
|
|
1426
|
+
const isAppSpace = appSpaceId === void 0 ? void 0 : spaceId === appSpaceId;
|
|
1427
|
+
const treeOutput = renderMigrationGraphSpaceTree({
|
|
1428
|
+
graph,
|
|
1429
|
+
migrations,
|
|
1430
|
+
liveContractHash,
|
|
1431
|
+
glyphMode,
|
|
1432
|
+
colorize,
|
|
1433
|
+
refsByHash: buildRefsByHashFromListEntries(migrations),
|
|
1434
|
+
styler: style,
|
|
1435
|
+
...isAppSpace !== void 0 ? { isAppSpace } : {},
|
|
1436
|
+
...globalMaxEdgeTreePrefixWidth !== void 0 ? { globalMaxEdgeTreePrefixWidth } : {},
|
|
1437
|
+
...globalMaxDirNameWidth !== void 0 ? { globalMaxDirNameWidth } : {}
|
|
1438
|
+
});
|
|
1439
|
+
if (!multiSpace) return treeOutput.length === 0 ? [] : [treeOutput];
|
|
1440
|
+
const indented = indentMigrationGraphTreeBlock(treeOutput, " ");
|
|
1441
|
+
return [style.spaceHeading(`${spaceId}:`), indented];
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Compose the styled `migration list` human output via the shared tree
|
|
1445
|
+
* renderer. Each on-disk migration is one edge row with package-fact
|
|
1446
|
+
* annotations; refs decorate destination contract nodes.
|
|
1447
|
+
*
|
|
1448
|
+
* `options.colorize` must match whether `style` emits ANSI (e.g. both true for
|
|
1449
|
+
* `createAnsiMigrationListStyler({ useColor: true })`).
|
|
1450
|
+
*/
|
|
1451
|
+
function renderMigrationListWithStyle(result, style, glyphMode = "unicode", options = {}) {
|
|
1452
|
+
const multiSpace = result.spaces.length > 1;
|
|
1453
|
+
const colorize = options.colorize ?? false;
|
|
1454
|
+
const liveContractHash = options.liveContractHash ?? EMPTY_CONTRACT_HASH;
|
|
1455
|
+
const graphForSpace = options.graphForSpace ?? (() => void 0);
|
|
1456
|
+
const appSpaceId = options.appSpaceId;
|
|
1457
|
+
const globalLayoutInputs = multiSpace ? result.spaces.filter((space) => space.migrations.length > 0).map((space) => ({
|
|
1458
|
+
graph: graphForSpace(space.space) ?? migrationGraphFromListEntries(space.migrations),
|
|
1459
|
+
liveContractHash
|
|
1460
|
+
})) : [];
|
|
1461
|
+
const globalMaxEdgeTreePrefixWidth = globalLayoutInputs.length > 0 ? computeGlobalMaxEdgeTreePrefixWidth(globalLayoutInputs) : void 0;
|
|
1462
|
+
const globalMaxDirNameWidth = globalLayoutInputs.length > 0 ? computeGlobalMaxDirNameWidth(globalLayoutInputs) : void 0;
|
|
1463
|
+
const lines = [];
|
|
1464
|
+
for (let index = 0; index < result.spaces.length; index++) {
|
|
1465
|
+
const space = result.spaces[index];
|
|
1466
|
+
if (index > 0) lines.push("");
|
|
1467
|
+
lines.push(...renderSpaceTreeBlock(space.space, space.migrations, multiSpace, glyphMode, style, colorize, liveContractHash, graphForSpace, appSpaceId, globalMaxEdgeTreePrefixWidth, globalMaxDirNameWidth));
|
|
1468
|
+
}
|
|
1469
|
+
if (result.spaces.reduce((count, space) => count + space.migrations.length, 0) > 0) {
|
|
1470
|
+
lines.push("");
|
|
1471
|
+
lines.push(style.summary(result.summary));
|
|
1472
|
+
}
|
|
1473
|
+
return lines.join("\n");
|
|
1474
|
+
}
|
|
1475
|
+
//#endregion
|
|
1476
|
+
//#region src/utils/formatters/migration-list-styler.ts
|
|
1477
|
+
function hasMarkersFormatter(styler) {
|
|
1478
|
+
return "markers" in styler && typeof styler.markers === "function";
|
|
1479
|
+
}
|
|
1480
|
+
function styleMarkerName(name) {
|
|
1481
|
+
return name === "contract" ? bold(green(name)) : green(name);
|
|
1482
|
+
}
|
|
1483
|
+
function plainMarkers(names) {
|
|
1484
|
+
return names.map((name) => `@${name}`).join(" ");
|
|
1485
|
+
}
|
|
1486
|
+
function formatContractNodeOverlays(styler, markers, refs) {
|
|
1487
|
+
const parts = [];
|
|
1488
|
+
if (markers.length > 0) parts.push(hasMarkersFormatter(styler) ? styler.markers(markers) : plainMarkers(markers));
|
|
1489
|
+
if (refs.length > 0) parts.push(styler.refs(refs));
|
|
1490
|
+
return parts.join(" ");
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* The current contract overlay marker. Unlike user refs, this names the user's
|
|
1494
|
+
* declared desired state — the implicit base/target for `plan` / `migrate` —
|
|
1495
|
+
* not a stored label. It is emphasized (bold) so it stands out from plain refs
|
|
1496
|
+
* (including the live-database `db` marker, which is just another ref).
|
|
1497
|
+
*/
|
|
1498
|
+
const CONTRACT_MARKER_NAME = "contract";
|
|
1499
|
+
function styleRefName(name) {
|
|
1500
|
+
return green(name);
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* Build a {@link MigrationListStyler} that decorates `migration list`
|
|
1504
|
+
* tokens with ANSI SGR codes. When `useColor` is `false` (non-TTY,
|
|
1505
|
+
* `--no-color`, `NO_COLOR=1`, piped output) the function returns the
|
|
1506
|
+
* shared identity styler so callers get plain text with zero ANSI
|
|
1507
|
+
* bytes — pipe-friendly by construction.
|
|
1508
|
+
*
|
|
1509
|
+
* Palette:
|
|
1510
|
+
*
|
|
1511
|
+
* - `dirName`: bold
|
|
1512
|
+
* - `sourceHash`: dim cyan
|
|
1513
|
+
* - `destHash`: bright cyan
|
|
1514
|
+
* - `kind` (`*` / `↩` / `⟲`): bright — the signal; lanes and arrows dim
|
|
1515
|
+
* - `glyph` (`→` / `⟲` / `∅`): dim
|
|
1516
|
+
* - `lane` (graph gutter lines `│` and fan/join connectors `├─┐` / `├─┘`): dim
|
|
1517
|
+
* - `invariants` (`{...}`): yellow
|
|
1518
|
+
* - `markers` (`@contract @db`): green; the `contract` desired-state marker is
|
|
1519
|
+
* green-bold (`db` is plain green); the `@` sigil is applied to each name
|
|
1520
|
+
* - `refs` (`(...)`): green (the active ref is bolded separately by the tree styler)
|
|
1521
|
+
* - `spaceHeading` (`<spaceId>:`): bold
|
|
1522
|
+
* - `summary`: dim
|
|
1523
|
+
* - `emptyState`: dim
|
|
1524
|
+
*/
|
|
1525
|
+
function createAnsiMigrationListStyler(opts) {
|
|
1526
|
+
if (!opts.useColor) return {
|
|
1527
|
+
...IDENTITY_MIGRATION_LIST_STYLER,
|
|
1528
|
+
markers: plainMarkers
|
|
1529
|
+
};
|
|
1530
|
+
return {
|
|
1531
|
+
kind: (text) => text,
|
|
1532
|
+
dirName: (text) => bold(text),
|
|
1533
|
+
sourceHash: (text) => dim(cyan(text)),
|
|
1534
|
+
destHash: (text) => cyanBright(text),
|
|
1535
|
+
glyph: (text) => dim(text),
|
|
1536
|
+
lane: (text) => dim(text),
|
|
1537
|
+
invariants: (ids) => yellow(`{${ids.join(", ")}}`),
|
|
1538
|
+
markers: (names) => {
|
|
1539
|
+
const sigil = green("@");
|
|
1540
|
+
return names.map((name) => sigil + styleMarkerName(name)).join(" ");
|
|
1541
|
+
},
|
|
1542
|
+
refs: (names) => {
|
|
1543
|
+
const open = green("(");
|
|
1544
|
+
const close = green(")");
|
|
1545
|
+
const separator = green(", ");
|
|
1546
|
+
return open + names.map(styleRefName).join(separator) + close;
|
|
1547
|
+
},
|
|
1548
|
+
spaceHeading: (text) => bold(text),
|
|
1549
|
+
summary: (text) => dim(text),
|
|
1550
|
+
emptyState: (text) => dim(text)
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
//#endregion
|
|
1554
|
+
//#region src/utils/formatters/migration-graph-labels.ts
|
|
1555
|
+
/**
|
|
1556
|
+
* Per-row label formatting for the command graph renderer.
|
|
1557
|
+
*
|
|
1558
|
+
* The command graph renderer ({@link renderMigrationGraphCommand}) derives the
|
|
1559
|
+
* graph structure — rows, gutter, lane colours — from the grid pipeline. The
|
|
1560
|
+
* per-row LABEL (contract hash + markers + refs for node rows;
|
|
1561
|
+
* migration name + `from → to` + ops/status/will-run for migration rows) is
|
|
1562
|
+
* formatted here. This module owns ONLY label text + styling; it knows nothing
|
|
1563
|
+
* about lanes, gutters, or grid geometry.
|
|
1564
|
+
*
|
|
1565
|
+
* The label format (hash abbreviation, `from → to` arrow column, `@contract`/
|
|
1566
|
+
* `@db` markers, `(refs)`, ops/status/will-run suffix, the legend) is the same
|
|
1567
|
+
* as the previous renderer — that part was never the bug.
|
|
1568
|
+
*/
|
|
1569
|
+
/**
|
|
1570
|
+
* The live-database overlay marker. Just another ref as far as styling goes —
|
|
1571
|
+
* the only emphasized markers are the active ref and the `contract`
|
|
1572
|
+
* desired-state marker (see {@link CONTRACT_MARKER_NAME}).
|
|
1573
|
+
*/
|
|
1574
|
+
const DB_MARKER_NAME = "db";
|
|
1575
|
+
/**
|
|
1576
|
+
* Forced-color functions that always emit ANSI regardless of the ambient TTY
|
|
1577
|
+
* environment (NO_COLOR, piped output). Used so on-path green / off-path dim are
|
|
1578
|
+
* deterministically emitted in tests that request colour while NO_COLOR is set.
|
|
1579
|
+
*/
|
|
1580
|
+
const { dim: forcedDim } = createColors({ useColor: true });
|
|
1581
|
+
const { greenBright: forcedGreen } = createColors({ useColor: true });
|
|
1582
|
+
/**
|
|
1583
|
+
* The two label styles used in `migrate --show` path-highlight mode.
|
|
1584
|
+
*
|
|
1585
|
+
* - `onPath`: bold name, neutral hashes (the on-path lane glyphs are coloured
|
|
1586
|
+
* green by the grid renderer, not here).
|
|
1587
|
+
* - `offPath`: uniform dim grey on the name and the whole hash column.
|
|
1588
|
+
*
|
|
1589
|
+
* To change the on-path / off-path label colour in future, edit this object.
|
|
1590
|
+
*/
|
|
1591
|
+
const PATH_HIGHLIGHT_STYLES = {
|
|
1592
|
+
onPath: (_style, colorize) => ({
|
|
1593
|
+
lane: colorize ? forcedGreen : (text) => text,
|
|
1594
|
+
arrow: (text) => text,
|
|
1595
|
+
dirName: (text) => bold(text),
|
|
1596
|
+
hashOverride: void 0
|
|
1597
|
+
}),
|
|
1598
|
+
offPath: (colorize) => ({
|
|
1599
|
+
lane: colorize ? forcedDim : (text) => text,
|
|
1600
|
+
arrow: colorize ? forcedDim : (text) => text,
|
|
1601
|
+
dirName: colorize ? forcedDim : (text) => text,
|
|
1602
|
+
hashOverride: colorize ? forcedDim : void 0
|
|
1603
|
+
})
|
|
1604
|
+
};
|
|
1605
|
+
function abbreviateHash(hash, hashLength, emptySource) {
|
|
1606
|
+
if (hash === EMPTY_CONTRACT_HASH) return emptySource;
|
|
1607
|
+
return (hash.startsWith("sha256:") ? hash.slice(7) : hash).slice(0, hashLength);
|
|
1608
|
+
}
|
|
1609
|
+
function overlayNamesForContract(contractHash, opts) {
|
|
1610
|
+
const markers = [];
|
|
1611
|
+
const refs = [];
|
|
1612
|
+
const userRefs = opts.refsByHash?.get(contractHash);
|
|
1613
|
+
if (userRefs) refs.push(...[...userRefs].sort((a, b) => a.localeCompare(b)));
|
|
1614
|
+
if (opts.isAppSpace !== false && opts.contractHash === contractHash && contractHash !== EMPTY_CONTRACT_HASH) markers.push(CONTRACT_MARKER_NAME);
|
|
1615
|
+
if (opts.dbHash === contractHash) markers.push(DB_MARKER_NAME);
|
|
1616
|
+
markers.sort((a, b) => {
|
|
1617
|
+
if (a === "contract") return -1;
|
|
1618
|
+
if (b === "contract") return 1;
|
|
1619
|
+
return a.localeCompare(b);
|
|
1620
|
+
});
|
|
1621
|
+
return {
|
|
1622
|
+
markers,
|
|
1623
|
+
refs
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
function createLabelStyler(opts) {
|
|
1627
|
+
const base = opts.styler ?? createAnsiMigrationListStyler({ useColor: opts.colorize });
|
|
1628
|
+
const activeRefName = opts.activeRefName;
|
|
1629
|
+
if (!opts.colorize || activeRefName === void 0) return base;
|
|
1630
|
+
return {
|
|
1631
|
+
...base,
|
|
1632
|
+
refs: (names) => {
|
|
1633
|
+
const styledNames = names.map((name) => name === activeRefName ? bold(name) : name);
|
|
1634
|
+
return base.refs(styledNames);
|
|
1635
|
+
}
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
function overlayStatusGlyphs(mode) {
|
|
1639
|
+
return mode === "ascii" ? {
|
|
1640
|
+
applied: "+",
|
|
1641
|
+
pending: ">"
|
|
1642
|
+
} : {
|
|
1643
|
+
applied: "✓",
|
|
1644
|
+
pending: "⧗"
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
function formatEdgeAnnotationSuffix(migrationHash, opts, style) {
|
|
1648
|
+
const annotation = opts.edgeAnnotationsByHash?.get(migrationHash);
|
|
1649
|
+
if (annotation === void 0) return "";
|
|
1650
|
+
const isOffPath = annotation.pathHighlight === "off-path";
|
|
1651
|
+
const segments = [];
|
|
1652
|
+
if (annotation.operationCount !== void 0) segments.push(`${annotation.operationCount} ops`);
|
|
1653
|
+
if (annotation.invariants !== void 0 && annotation.invariants.length > 0) segments.push(style.invariants(annotation.invariants));
|
|
1654
|
+
const status = annotation.status;
|
|
1655
|
+
if (status !== void 0) {
|
|
1656
|
+
const glyphs = overlayStatusGlyphs(opts.glyphMode ?? "unicode");
|
|
1657
|
+
const glyph = status === "applied" ? glyphs.applied : glyphs.pending;
|
|
1658
|
+
const label = status === "applied" ? "applied" : "pending";
|
|
1659
|
+
if (!opts.colorize) segments.push(`${glyph} ${label}`);
|
|
1660
|
+
else {
|
|
1661
|
+
const styler = status === "applied" ? green : yellow;
|
|
1662
|
+
segments.push(styler(`${glyph} ${label}`));
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
if (annotation.pathHighlight === "on-path") {
|
|
1666
|
+
const glyph = opts.glyphMode === "ascii" ? ">" : "↑";
|
|
1667
|
+
segments.push(`${glyph} will run`);
|
|
1668
|
+
}
|
|
1669
|
+
if (segments.length === 0) return "";
|
|
1670
|
+
const suffix = ` ${segments.join(" ")}`;
|
|
1671
|
+
return opts.colorize && isOffPath ? forcedDim(suffix) : suffix;
|
|
1672
|
+
}
|
|
1673
|
+
/**
|
|
1674
|
+
* Format the `from → to` hash data column for an edge row.
|
|
1675
|
+
*
|
|
1676
|
+
* When `hashOverride` is provided (off-path → `dim`), it replaces ALL sub-stylers
|
|
1677
|
+
* so dim reaches every character without inner ANSI codes overriding it.
|
|
1678
|
+
*/
|
|
1679
|
+
function formatEdgeHashColumn(edge, style, hashLength, glyphMode, hashOverride) {
|
|
1680
|
+
const emptySource = migrationListEmptySource(glyphMode);
|
|
1681
|
+
const forwardArrow = migrationListForwardArrow(glyphMode);
|
|
1682
|
+
const src = hashOverride ?? style.sourceHash;
|
|
1683
|
+
const dst = hashOverride ?? style.destHash;
|
|
1684
|
+
const glyph = hashOverride ?? style.glyph;
|
|
1685
|
+
if (edge.kind === "self") {
|
|
1686
|
+
const hash = abbreviateHash(edge.from, hashLength, emptySource);
|
|
1687
|
+
return `${padFromHashColumn(src(hash), hashLength)} ${glyph(forwardArrow)} ${dst(hash)}`;
|
|
1688
|
+
}
|
|
1689
|
+
return `${edge.from === EMPTY_CONTRACT_HASH ? padFromHashColumn(glyph(emptySource), hashLength) : padFromHashColumn(src(abbreviateHash(edge.from, hashLength, emptySource)), hashLength)} ${glyph(forwardArrow)} ${dst(abbreviateHash(edge.to, hashLength, emptySource))}`;
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* The label text for a contract node row: the abbreviated hash (or the `∅`
|
|
1693
|
+
* empty-source token for the baseline) followed by its `@contract`/`@db` markers
|
|
1694
|
+
* and `(refs)`, with two spaces between the hash and the overlay block.
|
|
1695
|
+
*/
|
|
1696
|
+
function formatNodeLabel(contractHash, opts, nodeHighlight) {
|
|
1697
|
+
const style = createLabelStyler(opts);
|
|
1698
|
+
const hashLength = opts.hashLength ?? 7;
|
|
1699
|
+
const emptySource = migrationListEmptySource(opts.glyphMode ?? "unicode");
|
|
1700
|
+
const overlays = overlayNamesForContract(contractHash, opts);
|
|
1701
|
+
const hasOverlays = overlays.markers.length > 0 || overlays.refs.length > 0;
|
|
1702
|
+
const offPath = nodeHighlight === "off-path" && opts.colorize;
|
|
1703
|
+
const hashText = contractHash === EMPTY_CONTRACT_HASH ? (offPath ? forcedDim : style.glyph)(emptySource) : (offPath ? forcedDim : style.sourceHash)(abbreviateHash(contractHash, hashLength, emptySource));
|
|
1704
|
+
if (!hasOverlays) return hashText;
|
|
1705
|
+
return `${hashText} ${formatContractNodeOverlays(style, overlays.markers, overlays.refs)}`;
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* The label text for a migration row: the migration name (padded to
|
|
1709
|
+
* `dirNameWidth`) followed by the `from → to` hash column and the annotation
|
|
1710
|
+
* suffix (ops / status / will-run).
|
|
1711
|
+
*
|
|
1712
|
+
* In flat mode the name is tinted with its lane's hue (`lane` ≥ 0), so the node
|
|
1713
|
+
* `○`, the edges/arrows in the gutter, and the name all read in one colour. In
|
|
1714
|
+
* focus mode the on-path/off-path role overrides the lane hue (bold / dim).
|
|
1715
|
+
*/
|
|
1716
|
+
function formatMigrationLabel(edge, dirNameWidth, opts, lane) {
|
|
1717
|
+
const style = createLabelStyler(opts);
|
|
1718
|
+
const hashLength = opts.hashLength ?? 7;
|
|
1719
|
+
const glyphMode = opts.glyphMode ?? "unicode";
|
|
1720
|
+
const highlight = opts.edgeAnnotationsByHash?.get(edge.migrationHash)?.pathHighlight;
|
|
1721
|
+
let dirNameStyler;
|
|
1722
|
+
let hashOverride;
|
|
1723
|
+
if (highlight === "on-path") {
|
|
1724
|
+
dirNameStyler = opts.colorize ? forcedGreen : (text) => text;
|
|
1725
|
+
hashOverride = void 0;
|
|
1726
|
+
} else if (highlight === "off-path") {
|
|
1727
|
+
dirNameStyler = opts.colorize ? forcedDim : style.dirName;
|
|
1728
|
+
hashOverride = opts.colorize ? forcedDim : void 0;
|
|
1729
|
+
} else if (opts.colorize && lane !== void 0) {
|
|
1730
|
+
dirNameStyler = (text) => laneColorizer(lane)(text);
|
|
1731
|
+
hashOverride = void 0;
|
|
1732
|
+
} else {
|
|
1733
|
+
dirNameStyler = style.dirName;
|
|
1734
|
+
hashOverride = void 0;
|
|
1735
|
+
}
|
|
1736
|
+
const dirNamePadding = " ".repeat(Math.max(0, dirNameWidth - edge.dirName.length));
|
|
1737
|
+
return `${`${dirNameStyler(edge.dirName)}${dirNamePadding}`}${formatEdgeHashColumn(edge, style, hashLength, glyphMode, hashOverride)}${formatEdgeAnnotationSuffix(edge.migrationHash, opts, style)}`;
|
|
1738
|
+
}
|
|
1739
|
+
/**
|
|
1740
|
+
* Format a single on-path migration row for the `migrate --show` run-list.
|
|
1741
|
+
* Shares PATH_HIGHLIGHT_STYLES.onPath with the graph tree so the run-list and
|
|
1742
|
+
* the graph are byte-for-byte identical in their name/hash columns.
|
|
1743
|
+
*/
|
|
1744
|
+
function formatOnPathMigrationRow(dirName, from, to, dirNameWidth, colorize, glyphMode) {
|
|
1745
|
+
const style = createAnsiMigrationListStyler({ useColor: colorize });
|
|
1746
|
+
const styledDirName = `${PATH_HIGHLIGHT_STYLES.onPath(style, colorize).dirName(dirName)}${" ".repeat(Math.max(0, dirNameWidth - dirName.length))}`;
|
|
1747
|
+
const hashLength = 7;
|
|
1748
|
+
const emptySource = migrationListEmptySource(glyphMode);
|
|
1749
|
+
const forwardArrow = migrationListForwardArrow(glyphMode);
|
|
1750
|
+
const fromAbbr = from === EMPTY_CONTRACT_HASH ? padFromHashColumn(style.glyph(emptySource), hashLength) : padFromHashColumn(style.sourceHash(abbreviateHash(from, hashLength, emptySource)), hashLength);
|
|
1751
|
+
const toAbbr = to === EMPTY_CONTRACT_HASH ? style.glyph(emptySource) : style.destHash(abbreviateHash(to, hashLength, emptySource));
|
|
1752
|
+
return `${styledDirName} ${fromAbbr} ${style.glyph(forwardArrow)} ${toAbbr}`;
|
|
1753
|
+
}
|
|
1754
|
+
function legendGlyphs(mode) {
|
|
1755
|
+
return mode === "ascii" ? {
|
|
1756
|
+
node: "*",
|
|
1757
|
+
forward: "^",
|
|
1758
|
+
rollback: "v",
|
|
1759
|
+
self: "@"
|
|
1760
|
+
} : {
|
|
1761
|
+
node: "○",
|
|
1762
|
+
forward: "↑",
|
|
1763
|
+
rollback: "↓",
|
|
1764
|
+
self: "⟲"
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
function formatLegendExampleMarkers(colorize) {
|
|
1768
|
+
if (!colorize) return "@contract @db";
|
|
1769
|
+
const sigil = green("@");
|
|
1770
|
+
return `${sigil + bold(green("contract"))} ${sigil}${green("db")}`;
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* A compact key for the tree visual language: the contract node glyph, the
|
|
1774
|
+
* in-lane direction arrows, the empty baseline, the system-marker `@…` and
|
|
1775
|
+
* user-ref `(…)` conventions, and a worked sample of the data-column hash arrow.
|
|
1776
|
+
*/
|
|
1777
|
+
function renderMigrationGraphLegend(opts) {
|
|
1778
|
+
const glyphMode = opts.glyphMode ?? "unicode";
|
|
1779
|
+
const style = createAnsiMigrationListStyler({ useColor: opts.colorize });
|
|
1780
|
+
const glyphs = legendGlyphs(glyphMode);
|
|
1781
|
+
const emptySource = migrationListEmptySource(glyphMode);
|
|
1782
|
+
const forwardArrow = migrationListForwardArrow(glyphMode);
|
|
1783
|
+
const sampleArrow = `${style.sourceHash("aaaaaa")} ${style.glyph(forwardArrow)} ${style.destHash("bbbbbb")}`;
|
|
1784
|
+
const statusGlyphs = overlayStatusGlyphs(glyphMode);
|
|
1785
|
+
const appliedPending = opts.colorize ? ` ${green(statusGlyphs.applied)} ${style.summary("applied")} ${yellow(statusGlyphs.pending)} ${style.summary("pending")}` : ` ${statusGlyphs.applied} ${style.summary("applied")} ${statusGlyphs.pending} ${style.summary("pending")}`;
|
|
1786
|
+
const exampleMarkers = formatLegendExampleMarkers(opts.colorize);
|
|
1787
|
+
const exampleRefs = opts.colorize ? style.refs(["prod", "staging"]) : "(prod, staging)";
|
|
1788
|
+
return [
|
|
1789
|
+
"Legend:",
|
|
1790
|
+
` ${style.kind(glyphs.node)} ${style.summary("contract")} ${style.kind(glyphs.forward)} ${style.summary("forward")} ${style.kind(glyphs.rollback)} ${style.summary("rollback")}`,
|
|
1791
|
+
` ${style.kind(glyphs.self)} ${style.summary("migration without schema change")}`,
|
|
1792
|
+
appliedPending,
|
|
1793
|
+
` ${style.kind(emptySource)} ${style.summary("empty database (baseline)")}`,
|
|
1794
|
+
` ${exampleMarkers} ${style.summary("reserved markers — also typeable as --from/--to tokens")}`,
|
|
1795
|
+
` ${exampleRefs} ${style.summary("user-defined refs")}`,
|
|
1796
|
+
` ${sampleArrow} ${style.summary("migration from contract aaaaaa to bbbbbb")}`
|
|
1797
|
+
].join("\n");
|
|
1798
|
+
}
|
|
1799
|
+
//#endregion
|
|
1800
|
+
//#region src/utils/formatters/migration-graph-command-render.ts
|
|
1801
|
+
/**
|
|
1802
|
+
* Command graph renderer: composes the gutter (from the grid) with per-row labels.
|
|
1803
|
+
*
|
|
1804
|
+
* Pipeline: buildMigrationGraphRows → buildGrid → renderMigrationGraphCommand
|
|
1805
|
+
*
|
|
1806
|
+
* Each grid row is classified by its cells: a node row gets a contract label;
|
|
1807
|
+
* a migration arrow row gets a migration label; connector rows get no label.
|
|
1808
|
+
* Label format and styling live in `./migration-graph-labels`.
|
|
1809
|
+
*/
|
|
1810
|
+
const LABEL_GAP = 2;
|
|
1811
|
+
const MIN_HASH_DATA_COLUMN = 25;
|
|
1812
|
+
/**
|
|
1813
|
+
* Classify a grid row by its own cells:
|
|
1814
|
+
* - a cell carrying a NodeRef → node row (contract label);
|
|
1815
|
+
* - a cell whose top line is an arrow ({up}/{down}/self-loop) → migration row;
|
|
1816
|
+
* - otherwise → no label.
|
|
1817
|
+
*
|
|
1818
|
+
* A migration's arrow appears in exactly one grid row (the forward `↑` row, the
|
|
1819
|
+
* adjacent-rollback `↓` row, or the self-loop `⟲` row), so each migration gets
|
|
1820
|
+
* exactly one label, on the row that draws its arrow.
|
|
1821
|
+
*
|
|
1822
|
+
* Two distinct migrations with identical content (same from/to/ops) hash to the
|
|
1823
|
+
* SAME migration hash, so the arrow line is matched on BOTH its hash and its
|
|
1824
|
+
* `dirName` (which the LineRef carries per-row) — otherwise both rows would
|
|
1825
|
+
* resolve to one edge and the other migration's name would be lost.
|
|
1826
|
+
*/
|
|
1827
|
+
function classifyRow(row, edgesByHash) {
|
|
1828
|
+
for (const cell of row) if (cell.node !== void 0) return {
|
|
1829
|
+
kind: "node",
|
|
1830
|
+
contractHash: cell.node.contractHash
|
|
1831
|
+
};
|
|
1832
|
+
for (const cell of row) {
|
|
1833
|
+
const arrow = arrowLine(cell);
|
|
1834
|
+
if (arrow === void 0) continue;
|
|
1835
|
+
const candidates = edgesByHash.get(arrow.line.migrationHash) ?? [];
|
|
1836
|
+
const edge = candidates.find((e) => e.dirName === arrow.line.dirName) ?? candidates[0];
|
|
1837
|
+
if (edge !== void 0) return {
|
|
1838
|
+
kind: "migration",
|
|
1839
|
+
edge,
|
|
1840
|
+
lane: arrow.line.lane
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
return { kind: "none" };
|
|
1844
|
+
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Return the cell's arrow line if it carries one — a self-loop, or a line whose
|
|
1847
|
+
* directions are exactly `{up}` or `{down}` (the migration-direction arrows).
|
|
1848
|
+
* Connector/corner/vertical lines are not arrows and yield `undefined`.
|
|
1849
|
+
*/
|
|
1850
|
+
function arrowLine(cell) {
|
|
1851
|
+
for (const line of cell.lines) {
|
|
1852
|
+
if (line.selfLoop === true) return line;
|
|
1853
|
+
if (line.landingArrow === true) continue;
|
|
1854
|
+
const dirs = line.directions;
|
|
1855
|
+
if (dirs.size !== 1) continue;
|
|
1856
|
+
if (dirs.has("up") || dirs.has("down")) return line;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* Resolve each contract's path-highlight role from the edges incident on it.
|
|
1861
|
+
* On-path wins: a contract touched by any on-path edge is on-path. Empty unless
|
|
1862
|
+
* focus-mode annotations are present.
|
|
1863
|
+
*/
|
|
1864
|
+
function resolveNodeHighlights(rowModel, edgeAnnotationsByHash) {
|
|
1865
|
+
const result = /* @__PURE__ */ new Map();
|
|
1866
|
+
if (edgeAnnotationsByHash === void 0) return result;
|
|
1867
|
+
for (const edge of rowModel.edges) {
|
|
1868
|
+
const highlight = edgeAnnotationsByHash.get(edge.migrationHash)?.pathHighlight;
|
|
1869
|
+
if (highlight === void 0) continue;
|
|
1870
|
+
for (const hash of [edge.from, edge.to]) {
|
|
1871
|
+
if (hash === EMPTY_CONTRACT_HASH) continue;
|
|
1872
|
+
if (result.get(hash) !== "on-path") result.set(hash, highlight);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
return result;
|
|
1876
|
+
}
|
|
1877
|
+
function maxDirNameLength(edges) {
|
|
1878
|
+
let max = 0;
|
|
1879
|
+
for (const edge of edges) max = Math.max(max, edge.dirName.length);
|
|
1880
|
+
return max;
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* The label column for a render: the widest gutter (visible width) across every
|
|
1884
|
+
* row, plus the label gap. Labels begin here so they line up regardless of how
|
|
1885
|
+
* deep the lane structure runs on any one row. A cross-space override widens it
|
|
1886
|
+
* so sibling space sections share one column.
|
|
1887
|
+
*/
|
|
1888
|
+
function computeLabelColumn(grid, glyphMode) {
|
|
1889
|
+
let maxGutter = 0;
|
|
1890
|
+
for (const row of grid) {
|
|
1891
|
+
const gutter = renderGridRow(row, {
|
|
1892
|
+
colorize: false,
|
|
1893
|
+
glyphMode
|
|
1894
|
+
});
|
|
1895
|
+
maxGutter = Math.max(maxGutter, stringWidth(gutter));
|
|
1896
|
+
}
|
|
1897
|
+
return maxGutter + LABEL_GAP;
|
|
1898
|
+
}
|
|
1899
|
+
function computeMaxDirNameWidth(rowModel) {
|
|
1900
|
+
return maxDirNameLength(rowModel.edges);
|
|
1901
|
+
}
|
|
1902
|
+
function padVisible(text, targetWidth) {
|
|
1903
|
+
const padding = Math.max(0, targetWidth - stringWidth(text));
|
|
1904
|
+
return text + " ".repeat(padding);
|
|
1905
|
+
}
|
|
1906
|
+
const ANSI_ESCAPE = "\x1B";
|
|
1907
|
+
function trimTrailingWhitespace(line) {
|
|
1908
|
+
const trailingSpaceBeforeReset = new RegExp(`[\\t ]+((?:${ANSI_ESCAPE}\\[[0-9;]*m)+)$`);
|
|
1909
|
+
return line.replace(trailingSpaceBeforeReset, "$1").replace(/\s+$/, "");
|
|
1910
|
+
}
|
|
1911
|
+
function renderMigrationGraphCommand(input) {
|
|
1912
|
+
const { grid, rowModel } = input;
|
|
1913
|
+
const glyphMode = input.glyphMode;
|
|
1914
|
+
const edgesByHash = /* @__PURE__ */ new Map();
|
|
1915
|
+
for (const edge of rowModel.edges) {
|
|
1916
|
+
const bucket = edgesByHash.get(edge.migrationHash);
|
|
1917
|
+
if (bucket) bucket.push(edge);
|
|
1918
|
+
else edgesByHash.set(edge.migrationHash, [edge]);
|
|
1919
|
+
}
|
|
1920
|
+
const labelOpts = {
|
|
1921
|
+
colorize: input.colorize,
|
|
1922
|
+
glyphMode,
|
|
1923
|
+
...ifDefined("refsByHash", input.refsByHash),
|
|
1924
|
+
...ifDefined("edgeAnnotationsByHash", input.edgeAnnotationsByHash),
|
|
1925
|
+
...ifDefined("dbHash", input.dbHash),
|
|
1926
|
+
...ifDefined("contractHash", input.contractHash),
|
|
1927
|
+
...ifDefined("isAppSpace", input.isAppSpace),
|
|
1928
|
+
...ifDefined("activeRefName", input.activeRefName),
|
|
1929
|
+
...ifDefined("styler", input.styler)
|
|
1930
|
+
};
|
|
1931
|
+
const nodeHighlights = resolveNodeHighlights(rowModel, input.edgeAnnotationsByHash);
|
|
1932
|
+
const labelColumn = input.globalLabelColumn ?? computeLabelColumn(grid, glyphMode);
|
|
1933
|
+
const maxDirNameLen = input.globalMaxDirNameWidth ?? maxDirNameLength(rowModel.edges);
|
|
1934
|
+
const dirNameWidth = Math.max(maxDirNameLen + LABEL_GAP, MIN_HASH_DATA_COLUMN - labelColumn);
|
|
1935
|
+
const lines = [];
|
|
1936
|
+
for (const row of grid) {
|
|
1937
|
+
const gutter = renderGridRow(row, {
|
|
1938
|
+
colorize: input.colorize,
|
|
1939
|
+
glyphMode
|
|
1940
|
+
});
|
|
1941
|
+
const identity = classifyRow(row, edgesByHash);
|
|
1942
|
+
if (identity.kind === "none") {
|
|
1943
|
+
lines.push(trimTrailingWhitespace(gutter));
|
|
1944
|
+
continue;
|
|
1945
|
+
}
|
|
1946
|
+
const gutterPad = padVisible(gutter, labelColumn);
|
|
1947
|
+
if (identity.kind === "node") {
|
|
1948
|
+
const label = formatNodeLabel(identity.contractHash, labelOpts, nodeHighlights.get(identity.contractHash));
|
|
1949
|
+
lines.push(trimTrailingWhitespace(label.length === 0 ? gutter : `${gutterPad}${label}`));
|
|
1950
|
+
continue;
|
|
1951
|
+
}
|
|
1952
|
+
const label = formatMigrationLabel(identity.edge, dirNameWidth, labelOpts, identity.lane);
|
|
1953
|
+
lines.push(trimTrailingWhitespace(`${gutterPad}${label}`));
|
|
1954
|
+
}
|
|
1955
|
+
return lines.join("\n");
|
|
1956
|
+
}
|
|
1957
|
+
//#endregion
|
|
1958
|
+
export { migrationListEmptySource as _, renderMigrationGraphLegend as a, renderMigrationListWithStyle as c, highlightFromEdgeAnnotations as d, indentMigrationGraphTreeBlock as f, abbreviateContractHash as g, buildGrid as h, formatOnPathMigrationRow as i, computeGlobalMaxDirNameWidth as l, buildMigrationGraphRows as m, computeMaxDirNameWidth as n, createAnsiMigrationListStyler as o, renderMigrationGraphSpaceTree as p, renderMigrationGraphCommand as r, IDENTITY_MIGRATION_LIST_STYLER as s, computeLabelColumn as t, computeGlobalMaxEdgeTreePrefixWidth as u, migrationListForwardArrow as v };
|
|
1959
|
+
|
|
1960
|
+
//# sourceMappingURL=migration-graph-command-render-CEez7YUK.mjs.map
|