@prisma-next/cli 0.12.0-dev.6 → 0.12.0-dev.61
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-ROablRVC.mjs} +198 -105
- package/dist/client-ROablRVC.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 +292 -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 +4 -4
- 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-Bsp46T8u.mjs} +3 -3
- package/dist/{contract-infer-D8uEbJuu.mjs.map → contract-infer-Bsp46T8u.mjs.map} +1 -1
- 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-CMKyBJZH.mjs} +6 -6
- package/dist/{db-verify-v_vUKXTU.mjs.map → db-verify-CMKyBJZH.mjs.map} +1 -1
- package/dist/exports/control-api.d.mts +2 -2
- 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-CDXkYGh0.mjs} +5 -5
- package/dist/{inspect-live-schema-C6ohV_oQ.mjs.map → inspect-live-schema-CDXkYGh0.mjs.map} +1 -1
- 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-BC3X6KBg.mjs} +5 -5
- package/dist/{migration-command-scaffold-CjvwO6at.mjs.map → migration-command-scaffold-BC3X6KBg.mjs.map} +1 -1
- package/dist/migration-graph-space-render-Cpg0ql8v.mjs +2370 -0
- package/dist/migration-graph-space-render-Cpg0ql8v.mjs.map +1 -0
- package/dist/migration-list-CyLslAtv.mjs +230 -0
- package/dist/migration-list-CyLslAtv.mjs.map +1 -0
- package/dist/migration-log-DvC-Iq_k.mjs +222 -0
- package/dist/migration-log-DvC-Iq_k.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-DUBRTJEl.mjs} +7 -7
- package/dist/{migration-plan-9DJ7q7_z.mjs.map → migration-plan-DUBRTJEl.mjs.map} +1 -1
- package/dist/migration-status-DnEW9YQn.mjs +447 -0
- package/dist/migration-status-DnEW9YQn.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-BepB6ydp.d.mts} +44 -31
- package/dist/types-BepB6ydp.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 +25 -22
- package/src/cli.ts +5 -0
- 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/json/schemas.ts +195 -0
- package/src/commands/migrate.ts +518 -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-show.ts +31 -66
- package/src/commands/migration-status-overlay.ts +61 -0
- package/src/commands/migration-status.ts +457 -1067
- package/src/commands/telemetry/index.ts +107 -0
- package/src/commands/telemetry/status.ts +67 -0
- package/src/control-api/client.ts +40 -9
- package/src/control-api/operations/contract-emit.ts +22 -2
- package/src/control-api/operations/db-init.ts +3 -3
- package/src/control-api/operations/{db-apply.ts → db-run.ts} +51 -13
- package/src/control-api/operations/db-update.ts +4 -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 +46 -29
- package/src/utils/cli-errors.ts +70 -2
- package/src/utils/formatters/errors.ts +11 -0
- package/src/utils/formatters/migration-graph-lane-colors.ts +194 -0
- package/src/utils/formatters/migration-graph-layout.ts +227 -38
- package/src/utils/formatters/migration-graph-rows.ts +128 -15
- package/src/utils/formatters/migration-graph-space-render.ts +148 -0
- package/src/utils/formatters/migration-graph-tree-render.ts +959 -81
- 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/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/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-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
|
@@ -0,0 +1,2370 @@
|
|
|
1
|
+
import { bold, createColors, cyan, cyanBright, dim, green, yellow } from "colorette";
|
|
2
|
+
import stringWidth from "string-width";
|
|
3
|
+
import { EMPTY_CONTRACT_HASH } from "@prisma-next/migration-tools/constants";
|
|
4
|
+
//#region src/utils/formatters/migration-graph-layout.ts
|
|
5
|
+
function forwardEdges(edges) {
|
|
6
|
+
return edges.filter((e) => e.kind === "forward");
|
|
7
|
+
}
|
|
8
|
+
function buildForwardProducersByTo(edges) {
|
|
9
|
+
const byTo = /* @__PURE__ */ new Map();
|
|
10
|
+
for (const edge of edges) {
|
|
11
|
+
if (edge.kind !== "forward") continue;
|
|
12
|
+
const bucket = byTo.get(edge.to);
|
|
13
|
+
if (bucket) bucket.push(edge);
|
|
14
|
+
else byTo.set(edge.to, [edge]);
|
|
15
|
+
}
|
|
16
|
+
return byTo;
|
|
17
|
+
}
|
|
18
|
+
function buildForwardOutDegree(edges) {
|
|
19
|
+
const out = /* @__PURE__ */ new Map();
|
|
20
|
+
for (const edge of edges) {
|
|
21
|
+
if (edge.kind !== "forward" || edge.from === edge.to) continue;
|
|
22
|
+
out.set(edge.from, (out.get(edge.from) ?? 0) + 1);
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
function buildForwardInDegree(edges) {
|
|
27
|
+
const indeg = /* @__PURE__ */ new Map();
|
|
28
|
+
for (const edge of forwardEdges(edges)) {
|
|
29
|
+
if (edge.from === edge.to) continue;
|
|
30
|
+
indeg.set(edge.to, (indeg.get(edge.to) ?? 0) + 1);
|
|
31
|
+
}
|
|
32
|
+
return indeg;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Distinct source contracts among a contract's forward producers. A contract is
|
|
36
|
+
* a *convergence* when this count is >= 2. Multiple migrations sharing one
|
|
37
|
+
* source (a multi-edge) count once — they stack in a single lane rather than
|
|
38
|
+
* fanning into a convergence.
|
|
39
|
+
*/
|
|
40
|
+
function buildDistinctSourceCountByTo(edges) {
|
|
41
|
+
const sources = /* @__PURE__ */ new Map();
|
|
42
|
+
for (const edge of edges) {
|
|
43
|
+
if (edge.kind !== "forward" || edge.from === edge.to) continue;
|
|
44
|
+
const set = sources.get(edge.to);
|
|
45
|
+
if (set) set.add(edge.from);
|
|
46
|
+
else sources.set(edge.to, new Set([edge.from]));
|
|
47
|
+
}
|
|
48
|
+
const counts = /* @__PURE__ */ new Map();
|
|
49
|
+
for (const [to, set] of sources) counts.set(to, set.size);
|
|
50
|
+
return counts;
|
|
51
|
+
}
|
|
52
|
+
function splitComponents(nodes) {
|
|
53
|
+
const components = [];
|
|
54
|
+
let current = [];
|
|
55
|
+
for (const node of nodes) {
|
|
56
|
+
if (node === null) {
|
|
57
|
+
if (current.length > 0) {
|
|
58
|
+
components.push(current);
|
|
59
|
+
current = [];
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
current.push(node);
|
|
64
|
+
}
|
|
65
|
+
if (current.length > 0) components.push(current);
|
|
66
|
+
return components;
|
|
67
|
+
}
|
|
68
|
+
function classifyForwardShortConvergenceAdjacency(rows, edgeRowIndex, edge, laneIndex) {
|
|
69
|
+
for (let index = edgeRowIndex + 1; index < rows.length; index++) {
|
|
70
|
+
const row = rows[index];
|
|
71
|
+
if (row === void 0) break;
|
|
72
|
+
if (row.kind === "component-separator" || row.kind === "branch-connector") continue;
|
|
73
|
+
if (row.kind === "merge-connector") continue;
|
|
74
|
+
if (row.kind === "edge") {
|
|
75
|
+
if (row.laneIndex === laneIndex) return "node-skipping-forward";
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (row.kind === "node" && row.contractHash === edge.from) return "adjacent";
|
|
79
|
+
}
|
|
80
|
+
return "node-skipping-forward";
|
|
81
|
+
}
|
|
82
|
+
function convergenceProducerUsesShortAdjacency(edge, laneIndex, forwardProducersByTo, producerLaneByHash) {
|
|
83
|
+
const producers = (forwardProducersByTo.get(edge.to) ?? []).filter((candidate) => candidate.kind === "forward");
|
|
84
|
+
if (producers.length < 2) return false;
|
|
85
|
+
const fanStart = [...new Set(producers.map((producer) => producerLaneByHash.get(producer.migrationHash)).filter((candidate) => candidate !== void 0))].sort((a, b) => a - b)[0];
|
|
86
|
+
if (fanStart === void 0) return false;
|
|
87
|
+
return laneIndex === fanStart;
|
|
88
|
+
}
|
|
89
|
+
function classifyForwardLayoutAdjacency(rows, edgeRowIndex, edge, laneIndex, passThroughLanes, nodeColumn, convergenceProducer, divergenceBranchEdge) {
|
|
90
|
+
let sawObstruction = false;
|
|
91
|
+
const passThroughLaneSet = new Set(passThroughLanes);
|
|
92
|
+
for (let index = edgeRowIndex + 1; index < rows.length; index++) {
|
|
93
|
+
const row = rows[index];
|
|
94
|
+
if (row === void 0) break;
|
|
95
|
+
if (row.kind === "component-separator") continue;
|
|
96
|
+
if (row.kind === "merge-connector") {
|
|
97
|
+
if (convergenceProducer) {
|
|
98
|
+
if (row.contractHash === edge.from) sawObstruction = true;
|
|
99
|
+
} else if (!divergenceBranchEdge && row.contractHash !== edge.from) sawObstruction = true;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (row.kind === "branch-connector") continue;
|
|
103
|
+
if (row.kind === "edge") {
|
|
104
|
+
if (row.laneIndex === laneIndex) return "node-skipping-forward";
|
|
105
|
+
if (!divergenceBranchEdge && row.edge !== void 0 && row.edge.to !== edge.to) sawObstruction = true;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (row.kind === "node" && row.contractHash !== void 0) {
|
|
109
|
+
if (row.contractHash === edge.from) return sawObstruction ? "node-skipping-forward" : "adjacent";
|
|
110
|
+
const nodeCol = nodeColumn.get(row.contractHash) ?? 0;
|
|
111
|
+
if (!divergenceBranchEdge && !passThroughLaneSet.has(nodeCol)) sawObstruction = true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return "node-skipping-forward";
|
|
115
|
+
}
|
|
116
|
+
function classifyLayoutAdjacency(rows, edgeRowIndex, edge, laneIndex, passThroughLanes, nodeColumn, position, forwardInDegree, convergenceProducer, divergenceBranchEdge) {
|
|
117
|
+
if (edge.kind === "self") return "adjacent";
|
|
118
|
+
const fromPos = position.get(edge.from);
|
|
119
|
+
const toPos = position.get(edge.to);
|
|
120
|
+
if (edge.kind === "forward") {
|
|
121
|
+
if ((forwardInDegree.get(edge.to) ?? 0) <= 1 && fromPos !== void 0 && toPos !== void 0 && fromPos === toPos + 1) return "adjacent";
|
|
122
|
+
return classifyForwardLayoutAdjacency(rows, edgeRowIndex, edge, laneIndex, passThroughLanes, nodeColumn, convergenceProducer, divergenceBranchEdge);
|
|
123
|
+
}
|
|
124
|
+
if (fromPos !== void 0 && toPos !== void 0 && toPos === fromPos + 1) return "adjacent";
|
|
125
|
+
for (let index = edgeRowIndex + 1; index < rows.length; index++) {
|
|
126
|
+
const row = rows[index];
|
|
127
|
+
if (row === void 0) break;
|
|
128
|
+
if (row.kind === "component-separator" || row.kind === "branch-connector" || row.kind === "merge-connector") continue;
|
|
129
|
+
if (row.kind === "edge") continue;
|
|
130
|
+
if (row.kind === "node") return row.contractHash === edge.to ? "adjacent" : "node-skipping-rollback";
|
|
131
|
+
}
|
|
132
|
+
return "node-skipping-rollback";
|
|
133
|
+
}
|
|
134
|
+
function refineAdjacency(rows, nodeColumn, position, forwardInDegree, forwardOutDegree, edges, producerLaneByHash) {
|
|
135
|
+
const forwardProducersByTo = buildForwardProducersByTo(edges);
|
|
136
|
+
function branchLaneForEdge(producer) {
|
|
137
|
+
const children = edges.filter((edge) => edge.from === producer.from && edge.kind === "forward" && edge.from !== edge.to);
|
|
138
|
+
if (children.length < 2) return void 0;
|
|
139
|
+
const index = children.findIndex((child) => child.migrationHash === producer.migrationHash);
|
|
140
|
+
return index >= 0 ? index : void 0;
|
|
141
|
+
}
|
|
142
|
+
return rows.map((row, rowIndex) => {
|
|
143
|
+
if (row.kind !== "edge" || row.edge === void 0 || row.laneIndex === void 0) return row;
|
|
144
|
+
const divergenceBranchEdge = row.edge.kind === "forward" && !(row.convergenceProducer ?? false) && (forwardOutDegree.get(row.edge.from) ?? 0) >= 2 && branchLaneForEdge(row.edge) !== void 0;
|
|
145
|
+
const adjacency = row.convergenceProducer === true && convergenceProducerUsesShortAdjacency(row.edge, row.laneIndex, forwardProducersByTo, producerLaneByHash) ? classifyForwardShortConvergenceAdjacency(rows, rowIndex, row.edge, row.laneIndex) : classifyLayoutAdjacency(rows, rowIndex, row.edge, row.laneIndex, row.passThroughLanes ?? [], nodeColumn, position, forwardInDegree, row.convergenceProducer ?? false, divergenceBranchEdge);
|
|
146
|
+
const existingLaneEdge = /* @__PURE__ */ new Map();
|
|
147
|
+
for (const lane of row.passThroughLanes ?? []) {
|
|
148
|
+
const cell = row.cells[lane];
|
|
149
|
+
if (cell !== void 0 && "migrationHash" in cell && cell.migrationHash !== void 0) existingLaneEdge.set(lane, cell.migrationHash);
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
...row,
|
|
153
|
+
cells: buildEdgeCells(row.edge, row.laneIndex, row.passThroughLanes ?? [], adjacency, row.cells.length, existingLaneEdge)
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function classifyEdgeAdjacency(edge, position) {
|
|
158
|
+
if (edge.kind === "self") return "adjacent";
|
|
159
|
+
const fromPos = position.get(edge.from);
|
|
160
|
+
const toPos = position.get(edge.to);
|
|
161
|
+
if (fromPos === void 0 || toPos === void 0) return "adjacent";
|
|
162
|
+
if (edge.kind === "forward") {
|
|
163
|
+
if (toPos >= fromPos) return "adjacent";
|
|
164
|
+
return fromPos === toPos + 1 ? "adjacent" : "node-skipping-forward";
|
|
165
|
+
}
|
|
166
|
+
if (toPos <= fromPos) return "adjacent";
|
|
167
|
+
return toPos === fromPos + 1 ? "adjacent" : "node-skipping-rollback";
|
|
168
|
+
}
|
|
169
|
+
function emptyCells(width) {
|
|
170
|
+
return Array.from({ length: width }, () => ({ kind: "empty" }));
|
|
171
|
+
}
|
|
172
|
+
/** Returns `{ migrationHash: hash }` when hash is defined, otherwise `{}`. */
|
|
173
|
+
function hashProp(hash) {
|
|
174
|
+
return hash !== void 0 ? { migrationHash: hash } : {};
|
|
175
|
+
}
|
|
176
|
+
/** Returns `{ arcMigrationHash: hash }` when hash is defined, otherwise `{}`. */
|
|
177
|
+
function arcHashProp(hash) {
|
|
178
|
+
return hash !== void 0 ? { arcMigrationHash: hash } : {};
|
|
179
|
+
}
|
|
180
|
+
function buildBranchConnectorCells(startLane, endLane, fanTargetLanes, activeLanes, gridWidth, trunkEdgeHash, fanEdgeHashByLane, laneEdgeByIndex) {
|
|
181
|
+
const cells = emptyCells(gridWidth);
|
|
182
|
+
for (let lane = 0; lane < gridWidth; lane++) {
|
|
183
|
+
if (activeLanes.has(lane) && (lane < startLane || lane > endLane)) {
|
|
184
|
+
cells[lane] = {
|
|
185
|
+
kind: "vertical-pass",
|
|
186
|
+
...hashProp(laneEdgeByIndex.get(lane))
|
|
187
|
+
};
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (lane === startLane) cells[lane] = {
|
|
191
|
+
kind: "branch-tee",
|
|
192
|
+
...hashProp(trunkEdgeHash)
|
|
193
|
+
};
|
|
194
|
+
else if (lane === endLane) cells[lane] = {
|
|
195
|
+
kind: "branch-corner",
|
|
196
|
+
...hashProp(fanEdgeHashByLane.get(lane))
|
|
197
|
+
};
|
|
198
|
+
else if (lane > startLane && lane < endLane) if (fanTargetLanes.has(lane)) cells[lane] = {
|
|
199
|
+
kind: "branch-tee",
|
|
200
|
+
...hashProp(fanEdgeHashByLane.get(lane))
|
|
201
|
+
};
|
|
202
|
+
else if (activeLanes.has(lane)) cells[lane] = {
|
|
203
|
+
kind: "arc-crossing",
|
|
204
|
+
...hashProp(laneEdgeByIndex.get(lane)),
|
|
205
|
+
...arcHashProp(fanEdgeHashByLane.get(endLane))
|
|
206
|
+
};
|
|
207
|
+
else cells[lane] = {
|
|
208
|
+
kind: "branch-tee",
|
|
209
|
+
...hashProp(fanEdgeHashByLane.get(lane))
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return cells;
|
|
213
|
+
}
|
|
214
|
+
function buildMergeConnectorCells(startLane, endLane, fanTargetLanes, activeLanes, gridWidth, laneEdgeByIndex) {
|
|
215
|
+
const cells = emptyCells(gridWidth);
|
|
216
|
+
for (let lane = 0; lane < gridWidth; lane++) {
|
|
217
|
+
if (activeLanes.has(lane) && (lane < startLane || lane > endLane)) {
|
|
218
|
+
cells[lane] = {
|
|
219
|
+
kind: "vertical-pass",
|
|
220
|
+
...hashProp(laneEdgeByIndex.get(lane))
|
|
221
|
+
};
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (lane === startLane) cells[lane] = {
|
|
225
|
+
kind: "merge-tee",
|
|
226
|
+
...hashProp(laneEdgeByIndex.get(lane))
|
|
227
|
+
};
|
|
228
|
+
else if (lane === endLane) cells[lane] = {
|
|
229
|
+
kind: "merge-corner",
|
|
230
|
+
...hashProp(laneEdgeByIndex.get(lane))
|
|
231
|
+
};
|
|
232
|
+
else if (lane > startLane && lane < endLane) if (fanTargetLanes.has(lane)) cells[lane] = {
|
|
233
|
+
kind: "merge-tee",
|
|
234
|
+
...hashProp(laneEdgeByIndex.get(lane))
|
|
235
|
+
};
|
|
236
|
+
else if (activeLanes.has(lane)) cells[lane] = {
|
|
237
|
+
kind: "arc-crossing",
|
|
238
|
+
...hashProp(laneEdgeByIndex.get(lane)),
|
|
239
|
+
...arcHashProp(laneEdgeByIndex.get(endLane))
|
|
240
|
+
};
|
|
241
|
+
else cells[lane] = {
|
|
242
|
+
kind: "horizontal-pass",
|
|
243
|
+
...hashProp(laneEdgeByIndex.get(startLane))
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return cells;
|
|
247
|
+
}
|
|
248
|
+
function buildNodeCells(contractHash, nodeColumn, activeLanes, gridWidth, laneEdgeByIndex) {
|
|
249
|
+
const cells = emptyCells(gridWidth);
|
|
250
|
+
for (const lane of activeLanes) if (lane !== nodeColumn && lane < gridWidth) cells[lane] = {
|
|
251
|
+
kind: "vertical-pass",
|
|
252
|
+
...hashProp(laneEdgeByIndex.get(lane))
|
|
253
|
+
};
|
|
254
|
+
if (nodeColumn < gridWidth) cells[nodeColumn] = {
|
|
255
|
+
kind: "node",
|
|
256
|
+
contractHash
|
|
257
|
+
};
|
|
258
|
+
return cells;
|
|
259
|
+
}
|
|
260
|
+
function buildEdgeCells(edge, laneIndex, passThroughLanes, adjacency, gridWidth, laneEdgeByIndex) {
|
|
261
|
+
const cells = emptyCells(gridWidth);
|
|
262
|
+
for (const lane of passThroughLanes) if (lane < gridWidth) cells[lane] = {
|
|
263
|
+
kind: "vertical-pass",
|
|
264
|
+
...hashProp(laneEdgeByIndex.get(lane))
|
|
265
|
+
};
|
|
266
|
+
if (laneIndex < gridWidth) cells[laneIndex] = {
|
|
267
|
+
kind: "edge-lane",
|
|
268
|
+
migrationHash: edge.migrationHash,
|
|
269
|
+
edgeKind: edge.kind,
|
|
270
|
+
ownsLabel: true,
|
|
271
|
+
adjacency
|
|
272
|
+
};
|
|
273
|
+
return cells;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Compute the vertical node order for a component: tips at the top (index 0),
|
|
277
|
+
* roots at the bottom. This is a DFS post-order over forward edges starting
|
|
278
|
+
* from forward roots, visiting children in their input (insertion) order. A
|
|
279
|
+
* node is emitted only after all of its forward children, so convergence nodes
|
|
280
|
+
* sit below every branch that feeds them and the longest contiguous chain reads
|
|
281
|
+
* top-to-bottom without braiding.
|
|
282
|
+
*/
|
|
283
|
+
function computeVerticalOrder(componentNodes, forwardChildren, forwardInDegree) {
|
|
284
|
+
const WHITE = 0;
|
|
285
|
+
const GRAY = 1;
|
|
286
|
+
const BLACK = 2;
|
|
287
|
+
const color = /* @__PURE__ */ new Map();
|
|
288
|
+
for (const node of componentNodes) color.set(node, WHITE);
|
|
289
|
+
const sortRoots = (roots) => [...roots].sort((a, b) => {
|
|
290
|
+
if (a === EMPTY_CONTRACT_HASH) return -1;
|
|
291
|
+
if (b === EMPTY_CONTRACT_HASH) return 1;
|
|
292
|
+
return a.localeCompare(b);
|
|
293
|
+
});
|
|
294
|
+
let roots = sortRoots(componentNodes.filter((n) => (forwardInDegree.get(n) ?? 0) === 0));
|
|
295
|
+
if (roots.length === 0) roots = sortRoots(componentNodes);
|
|
296
|
+
const result = [];
|
|
297
|
+
function runDfs(root) {
|
|
298
|
+
if (color.get(root) !== WHITE) return;
|
|
299
|
+
const stack = [{
|
|
300
|
+
node: root,
|
|
301
|
+
children: forwardChildren.get(root) ?? [],
|
|
302
|
+
index: 0
|
|
303
|
+
}];
|
|
304
|
+
color.set(root, GRAY);
|
|
305
|
+
while (stack.length > 0) {
|
|
306
|
+
const frame = stack[stack.length - 1];
|
|
307
|
+
if (frame === void 0) break;
|
|
308
|
+
if (frame.index >= frame.children.length) {
|
|
309
|
+
color.set(frame.node, BLACK);
|
|
310
|
+
result.push(frame.node);
|
|
311
|
+
stack.pop();
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const child = frame.children[frame.index];
|
|
315
|
+
frame.index += 1;
|
|
316
|
+
if (child === void 0) continue;
|
|
317
|
+
if (color.get(child.to) === WHITE) {
|
|
318
|
+
color.set(child.to, GRAY);
|
|
319
|
+
stack.push({
|
|
320
|
+
node: child.to,
|
|
321
|
+
children: forwardChildren.get(child.to) ?? [],
|
|
322
|
+
index: 0
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
for (const root of roots) runDfs(root);
|
|
328
|
+
for (const node of componentNodes) if (color.get(node) === WHITE) runDfs(node);
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
function rollbackSpan(edge, position) {
|
|
332
|
+
const top = position.get(edge.from) ?? 0;
|
|
333
|
+
return {
|
|
334
|
+
top,
|
|
335
|
+
bottom: position.get(edge.to) ?? top
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function spansOverlap(a, b) {
|
|
339
|
+
return a.top <= b.bottom && b.top <= a.bottom;
|
|
340
|
+
}
|
|
341
|
+
function forwardMaxLane(rows, skipMigrationHashes) {
|
|
342
|
+
let max = 0;
|
|
343
|
+
for (const row of rows) {
|
|
344
|
+
if (row.kind === "edge" && row.edge !== void 0 && skipMigrationHashes.has(row.edge.migrationHash)) continue;
|
|
345
|
+
max = Math.max(max, row.laneIndex ?? 0);
|
|
346
|
+
for (const lane of row.passThroughLanes ?? []) max = Math.max(max, lane);
|
|
347
|
+
if (row.startLane !== void 0) max = Math.max(max, row.startLane, row.endLane ?? row.startLane);
|
|
348
|
+
}
|
|
349
|
+
return max;
|
|
350
|
+
}
|
|
351
|
+
function allocateSkipRollbackBackLanes(skipRollbacks, position, forwardMax) {
|
|
352
|
+
const sorted = [...skipRollbacks].sort((a, b) => {
|
|
353
|
+
const aTop = position.get(a.from) ?? 0;
|
|
354
|
+
const bTop = position.get(b.from) ?? 0;
|
|
355
|
+
if (aTop !== bTop) return aTop - bTop;
|
|
356
|
+
return b.dirName.localeCompare(a.dirName);
|
|
357
|
+
});
|
|
358
|
+
const occupied = [];
|
|
359
|
+
const lanes = /* @__PURE__ */ new Map();
|
|
360
|
+
let nextLane = forwardMax + 1;
|
|
361
|
+
for (const edge of sorted) {
|
|
362
|
+
const span = rollbackSpan(edge, position);
|
|
363
|
+
let lane = nextLane;
|
|
364
|
+
while (occupied.some((entry) => entry.lane === lane && spansOverlap(entry, span))) lane += 1;
|
|
365
|
+
occupied.push({
|
|
366
|
+
...span,
|
|
367
|
+
lane
|
|
368
|
+
});
|
|
369
|
+
lanes.set(edge.migrationHash, lane);
|
|
370
|
+
nextLane = Math.max(nextLane, lane + 1);
|
|
371
|
+
}
|
|
372
|
+
return lanes;
|
|
373
|
+
}
|
|
374
|
+
function findNodeRowIndex(rows, contractHash) {
|
|
375
|
+
return rows.findIndex((row) => row.kind === "node" && row.contractHash === contractHash);
|
|
376
|
+
}
|
|
377
|
+
function findEdgeRowIndex(rows, migrationHash) {
|
|
378
|
+
return rows.findIndex((row) => row.kind === "edge" && row.edge?.migrationHash === migrationHash);
|
|
379
|
+
}
|
|
380
|
+
function ensureCellWidth(cells, width) {
|
|
381
|
+
while (cells.length < width) cells.push({ kind: "empty" });
|
|
382
|
+
}
|
|
383
|
+
function cloneRow(row) {
|
|
384
|
+
return {
|
|
385
|
+
...row,
|
|
386
|
+
cells: [...row.cells]
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function routeCrossesRow(route, rowIndex, rows) {
|
|
390
|
+
const sourceRow = findNodeRowIndex(rows, route.edge.from);
|
|
391
|
+
const targetRow = findNodeRowIndex(rows, route.edge.to);
|
|
392
|
+
if (sourceRow < 0 || targetRow < 0) return false;
|
|
393
|
+
return rowIndex > sourceRow && rowIndex <= targetRow;
|
|
394
|
+
}
|
|
395
|
+
function applySkipRollbackRouting(rows, skipRollbacks, position, nodeColumn, edgeColumn) {
|
|
396
|
+
if (skipRollbacks.length === 0) return [...rows];
|
|
397
|
+
const forwardMax = forwardMaxLane(rows, new Set(skipRollbacks.map((edge) => edge.migrationHash)));
|
|
398
|
+
const backLaneByHash = allocateSkipRollbackBackLanes(skipRollbacks, position, forwardMax);
|
|
399
|
+
const routes = skipRollbacks.map((edge) => ({
|
|
400
|
+
edge,
|
|
401
|
+
backLane: backLaneByHash.get(edge.migrationHash) ?? forwardMax + 1
|
|
402
|
+
}));
|
|
403
|
+
const result = rows.map(cloneRow);
|
|
404
|
+
for (const route of routes) {
|
|
405
|
+
const { edge, backLane } = route;
|
|
406
|
+
const nodeCol = nodeColumn.get(edge.from) ?? 0;
|
|
407
|
+
const targetCol = nodeColumn.get(edge.to) ?? 0;
|
|
408
|
+
const sourceRowIndex = findNodeRowIndex(result, edge.from);
|
|
409
|
+
const targetRowIndex = findNodeRowIndex(result, edge.to);
|
|
410
|
+
const edgeRowIndex = findEdgeRowIndex(result, edge.migrationHash);
|
|
411
|
+
if (sourceRowIndex < 0 || targetRowIndex < 0 || edgeRowIndex < 0) continue;
|
|
412
|
+
edgeColumn.set(edge.migrationHash, backLane);
|
|
413
|
+
const coSourcedLanes = routes.filter((other) => other.edge.from === edge.from).map((other) => other.backLane);
|
|
414
|
+
const maxCoSourcedLane = Math.max(...coSourcedLanes);
|
|
415
|
+
const coLandingLanes = routes.filter((other) => other.edge.to === edge.to).map((other) => other.backLane);
|
|
416
|
+
const maxCoLandingLane = Math.max(...coLandingLanes);
|
|
417
|
+
const { migrationHash: arcHash } = edge;
|
|
418
|
+
const sourceRow = result[sourceRowIndex];
|
|
419
|
+
if (sourceRow !== void 0) {
|
|
420
|
+
const cells = sourceRow.cells;
|
|
421
|
+
ensureCellWidth(cells, backLane + 1);
|
|
422
|
+
cells[nodeCol] = {
|
|
423
|
+
kind: "node",
|
|
424
|
+
contractHash: sourceRow.contractHash ?? EMPTY_CONTRACT_HASH,
|
|
425
|
+
arcTee: true
|
|
426
|
+
};
|
|
427
|
+
for (let lane = nodeCol + 1; lane < backLane; lane += 1) {
|
|
428
|
+
if (coSourcedLanes.includes(lane)) {
|
|
429
|
+
cells[lane] = {
|
|
430
|
+
kind: "arc-branch-tee",
|
|
431
|
+
...hashProp(routes.find((r) => r.backLane === lane && r.edge.from === edge.from)?.edge.migrationHash)
|
|
432
|
+
};
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const existing = cells[lane];
|
|
436
|
+
if (existing !== void 0 && existing.kind !== "empty" && existing.kind !== "horizontal-pass" && existing.kind !== "arc-land-bridge" || routes.some((other) => other.edge.migrationHash !== arcHash && other.backLane === lane && routeCrossesRow(other, sourceRowIndex, result))) cells[lane] = {
|
|
437
|
+
kind: "arc-crossing",
|
|
438
|
+
...hashProp(existing !== void 0 && "migrationHash" in existing ? existing.migrationHash : void 0),
|
|
439
|
+
arcMigrationHash: arcHash
|
|
440
|
+
};
|
|
441
|
+
else cells[lane] = {
|
|
442
|
+
kind: "horizontal-pass",
|
|
443
|
+
migrationHash: arcHash
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
cells[backLane] = backLane < maxCoSourcedLane ? {
|
|
447
|
+
kind: "arc-branch-tee",
|
|
448
|
+
migrationHash: arcHash
|
|
449
|
+
} : {
|
|
450
|
+
kind: "arc-branch-corner",
|
|
451
|
+
migrationHash: arcHash
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
const edgeRow = result[edgeRowIndex];
|
|
455
|
+
if (edgeRow !== void 0) {
|
|
456
|
+
const cells = edgeRow.cells;
|
|
457
|
+
ensureCellWidth(cells, backLane + 1);
|
|
458
|
+
const forwardLaneCell = cells[nodeCol];
|
|
459
|
+
cells[nodeCol] = {
|
|
460
|
+
kind: "vertical-pass",
|
|
461
|
+
...hashProp(forwardLaneCell !== void 0 && "migrationHash" in forwardLaneCell ? forwardLaneCell.migrationHash : void 0)
|
|
462
|
+
};
|
|
463
|
+
cells[backLane] = {
|
|
464
|
+
kind: "edge-lane",
|
|
465
|
+
migrationHash: arcHash,
|
|
466
|
+
edgeKind: edge.kind,
|
|
467
|
+
ownsLabel: true,
|
|
468
|
+
adjacency: "node-skipping-rollback"
|
|
469
|
+
};
|
|
470
|
+
result[edgeRowIndex] = {
|
|
471
|
+
...edgeRow,
|
|
472
|
+
laneIndex: backLane,
|
|
473
|
+
passThroughLanes: [nodeCol]
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
for (let index = sourceRowIndex + 1; index < targetRowIndex; index += 1) {
|
|
477
|
+
if (index === edgeRowIndex) continue;
|
|
478
|
+
const row = result[index];
|
|
479
|
+
if (row === void 0) continue;
|
|
480
|
+
const cells = row.cells;
|
|
481
|
+
ensureCellWidth(cells, backLane + 1);
|
|
482
|
+
const existing = cells[backLane];
|
|
483
|
+
if (existing?.kind !== "arc-land-corner" && existing?.kind !== "arc-land-tee" && existing?.kind !== "arc-land-bridge" && existing?.kind !== "arc-branch-corner" && existing?.kind !== "arc-branch-tee" && existing?.kind !== "arc-crossing") cells[backLane] = {
|
|
484
|
+
kind: "vertical-pass",
|
|
485
|
+
migrationHash: arcHash
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
const targetRow = result[targetRowIndex];
|
|
489
|
+
if (targetRow !== void 0) {
|
|
490
|
+
const cells = targetRow.cells;
|
|
491
|
+
ensureCellWidth(cells, backLane + 1);
|
|
492
|
+
cells[targetCol] = {
|
|
493
|
+
kind: "node",
|
|
494
|
+
contractHash: targetRow.contractHash ?? EMPTY_CONTRACT_HASH,
|
|
495
|
+
arcLand: true
|
|
496
|
+
};
|
|
497
|
+
for (let lane = targetCol + 1; lane < backLane; lane += 1) {
|
|
498
|
+
if (coLandingLanes.includes(lane)) {
|
|
499
|
+
cells[lane] = {
|
|
500
|
+
kind: "arc-land-tee",
|
|
501
|
+
...hashProp(routes.find((r) => r.backLane === lane && r.edge.to === edge.to)?.edge.migrationHash)
|
|
502
|
+
};
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
const existing = cells[lane];
|
|
506
|
+
if (existing !== void 0 && existing.kind !== "empty" && existing.kind !== "horizontal-pass" && existing.kind !== "arc-land-bridge" && existing.kind !== "arc-land-tee" || routes.some((other) => other.edge.migrationHash !== arcHash && other.backLane === lane && routeCrossesRow(other, targetRowIndex, result))) cells[lane] = {
|
|
507
|
+
kind: "arc-crossing",
|
|
508
|
+
...hashProp(existing !== void 0 && "migrationHash" in existing ? existing.migrationHash : void 0),
|
|
509
|
+
arcMigrationHash: arcHash
|
|
510
|
+
};
|
|
511
|
+
else cells[lane] = {
|
|
512
|
+
kind: "arc-land-bridge",
|
|
513
|
+
migrationHash: arcHash
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
cells[backLane] = backLane < maxCoLandingLane ? {
|
|
517
|
+
kind: "arc-land-tee",
|
|
518
|
+
migrationHash: arcHash
|
|
519
|
+
} : {
|
|
520
|
+
kind: "arc-land-corner",
|
|
521
|
+
migrationHash: arcHash
|
|
522
|
+
};
|
|
523
|
+
for (const other of routes) {
|
|
524
|
+
if (other.backLane <= backLane) continue;
|
|
525
|
+
if (!routeCrossesRow(other, targetRowIndex, result)) continue;
|
|
526
|
+
ensureCellWidth(cells, other.backLane + 1);
|
|
527
|
+
const existing = cells[other.backLane];
|
|
528
|
+
if (existing?.kind !== "arc-land-corner" && existing?.kind !== "arc-land-tee" && existing?.kind !== "arc-land-bridge" && existing?.kind !== "node") cells[other.backLane] = {
|
|
529
|
+
kind: "vertical-pass",
|
|
530
|
+
migrationHash: other.edge.migrationHash
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return result;
|
|
536
|
+
}
|
|
537
|
+
function collectNodeSkippingRollbacks(edges, position) {
|
|
538
|
+
return edges.filter((edge) => edge.kind === "rollback" && classifyEdgeAdjacency(edge, position) === "node-skipping-rollback");
|
|
539
|
+
}
|
|
540
|
+
function layoutComponent(componentNodes, allEdges) {
|
|
541
|
+
const componentSet = new Set(componentNodes);
|
|
542
|
+
const edges = allEdges.filter((e) => componentSet.has(e.from) && componentSet.has(e.to));
|
|
543
|
+
const forwardChildren = /* @__PURE__ */ new Map();
|
|
544
|
+
const producersByTo = /* @__PURE__ */ new Map();
|
|
545
|
+
const rollbacksByFrom = /* @__PURE__ */ new Map();
|
|
546
|
+
const selfByFrom = /* @__PURE__ */ new Map();
|
|
547
|
+
for (const edge of edges) {
|
|
548
|
+
if (edge.kind === "self" || edge.from === edge.to) {
|
|
549
|
+
const bucket = selfByFrom.get(edge.from);
|
|
550
|
+
if (bucket) bucket.push(edge);
|
|
551
|
+
else selfByFrom.set(edge.from, [edge]);
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
if (edge.kind === "forward") {
|
|
555
|
+
const children = forwardChildren.get(edge.from);
|
|
556
|
+
if (children) children.push(edge);
|
|
557
|
+
else forwardChildren.set(edge.from, [edge]);
|
|
558
|
+
const producers = producersByTo.get(edge.to);
|
|
559
|
+
if (producers) producers.push(edge);
|
|
560
|
+
else producersByTo.set(edge.to, [edge]);
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
const bucket = rollbacksByFrom.get(edge.from);
|
|
564
|
+
if (bucket) bucket.push(edge);
|
|
565
|
+
else rollbacksByFrom.set(edge.from, [edge]);
|
|
566
|
+
}
|
|
567
|
+
const forwardInDegree = buildForwardInDegree(edges);
|
|
568
|
+
const forwardOutDegree = buildForwardOutDegree(edges);
|
|
569
|
+
const distinctSourceCountByTo = buildDistinctSourceCountByTo(edges);
|
|
570
|
+
const order = computeVerticalOrder(componentNodes, forwardChildren, forwardInDegree);
|
|
571
|
+
const position = /* @__PURE__ */ new Map();
|
|
572
|
+
for (let index = 0; index < order.length; index++) {
|
|
573
|
+
const node = order[index];
|
|
574
|
+
if (node !== void 0) position.set(node, index);
|
|
575
|
+
}
|
|
576
|
+
const lanes = [];
|
|
577
|
+
const rows = [];
|
|
578
|
+
const nodeColumn = /* @__PURE__ */ new Map();
|
|
579
|
+
const edgeColumn = /* @__PURE__ */ new Map();
|
|
580
|
+
const producerLaneByHash = /* @__PURE__ */ new Map();
|
|
581
|
+
const laneEdgeByIndex = /* @__PURE__ */ new Map();
|
|
582
|
+
let gridWidth = 1;
|
|
583
|
+
function ensureGridWidth(minWidth) {
|
|
584
|
+
if (minWidth > gridWidth) gridWidth = minWidth;
|
|
585
|
+
}
|
|
586
|
+
function setLane(index, want) {
|
|
587
|
+
while (lanes.length <= index) lanes.push(null);
|
|
588
|
+
lanes[index] = want;
|
|
589
|
+
if (want !== null) ensureGridWidth(index + 1);
|
|
590
|
+
}
|
|
591
|
+
function activeLaneIndices() {
|
|
592
|
+
const indices = [];
|
|
593
|
+
for (let index = 0; index < lanes.length; index++) if (lanes[index] !== null) indices.push(index);
|
|
594
|
+
return indices;
|
|
595
|
+
}
|
|
596
|
+
function passThroughExcept(lane) {
|
|
597
|
+
return activeLaneIndices().filter((index) => index !== lane);
|
|
598
|
+
}
|
|
599
|
+
function leftmostFreeLane() {
|
|
600
|
+
for (let index = 0; index < lanes.length; index++) if (lanes[index] === null) return index;
|
|
601
|
+
return lanes.length;
|
|
602
|
+
}
|
|
603
|
+
function lanesWanting(contract) {
|
|
604
|
+
const indices = [];
|
|
605
|
+
for (let index = 0; index < lanes.length; index++) if (lanes[index] === contract) indices.push(index);
|
|
606
|
+
return indices;
|
|
607
|
+
}
|
|
608
|
+
function emitMergeConnector(contractHash, laneIndices) {
|
|
609
|
+
const startLane = Math.min(...laneIndices);
|
|
610
|
+
const endLane = Math.max(...laneIndices);
|
|
611
|
+
ensureGridWidth(endLane + 1);
|
|
612
|
+
const activeLanes = new Set(activeLaneIndices());
|
|
613
|
+
const fanTargetLanes = new Set(laneIndices);
|
|
614
|
+
rows.push({
|
|
615
|
+
kind: "merge-connector",
|
|
616
|
+
contractHash,
|
|
617
|
+
startLane,
|
|
618
|
+
endLane,
|
|
619
|
+
branchCount: laneIndices.length,
|
|
620
|
+
cells: buildMergeConnectorCells(startLane, endLane, fanTargetLanes, activeLanes, gridWidth, laneEdgeByIndex)
|
|
621
|
+
});
|
|
622
|
+
for (const index of laneIndices) if (index !== startLane) setLane(index, null);
|
|
623
|
+
return startLane;
|
|
624
|
+
}
|
|
625
|
+
function emitBranchConnector(contractHash, startLane, endLane, branchCount, fanTargetLanes, fanEdgeHashByLane) {
|
|
626
|
+
ensureGridWidth(endLane + 1);
|
|
627
|
+
const activeLanes = new Set(activeLaneIndices());
|
|
628
|
+
const trunkEdgeHash = fanEdgeHashByLane.get(startLane) ?? laneEdgeByIndex.get(startLane);
|
|
629
|
+
rows.push({
|
|
630
|
+
kind: "branch-connector",
|
|
631
|
+
contractHash,
|
|
632
|
+
startLane,
|
|
633
|
+
endLane,
|
|
634
|
+
branchCount,
|
|
635
|
+
cells: buildBranchConnectorCells(startLane, endLane, new Set(fanTargetLanes), activeLanes, gridWidth, trunkEdgeHash, fanEdgeHashByLane, laneEdgeByIndex)
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
function emitEdgeRow(edge, lane, convergenceProducer) {
|
|
639
|
+
const passThrough = passThroughExcept(lane);
|
|
640
|
+
const adjacency = classifyEdgeAdjacency(edge, position);
|
|
641
|
+
ensureGridWidth(Math.max(lane, ...passThrough, 0) + 1);
|
|
642
|
+
const row = {
|
|
643
|
+
kind: "edge",
|
|
644
|
+
edge,
|
|
645
|
+
laneIndex: lane,
|
|
646
|
+
passThroughLanes: passThrough,
|
|
647
|
+
cells: buildEdgeCells(edge, lane, passThrough, adjacency, gridWidth, laneEdgeByIndex)
|
|
648
|
+
};
|
|
649
|
+
rows.push(convergenceProducer ? {
|
|
650
|
+
...row,
|
|
651
|
+
convergenceProducer: true
|
|
652
|
+
} : row);
|
|
653
|
+
edgeColumn.set(edge.migrationHash, lane);
|
|
654
|
+
if (convergenceProducer) producerLaneByHash.set(edge.migrationHash, lane);
|
|
655
|
+
laneEdgeByIndex.set(lane, edge.migrationHash);
|
|
656
|
+
}
|
|
657
|
+
function emitNodeRow(contractHash, column) {
|
|
658
|
+
ensureGridWidth(column + 1);
|
|
659
|
+
const passThrough = activeLaneIndices().filter((index) => index !== column);
|
|
660
|
+
rows.push({
|
|
661
|
+
kind: "node",
|
|
662
|
+
contractHash,
|
|
663
|
+
cells: buildNodeCells(contractHash, column, passThrough, gridWidth, laneEdgeByIndex)
|
|
664
|
+
});
|
|
665
|
+
nodeColumn.set(contractHash, column);
|
|
666
|
+
}
|
|
667
|
+
function producerGroups(node) {
|
|
668
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
669
|
+
for (const producer of producersByTo.get(node) ?? []) {
|
|
670
|
+
const group = byTarget.get(producer.from);
|
|
671
|
+
if (group) group.edges.push(producer);
|
|
672
|
+
else byTarget.set(producer.from, {
|
|
673
|
+
target: producer.from,
|
|
674
|
+
edges: [producer]
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
const groups = [...byTarget.values()];
|
|
678
|
+
groups.sort((a, b) => (position.get(a.target) ?? 0) - (position.get(b.target) ?? 0));
|
|
679
|
+
for (const group of groups) group.edges.sort((a, b) => b.dirName.localeCompare(a.dirName));
|
|
680
|
+
return groups;
|
|
681
|
+
}
|
|
682
|
+
function processNode(node) {
|
|
683
|
+
const wanting = lanesWanting(node);
|
|
684
|
+
let column;
|
|
685
|
+
if (wanting.length >= 2) column = emitMergeConnector(node, wanting);
|
|
686
|
+
else if (wanting.length === 1) column = wanting[0] ?? 0;
|
|
687
|
+
else column = leftmostFreeLane();
|
|
688
|
+
const selfEdges = [...selfByFrom.get(node) ?? []].sort((a, b) => b.dirName.localeCompare(a.dirName));
|
|
689
|
+
for (const selfEdge of selfEdges) emitEdgeRow(selfEdge, column, false);
|
|
690
|
+
emitNodeRow(node, column);
|
|
691
|
+
const rollbacks = [...rollbacksByFrom.get(node) ?? []].sort((a, b) => b.dirName.localeCompare(a.dirName));
|
|
692
|
+
const skipRollbacks = [];
|
|
693
|
+
const adjacentRollbacks = [];
|
|
694
|
+
for (const rollback of rollbacks) if (classifyEdgeAdjacency(rollback, position) === "node-skipping-rollback") skipRollbacks.push(rollback);
|
|
695
|
+
else adjacentRollbacks.push(rollback);
|
|
696
|
+
for (const rollback of skipRollbacks) emitEdgeRow(rollback, column, false);
|
|
697
|
+
const groups = producerGroups(node);
|
|
698
|
+
const isConvergence = (distinctSourceCountByTo.get(node) ?? 0) >= 2;
|
|
699
|
+
const laneForGroup = [];
|
|
700
|
+
for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
|
|
701
|
+
const group = groups[groupIndex];
|
|
702
|
+
if (group === void 0) continue;
|
|
703
|
+
const lane = groupIndex === 0 ? column : leftmostFreeLane();
|
|
704
|
+
laneForGroup[groupIndex] = lane;
|
|
705
|
+
setLane(lane, group.target);
|
|
706
|
+
}
|
|
707
|
+
if (groups.length >= 2) {
|
|
708
|
+
const endLane = Math.max(...laneForGroup);
|
|
709
|
+
const fanEdgeHashByLane = /* @__PURE__ */ new Map();
|
|
710
|
+
for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
|
|
711
|
+
const group = groups[groupIndex];
|
|
712
|
+
const lane = laneForGroup[groupIndex];
|
|
713
|
+
if (group === void 0 || lane === void 0) continue;
|
|
714
|
+
const firstEdge = group.edges[0];
|
|
715
|
+
if (firstEdge !== void 0) fanEdgeHashByLane.set(lane, firstEdge.migrationHash);
|
|
716
|
+
}
|
|
717
|
+
emitBranchConnector(node, column, endLane, groups.length, laneForGroup, fanEdgeHashByLane);
|
|
718
|
+
for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
|
|
719
|
+
const fanLane = laneForGroup[groupIndex];
|
|
720
|
+
if (fanLane === void 0) continue;
|
|
721
|
+
const fanHash = fanEdgeHashByLane.get(fanLane);
|
|
722
|
+
if (fanHash !== void 0) laneEdgeByIndex.set(fanLane, fanHash);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
|
|
726
|
+
const group = groups[groupIndex];
|
|
727
|
+
const lane = laneForGroup[groupIndex];
|
|
728
|
+
if (group === void 0 || lane === void 0) continue;
|
|
729
|
+
for (const edge of group.edges) emitEdgeRow(edge, lane, isConvergence);
|
|
730
|
+
}
|
|
731
|
+
for (const rollback of adjacentRollbacks) emitEdgeRow(rollback, column, false);
|
|
732
|
+
if (groups.length === 0) setLane(column, null);
|
|
733
|
+
}
|
|
734
|
+
for (const node of order) processNode(node);
|
|
735
|
+
return {
|
|
736
|
+
rows: applySkipRollbackRouting(refineAdjacency(rows, nodeColumn, position, forwardInDegree, forwardOutDegree, edges, producerLaneByHash), collectNodeSkippingRollbacks(edges, position), position, nodeColumn, edgeColumn),
|
|
737
|
+
nodeColumn,
|
|
738
|
+
edgeColumn
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
function buildMigrationGraphLayout(rowModel) {
|
|
742
|
+
if (rowModel.nodes.length === 0) return {
|
|
743
|
+
rows: [],
|
|
744
|
+
nodeColumn: /* @__PURE__ */ new Map(),
|
|
745
|
+
edgeColumn: /* @__PURE__ */ new Map()
|
|
746
|
+
};
|
|
747
|
+
const components = splitComponents(rowModel.nodes);
|
|
748
|
+
const allRows = [];
|
|
749
|
+
const nodeColumn = /* @__PURE__ */ new Map();
|
|
750
|
+
const edgeColumn = /* @__PURE__ */ new Map();
|
|
751
|
+
for (let componentIndex = 0; componentIndex < components.length; componentIndex++) {
|
|
752
|
+
if (componentIndex > 0) allRows.push({
|
|
753
|
+
kind: "component-separator",
|
|
754
|
+
cells: []
|
|
755
|
+
});
|
|
756
|
+
const component = components[componentIndex];
|
|
757
|
+
if (component === void 0 || component.length === 0) continue;
|
|
758
|
+
const result = layoutComponent(component, rowModel.edges);
|
|
759
|
+
allRows.push(...result.rows);
|
|
760
|
+
for (const [hash, column] of result.nodeColumn) nodeColumn.set(hash, column);
|
|
761
|
+
for (const [hash, column] of result.edgeColumn) edgeColumn.set(hash, column);
|
|
762
|
+
}
|
|
763
|
+
return {
|
|
764
|
+
rows: allRows,
|
|
765
|
+
nodeColumn,
|
|
766
|
+
edgeColumn
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
//#endregion
|
|
770
|
+
//#region src/utils/formatters/migration-list-graph-topology.ts
|
|
771
|
+
function compareDirNameDesc(a, b) {
|
|
772
|
+
return b.dirName.localeCompare(a.dirName);
|
|
773
|
+
}
|
|
774
|
+
function bumpDegree(map, key) {
|
|
775
|
+
map.set(key, (map.get(key) ?? 0) + 1);
|
|
776
|
+
}
|
|
777
|
+
function compareNodesRootFirst(a, b) {
|
|
778
|
+
if (a === EMPTY_CONTRACT_HASH) return -1;
|
|
779
|
+
if (b === EMPTY_CONTRACT_HASH) return 1;
|
|
780
|
+
return a.localeCompare(b);
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Shortest-path distance of each node from the forward roots, over the given
|
|
784
|
+
* candidate edges. Roots are the in-degree-0 nodes (baseline first, then lex);
|
|
785
|
+
* a rooted component therefore distances every node by how many forward steps
|
|
786
|
+
* it sits from a root. A component with no root (a pure cycle) is seeded from
|
|
787
|
+
* its single lexically-smallest node so the cycle still gets a stable layering.
|
|
788
|
+
*
|
|
789
|
+
* Crucially this is *shortest* path, not longest: a backward (rollback) edge
|
|
790
|
+
* `deep → shallow` never offers a shorter route to the already-shallower
|
|
791
|
+
* target, so it is inert here. Distances are thus stable whether or not the
|
|
792
|
+
* rollbacks are still in the candidate set — which is what lets the peel below
|
|
793
|
+
* tell a genuine back-edge (target strictly shallower than source) apart from a
|
|
794
|
+
* forward edge that merely happens to share the back-edge's cycle.
|
|
795
|
+
*/
|
|
796
|
+
function forwardDistances(nodes, candidates) {
|
|
797
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
798
|
+
for (const node of nodes) inDegree.set(node, 0);
|
|
799
|
+
for (const edge of candidates) bumpDegree(inDegree, edge.to);
|
|
800
|
+
const roots = [...nodes].filter((node) => (inDegree.get(node) ?? 0) === 0);
|
|
801
|
+
roots.sort(compareNodesRootFirst);
|
|
802
|
+
const seeds = roots.length > 0 ? roots : [...nodes].sort(compareNodesRootFirst).slice(0, 1);
|
|
803
|
+
const dist = /* @__PURE__ */ new Map();
|
|
804
|
+
for (const seed of seeds) dist.set(seed, 0);
|
|
805
|
+
const maxPasses = nodes.size;
|
|
806
|
+
for (let pass = 0; pass < maxPasses; pass++) {
|
|
807
|
+
let changed = false;
|
|
808
|
+
for (const edge of candidates) {
|
|
809
|
+
const base = dist.get(edge.from);
|
|
810
|
+
if (base === void 0) continue;
|
|
811
|
+
const next = base + 1;
|
|
812
|
+
if (next < (dist.get(edge.to) ?? Number.POSITIVE_INFINITY)) {
|
|
813
|
+
dist.set(edge.to, next);
|
|
814
|
+
changed = true;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (!changed) break;
|
|
818
|
+
}
|
|
819
|
+
for (const node of nodes) if (!dist.has(node)) dist.set(node, 0);
|
|
820
|
+
return dist;
|
|
821
|
+
}
|
|
822
|
+
function canReachForward(start, goal, candidates) {
|
|
823
|
+
if (start === goal) return true;
|
|
824
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
825
|
+
for (const edge of candidates) {
|
|
826
|
+
const bucket = outgoing.get(edge.from);
|
|
827
|
+
if (bucket) bucket.push(edge.to);
|
|
828
|
+
else outgoing.set(edge.from, [edge.to]);
|
|
829
|
+
}
|
|
830
|
+
const visited = new Set([start]);
|
|
831
|
+
const queue = [start];
|
|
832
|
+
while (queue.length > 0) {
|
|
833
|
+
const node = queue.shift();
|
|
834
|
+
if (node === void 0) continue;
|
|
835
|
+
for (const next of outgoing.get(node) ?? []) {
|
|
836
|
+
if (next === goal) return true;
|
|
837
|
+
if (!visited.has(next)) {
|
|
838
|
+
visited.add(next);
|
|
839
|
+
queue.push(next);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return false;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Demote node-skipping rollbacks left forward by the DFS. An edge `from → to`
|
|
847
|
+
* is a rollback exactly when both hold:
|
|
848
|
+
* 1. `to` is a forward-ancestor of `from` — `to` can still reach `from` over
|
|
849
|
+
* the other forward edges, so the edge closes a cycle; and
|
|
850
|
+
* 2. `to` is strictly shallower than `from` (smaller forward distance) — the
|
|
851
|
+
* edge points back toward the root rather than advancing history.
|
|
852
|
+
*
|
|
853
|
+
* Condition 2 is the discriminator: in a cycle created by a rollback every edge
|
|
854
|
+
* satisfies condition 1, but only the rollback itself runs deep → shallow. The
|
|
855
|
+
* forward chain edges run shallow → deep and are never peeled, however many
|
|
856
|
+
* rollbacks converge on the same target. Tight back-edges whose source and
|
|
857
|
+
* target sit at the same distance (mutual two-node cycles) are already resolved
|
|
858
|
+
* by the DFS immediate-parent rule, so they never reach this pass. One edge is
|
|
859
|
+
* peeled per iteration (dirName-descending tie-break) and distances/reachability
|
|
860
|
+
* are recomputed, making the outcome independent of edge input order.
|
|
861
|
+
*/
|
|
862
|
+
function peelNodeSkippingRollbacks(nodes, kindByMigrationHash, nonSelf) {
|
|
863
|
+
let candidates = nonSelf.filter((edge) => kindByMigrationHash.get(edge.hash) === "forward");
|
|
864
|
+
while (candidates.length > 0) {
|
|
865
|
+
const dist = forwardDistances(nodes, candidates);
|
|
866
|
+
const backEdges = candidates.filter((edge) => {
|
|
867
|
+
if ((dist.get(edge.to) ?? 0) >= (dist.get(edge.from) ?? 0)) return false;
|
|
868
|
+
const without = candidates.filter((candidate) => candidate !== edge);
|
|
869
|
+
return canReachForward(edge.to, edge.from, without);
|
|
870
|
+
});
|
|
871
|
+
if (backEdges.length === 0) break;
|
|
872
|
+
backEdges.sort(compareDirNameDesc);
|
|
873
|
+
const rollback = backEdges[0];
|
|
874
|
+
if (rollback === void 0) break;
|
|
875
|
+
kindByMigrationHash.set(rollback.hash, "rollback");
|
|
876
|
+
candidates = candidates.filter((edge) => edge !== rollback);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* DFS with dirName-descending traversal. A GRAY target is a rollback only when it
|
|
881
|
+
* is the immediate DFS parent of the source — cross-links to other GRAY nodes
|
|
882
|
+
* stay forward. A follow-up peel pass demotes node-skipping rollbacks (target is
|
|
883
|
+
* a forward-ancestor of the source and sits strictly shallower than it).
|
|
884
|
+
*/
|
|
885
|
+
function classifyNormalizedEdges(edges) {
|
|
886
|
+
const nodes = /* @__PURE__ */ new Set();
|
|
887
|
+
const kindByMigrationHash = /* @__PURE__ */ new Map();
|
|
888
|
+
const outgoingByFrom = /* @__PURE__ */ new Map();
|
|
889
|
+
const nonSelf = [];
|
|
890
|
+
for (const edge of edges) {
|
|
891
|
+
nodes.add(edge.from);
|
|
892
|
+
nodes.add(edge.to);
|
|
893
|
+
if (edge.from === edge.to) {
|
|
894
|
+
kindByMigrationHash.set(edge.hash, "self");
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
nonSelf.push(edge);
|
|
898
|
+
const bucket = outgoingByFrom.get(edge.from);
|
|
899
|
+
if (bucket) bucket.push(edge);
|
|
900
|
+
else outgoingByFrom.set(edge.from, [edge]);
|
|
901
|
+
}
|
|
902
|
+
for (const bucket of outgoingByFrom.values()) bucket.sort(compareDirNameDesc);
|
|
903
|
+
const nonSelfInDegree = /* @__PURE__ */ new Map();
|
|
904
|
+
for (const node of nodes) nonSelfInDegree.set(node, 0);
|
|
905
|
+
for (const bucket of outgoingByFrom.values()) for (const edge of bucket) bumpDegree(nonSelfInDegree, edge.to);
|
|
906
|
+
const dfsRoots = [];
|
|
907
|
+
for (const node of nodes) if ((nonSelfInDegree.get(node) ?? 0) === 0) dfsRoots.push(node);
|
|
908
|
+
dfsRoots.sort((a, b) => {
|
|
909
|
+
if (a === EMPTY_CONTRACT_HASH) return -1;
|
|
910
|
+
if (b === EMPTY_CONTRACT_HASH) return 1;
|
|
911
|
+
return a.localeCompare(b);
|
|
912
|
+
});
|
|
913
|
+
if (dfsRoots.length === 0) dfsRoots.push(...[...nodes].sort((a, b) => a.localeCompare(b)));
|
|
914
|
+
const WHITE = 0;
|
|
915
|
+
const GRAY = 1;
|
|
916
|
+
const BLACK = 2;
|
|
917
|
+
const color = /* @__PURE__ */ new Map();
|
|
918
|
+
const dfsParent = /* @__PURE__ */ new Map();
|
|
919
|
+
for (const node of nodes) color.set(node, WHITE);
|
|
920
|
+
const stack = [];
|
|
921
|
+
function isImmediateDfsParent(ancestor, node) {
|
|
922
|
+
return dfsParent.get(node) === ancestor;
|
|
923
|
+
}
|
|
924
|
+
function pushFrame(node, parent) {
|
|
925
|
+
color.set(node, GRAY);
|
|
926
|
+
dfsParent.set(node, parent);
|
|
927
|
+
stack.push({
|
|
928
|
+
node,
|
|
929
|
+
outgoing: outgoingByFrom.get(node) ?? [],
|
|
930
|
+
index: 0
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
function runDfsFrom(root) {
|
|
934
|
+
if (color.get(root) !== WHITE) return;
|
|
935
|
+
pushFrame(root, void 0);
|
|
936
|
+
while (stack.length > 0) {
|
|
937
|
+
const frame = stack[stack.length - 1];
|
|
938
|
+
if (frame === void 0) break;
|
|
939
|
+
if (frame.index >= frame.outgoing.length) {
|
|
940
|
+
color.set(frame.node, BLACK);
|
|
941
|
+
stack.pop();
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
const edge = frame.outgoing[frame.index];
|
|
945
|
+
frame.index += 1;
|
|
946
|
+
if (edge === void 0) continue;
|
|
947
|
+
const v = edge.to;
|
|
948
|
+
const vColor = color.get(v);
|
|
949
|
+
if (vColor === GRAY && isImmediateDfsParent(v, frame.node)) kindByMigrationHash.set(edge.hash, "rollback");
|
|
950
|
+
else {
|
|
951
|
+
kindByMigrationHash.set(edge.hash, "forward");
|
|
952
|
+
if (vColor === WHITE) pushFrame(v, frame.node);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
for (const root of dfsRoots) runDfsFrom(root);
|
|
957
|
+
const remainingWhite = [...nodes].filter((node) => color.get(node) === WHITE);
|
|
958
|
+
remainingWhite.sort((a, b) => a.localeCompare(b));
|
|
959
|
+
for (const root of remainingWhite) runDfsFrom(root);
|
|
960
|
+
peelNodeSkippingRollbacks(nodes, kindByMigrationHash, nonSelf);
|
|
961
|
+
const forwardInDegree = /* @__PURE__ */ new Map();
|
|
962
|
+
const forwardOutDegree = /* @__PURE__ */ new Map();
|
|
963
|
+
for (const edge of edges) {
|
|
964
|
+
if (kindByMigrationHash.get(edge.hash) !== "forward") continue;
|
|
965
|
+
bumpDegree(forwardOutDegree, edge.from);
|
|
966
|
+
bumpDegree(forwardInDegree, edge.to);
|
|
967
|
+
}
|
|
968
|
+
return {
|
|
969
|
+
kindByMigrationHash,
|
|
970
|
+
forwardInDegree,
|
|
971
|
+
forwardOutDegree
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Classify forward/rollback/self for a `MigrationGraph` edge set (Tier-3).
|
|
976
|
+
*/
|
|
977
|
+
function classifyMigrationGraphTopology(graph) {
|
|
978
|
+
const normalized = [];
|
|
979
|
+
for (const edges of graph.forwardChain.values()) for (const edge of edges) normalized.push({
|
|
980
|
+
hash: edge.migrationHash,
|
|
981
|
+
from: edge.from,
|
|
982
|
+
to: edge.to,
|
|
983
|
+
dirName: edge.dirName
|
|
984
|
+
});
|
|
985
|
+
return classifyNormalizedEdges(normalized);
|
|
986
|
+
}
|
|
987
|
+
//#endregion
|
|
988
|
+
//#region src/utils/formatters/migration-graph-rows.ts
|
|
989
|
+
/**
|
|
990
|
+
* Return the weakly-connected components of `graph` as an array of node sets,
|
|
991
|
+
* ordered so the component containing EMPTY_CONTRACT_HASH comes first (if
|
|
992
|
+
* present), with remaining components sorted by their lex-smallest node hash.
|
|
993
|
+
*/
|
|
994
|
+
function weaklyConnectedComponents(graph) {
|
|
995
|
+
const visited = /* @__PURE__ */ new Set();
|
|
996
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
997
|
+
function addAdjacent(a, b) {
|
|
998
|
+
const aList = adjacency.get(a);
|
|
999
|
+
if (aList) aList.push(b);
|
|
1000
|
+
else adjacency.set(a, [b]);
|
|
1001
|
+
const bList = adjacency.get(b);
|
|
1002
|
+
if (bList) bList.push(a);
|
|
1003
|
+
else adjacency.set(b, [a]);
|
|
1004
|
+
}
|
|
1005
|
+
for (const edges of graph.forwardChain.values()) for (const edge of edges) if (edge.from !== edge.to) addAdjacent(edge.from, edge.to);
|
|
1006
|
+
for (const node of graph.nodes) if (!adjacency.has(node)) adjacency.set(node, []);
|
|
1007
|
+
const components = [];
|
|
1008
|
+
function bfsComponent(start) {
|
|
1009
|
+
const component = /* @__PURE__ */ new Set();
|
|
1010
|
+
const queue = [start];
|
|
1011
|
+
while (queue.length > 0) {
|
|
1012
|
+
const node = queue.shift();
|
|
1013
|
+
if (node === void 0 || visited.has(node)) continue;
|
|
1014
|
+
visited.add(node);
|
|
1015
|
+
component.add(node);
|
|
1016
|
+
for (const neighbor of adjacency.get(node) ?? []) if (!visited.has(neighbor)) queue.push(neighbor);
|
|
1017
|
+
}
|
|
1018
|
+
return component;
|
|
1019
|
+
}
|
|
1020
|
+
const allNodes = [...graph.nodes].sort((a, b) => {
|
|
1021
|
+
if (a === EMPTY_CONTRACT_HASH) return -1;
|
|
1022
|
+
if (b === EMPTY_CONTRACT_HASH) return 1;
|
|
1023
|
+
return a.localeCompare(b);
|
|
1024
|
+
});
|
|
1025
|
+
for (const node of allNodes) if (!visited.has(node)) components.push(bfsComponent(node));
|
|
1026
|
+
components.sort((a, b) => {
|
|
1027
|
+
const aHasEmpty = a.has(EMPTY_CONTRACT_HASH);
|
|
1028
|
+
const bHasEmpty = b.has(EMPTY_CONTRACT_HASH);
|
|
1029
|
+
if (aHasEmpty && !bHasEmpty) return -1;
|
|
1030
|
+
if (!aHasEmpty && bHasEmpty) return 1;
|
|
1031
|
+
const aMin = [...a].sort((x, y) => x.localeCompare(y))[0] ?? "";
|
|
1032
|
+
const bMin = [...b].sort((x, y) => x.localeCompare(y))[0] ?? "";
|
|
1033
|
+
return aMin.localeCompare(bMin);
|
|
1034
|
+
});
|
|
1035
|
+
return components;
|
|
1036
|
+
}
|
|
1037
|
+
function forwardRootsInComponent(componentNodes, topology) {
|
|
1038
|
+
const roots = [];
|
|
1039
|
+
for (const node of componentNodes) if ((topology.forwardInDegree.get(node) ?? 0) === 0) roots.push(node);
|
|
1040
|
+
roots.sort((a, b) => {
|
|
1041
|
+
if (a === EMPTY_CONTRACT_HASH) return -1;
|
|
1042
|
+
if (b === EMPTY_CONTRACT_HASH) return 1;
|
|
1043
|
+
return a.localeCompare(b);
|
|
1044
|
+
});
|
|
1045
|
+
if (roots.length > 0) return roots;
|
|
1046
|
+
return [...componentNodes].sort((a, b) => {
|
|
1047
|
+
if (a === EMPTY_CONTRACT_HASH) return -1;
|
|
1048
|
+
if (b === EMPTY_CONTRACT_HASH) return 1;
|
|
1049
|
+
return a.localeCompare(b);
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
function compareNodesTipsFirst(a, b, rank) {
|
|
1053
|
+
const rankA = rank.get(a) ?? 0;
|
|
1054
|
+
const rankB = rank.get(b) ?? 0;
|
|
1055
|
+
if (rankA !== rankB) return rankB - rankA;
|
|
1056
|
+
if (a === EMPTY_CONTRACT_HASH) return 1;
|
|
1057
|
+
if (b === EMPTY_CONTRACT_HASH) return -1;
|
|
1058
|
+
return a.localeCompare(b);
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Layer nodes by longest forward-path rank from forward roots within the
|
|
1062
|
+
* component. Rank 0 is the root (bottom row); the maximum rank is the tip
|
|
1063
|
+
* (top row). Emits rank-descending with lex-ascending tie-break among siblings
|
|
1064
|
+
* at the same rank — stable across edge-insertion order and correct under
|
|
1065
|
+
* diamonds, cross-links, and rollbacks.
|
|
1066
|
+
*/
|
|
1067
|
+
function maxRank(rank) {
|
|
1068
|
+
let max = 0;
|
|
1069
|
+
for (const value of rank.values()) if (value > max) max = value;
|
|
1070
|
+
return max;
|
|
1071
|
+
}
|
|
1072
|
+
function layerNodesByLongestForwardPath(componentNodes, topology, graph, contractHash) {
|
|
1073
|
+
const forwardOut = /* @__PURE__ */ new Map();
|
|
1074
|
+
for (const node of componentNodes) forwardOut.set(node, []);
|
|
1075
|
+
for (const edges of graph.forwardChain.values()) for (const edge of edges) {
|
|
1076
|
+
if (!componentNodes.has(edge.from) || !componentNodes.has(edge.to)) continue;
|
|
1077
|
+
if (edge.from === edge.to) continue;
|
|
1078
|
+
if (topology.kindByMigrationHash.get(edge.migrationHash) !== "forward") continue;
|
|
1079
|
+
const bucket = forwardOut.get(edge.from);
|
|
1080
|
+
if (bucket) bucket.push(edge.to);
|
|
1081
|
+
}
|
|
1082
|
+
const roots = forwardRootsInComponent(componentNodes, topology);
|
|
1083
|
+
const rank = /* @__PURE__ */ new Map();
|
|
1084
|
+
for (const root of roots) rank.set(root, 0);
|
|
1085
|
+
const maxPasses = componentNodes.size;
|
|
1086
|
+
for (let pass = 0; pass < maxPasses; pass++) {
|
|
1087
|
+
let changed = false;
|
|
1088
|
+
for (const node of componentNodes) {
|
|
1089
|
+
const base = rank.get(node);
|
|
1090
|
+
if (base === void 0) continue;
|
|
1091
|
+
for (const to of forwardOut.get(node) ?? []) {
|
|
1092
|
+
const next = base + 1;
|
|
1093
|
+
if (next > (rank.get(to) ?? -1)) {
|
|
1094
|
+
rank.set(to, next);
|
|
1095
|
+
changed = true;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
if (!changed) break;
|
|
1100
|
+
}
|
|
1101
|
+
for (const node of componentNodes) if (!rank.has(node)) rank.set(node, 0);
|
|
1102
|
+
if (contractHash !== void 0 && contractHash !== EMPTY_CONTRACT_HASH && componentNodes.has(contractHash) && (forwardOut.get(contractHash) ?? []).length === 0) rank.set(contractHash, maxRank(rank) + 1);
|
|
1103
|
+
return [...componentNodes].sort((a, b) => compareNodesTipsFirst(a, b, rank));
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Build the row model from a tolerant `MigrationGraph`.
|
|
1107
|
+
*
|
|
1108
|
+
* The row model is the first pure-data stage of the `migration graph` render
|
|
1109
|
+
* pipeline. It:
|
|
1110
|
+
* - classifies every edge as `forward`, `rollback`, or `self`;
|
|
1111
|
+
* - produces a deterministic vertical node ordering (tips at index 0, roots
|
|
1112
|
+
* at the end) within each weakly-connected component;
|
|
1113
|
+
* - separates disjoint components with `null` sentinels;
|
|
1114
|
+
* - optionally prepends a detached current contract as its own single-node
|
|
1115
|
+
* component when `contractHash` is not already in the graph.
|
|
1116
|
+
*
|
|
1117
|
+
* No columns, no lane allocation, no glyphs, no rendering.
|
|
1118
|
+
*/
|
|
1119
|
+
/**
|
|
1120
|
+
* Resolve the detached current contract, if any: a real contract (not the
|
|
1121
|
+
* empty baseline) that no migration on disk produces, so it is absent from
|
|
1122
|
+
* the graph. Such a contract renders as a floating node rather than
|
|
1123
|
+
* decorating an existing one. Returns the hash when detached, else undefined.
|
|
1124
|
+
*/
|
|
1125
|
+
function detachedContractHash(graph, contractHash) {
|
|
1126
|
+
return contractHash !== void 0 && contractHash !== EMPTY_CONTRACT_HASH && !graph.nodes.has(contractHash) ? contractHash : void 0;
|
|
1127
|
+
}
|
|
1128
|
+
function isForwardLeaf(node, edges) {
|
|
1129
|
+
return !edges.some((e) => e.kind === "forward" && e.from === node && e.from !== e.to);
|
|
1130
|
+
}
|
|
1131
|
+
function forwardReachableFrom(start, forwardTo) {
|
|
1132
|
+
const reachable = new Set([start]);
|
|
1133
|
+
const queue = [start];
|
|
1134
|
+
while (queue.length > 0) {
|
|
1135
|
+
const node = queue.shift();
|
|
1136
|
+
if (node === void 0) continue;
|
|
1137
|
+
for (const next of forwardTo.get(node) ?? []) if (!reachable.has(next)) {
|
|
1138
|
+
reachable.add(next);
|
|
1139
|
+
queue.push(next);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return reachable;
|
|
1143
|
+
}
|
|
1144
|
+
function buildForwardToMap(edges) {
|
|
1145
|
+
const forwardTo = /* @__PURE__ */ new Map();
|
|
1146
|
+
for (const edge of edges) {
|
|
1147
|
+
if (edge.kind !== "forward" || edge.from === edge.to) continue;
|
|
1148
|
+
const bucket = forwardTo.get(edge.from);
|
|
1149
|
+
if (bucket) bucket.push(edge.to);
|
|
1150
|
+
else forwardTo.set(edge.from, [edge.to]);
|
|
1151
|
+
}
|
|
1152
|
+
return forwardTo;
|
|
1153
|
+
}
|
|
1154
|
+
function sortEdgesForContractHashTrunk(edges, contractHash) {
|
|
1155
|
+
if (contractHash === void 0 || contractHash === EMPTY_CONTRACT_HASH || !isForwardLeaf(contractHash, edges)) return edges;
|
|
1156
|
+
const preferredLeaf = contractHash;
|
|
1157
|
+
const forwardTo = buildForwardToMap(edges);
|
|
1158
|
+
const reachability = /* @__PURE__ */ new Map();
|
|
1159
|
+
function canReachContractHash(from) {
|
|
1160
|
+
let cached = reachability.get(from);
|
|
1161
|
+
if (cached === void 0) {
|
|
1162
|
+
cached = forwardReachableFrom(from, forwardTo);
|
|
1163
|
+
reachability.set(from, cached);
|
|
1164
|
+
}
|
|
1165
|
+
return cached.has(preferredLeaf);
|
|
1166
|
+
}
|
|
1167
|
+
function trunkBias(edge) {
|
|
1168
|
+
if (edge.kind !== "forward" || edge.from === edge.to) return 0;
|
|
1169
|
+
if (edge.to === preferredLeaf) return 2;
|
|
1170
|
+
if (canReachContractHash(edge.to)) return 1;
|
|
1171
|
+
return 0;
|
|
1172
|
+
}
|
|
1173
|
+
return edges.map((edge, index) => ({
|
|
1174
|
+
edge,
|
|
1175
|
+
index,
|
|
1176
|
+
bias: trunkBias(edge)
|
|
1177
|
+
})).sort((a, b) => {
|
|
1178
|
+
if (a.edge.from !== b.edge.from) return a.index - b.index;
|
|
1179
|
+
if (a.bias !== b.bias) return b.bias - a.bias;
|
|
1180
|
+
return a.index - b.index;
|
|
1181
|
+
}).map(({ edge }) => edge);
|
|
1182
|
+
}
|
|
1183
|
+
function rebuildEdgeLookupMaps(edges) {
|
|
1184
|
+
const edgesByFrom = /* @__PURE__ */ new Map();
|
|
1185
|
+
const edgesByTo = /* @__PURE__ */ new Map();
|
|
1186
|
+
for (const classified of edges) {
|
|
1187
|
+
const fromBucket = edgesByFrom.get(classified.from);
|
|
1188
|
+
if (fromBucket) fromBucket.push(classified);
|
|
1189
|
+
else edgesByFrom.set(classified.from, [classified]);
|
|
1190
|
+
const toBucket = edgesByTo.get(classified.to);
|
|
1191
|
+
if (toBucket) toBucket.push(classified);
|
|
1192
|
+
else edgesByTo.set(classified.to, [classified]);
|
|
1193
|
+
}
|
|
1194
|
+
return {
|
|
1195
|
+
edgesByFrom,
|
|
1196
|
+
edgesByTo
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
function buildMigrationGraphRows(graph, options = {}) {
|
|
1200
|
+
const emptyModel = {
|
|
1201
|
+
nodes: [],
|
|
1202
|
+
edges: [],
|
|
1203
|
+
edgesByFrom: /* @__PURE__ */ new Map(),
|
|
1204
|
+
edgesByTo: /* @__PURE__ */ new Map()
|
|
1205
|
+
};
|
|
1206
|
+
if (graph.nodes.size === 0) {
|
|
1207
|
+
const detached = detachedContractHash(graph, options.contractHash);
|
|
1208
|
+
return detached !== void 0 ? {
|
|
1209
|
+
...emptyModel,
|
|
1210
|
+
nodes: [detached]
|
|
1211
|
+
} : emptyModel;
|
|
1212
|
+
}
|
|
1213
|
+
const topology = classifyMigrationGraphTopology(graph);
|
|
1214
|
+
const edges = [];
|
|
1215
|
+
for (const edgeList of graph.forwardChain.values()) for (const edge of edgeList) {
|
|
1216
|
+
const kind = topology.kindByMigrationHash.get(edge.migrationHash) ?? "forward";
|
|
1217
|
+
edges.push({
|
|
1218
|
+
migrationHash: edge.migrationHash,
|
|
1219
|
+
from: edge.from,
|
|
1220
|
+
to: edge.to,
|
|
1221
|
+
dirName: edge.dirName,
|
|
1222
|
+
kind
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
const sortedEdges = sortEdgesForContractHashTrunk(edges, options.contractHash);
|
|
1226
|
+
const { edgesByFrom, edgesByTo } = rebuildEdgeLookupMaps(sortedEdges);
|
|
1227
|
+
const components = weaklyConnectedComponents(graph);
|
|
1228
|
+
const nodes = [];
|
|
1229
|
+
for (let i = 0; i < components.length; i++) {
|
|
1230
|
+
if (i > 0) nodes.push(null);
|
|
1231
|
+
const component = components[i];
|
|
1232
|
+
if (component === void 0) continue;
|
|
1233
|
+
const ordered = layerNodesByLongestForwardPath(component, topology, graph, options.contractHash);
|
|
1234
|
+
for (const node of ordered) nodes.push(node);
|
|
1235
|
+
}
|
|
1236
|
+
const detached = detachedContractHash(graph, options.contractHash);
|
|
1237
|
+
if (detached !== void 0) {
|
|
1238
|
+
if (nodes.length > 0) nodes.unshift(null);
|
|
1239
|
+
nodes.unshift(detached);
|
|
1240
|
+
}
|
|
1241
|
+
return {
|
|
1242
|
+
nodes,
|
|
1243
|
+
edges: sortedEdges,
|
|
1244
|
+
edgesByFrom,
|
|
1245
|
+
edgesByTo
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
//#endregion
|
|
1249
|
+
//#region src/utils/formatters/migration-graph-lane-colors.ts
|
|
1250
|
+
const { magenta: magenta$1, cyan: cyan$1, green: green$1, yellow: yellow$1, blueBright, red: red$1 } = createColors({ useColor: true });
|
|
1251
|
+
const LANE_COLOR_CYCLE = [
|
|
1252
|
+
magenta$1,
|
|
1253
|
+
cyan$1,
|
|
1254
|
+
green$1,
|
|
1255
|
+
yellow$1,
|
|
1256
|
+
blueBright,
|
|
1257
|
+
red$1
|
|
1258
|
+
];
|
|
1259
|
+
/**
|
|
1260
|
+
* The hue for a gutter column. The leftmost lane (column 0) is **neutral** — it
|
|
1261
|
+
* has nothing to be told apart from in the common single-lane linear case, so
|
|
1262
|
+
* the renderer dims it rather than tinting it; the rotating palette is reserved
|
|
1263
|
+
* for columns ≥ 1 (where a second lane exists to distinguish). Callers must dim
|
|
1264
|
+
* column 0 themselves; this returns identity for it as a guard. A lane freed and
|
|
1265
|
+
* reused by a later branch keeps its column's hue — coloring is by position, not
|
|
1266
|
+
* branch identity, exactly like `git log --graph`.
|
|
1267
|
+
*/
|
|
1268
|
+
function laneColorForColumn(column) {
|
|
1269
|
+
if (column <= 0) return (text) => text;
|
|
1270
|
+
return LANE_COLOR_CYCLE[(column - 1) % LANE_COLOR_CYCLE.length] ?? ((text) => text);
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Style a structural glyph by its resolved colour column. Column 0 and the
|
|
1274
|
+
* neutral sentinel render dim (`dimLane`); columns ≥ 1 take a palette hue.
|
|
1275
|
+
*/
|
|
1276
|
+
function stylerForLaneColumn(colorColumn, colorize, dimLane) {
|
|
1277
|
+
if (!colorize || colorColumn <= 0) return dimLane;
|
|
1278
|
+
return laneColorForColumn(colorColumn);
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Resolve per-cell colour columns for a node/arc row. Scanning right-to-left
|
|
1282
|
+
* lets each arc segment inherit the hue of the arc it leads into.
|
|
1283
|
+
*/
|
|
1284
|
+
function resolveRowArcLaneColors(cells) {
|
|
1285
|
+
const lane = new Array(cells.length);
|
|
1286
|
+
const connector = new Array(cells.length);
|
|
1287
|
+
const dash = new Array(cells.length);
|
|
1288
|
+
let arcCorner = 0;
|
|
1289
|
+
let landingAnchor = 0;
|
|
1290
|
+
for (let column = cells.length - 1; column >= 0; column--) {
|
|
1291
|
+
const cell = cells[column];
|
|
1292
|
+
connector[column] = landingAnchor !== 0 ? landingAnchor : arcCorner;
|
|
1293
|
+
switch (cell?.kind) {
|
|
1294
|
+
case "arc-branch-corner":
|
|
1295
|
+
arcCorner = column;
|
|
1296
|
+
lane[column] = column;
|
|
1297
|
+
dash[column] = column;
|
|
1298
|
+
break;
|
|
1299
|
+
case "arc-land-corner":
|
|
1300
|
+
arcCorner = column;
|
|
1301
|
+
landingAnchor = column;
|
|
1302
|
+
lane[column] = column;
|
|
1303
|
+
dash[column] = column;
|
|
1304
|
+
break;
|
|
1305
|
+
case "arc-branch-tee":
|
|
1306
|
+
lane[column] = column;
|
|
1307
|
+
dash[column] = column;
|
|
1308
|
+
break;
|
|
1309
|
+
case "arc-land-tee":
|
|
1310
|
+
lane[column] = column;
|
|
1311
|
+
dash[column] = landingAnchor === 0 ? column : landingAnchor;
|
|
1312
|
+
landingAnchor = column;
|
|
1313
|
+
break;
|
|
1314
|
+
case "arc-crossing":
|
|
1315
|
+
case "arc-land-bridge": {
|
|
1316
|
+
const served = landingAnchor !== 0 ? landingAnchor : arcCorner;
|
|
1317
|
+
lane[column] = served;
|
|
1318
|
+
dash[column] = served;
|
|
1319
|
+
break;
|
|
1320
|
+
}
|
|
1321
|
+
case "horizontal-pass":
|
|
1322
|
+
lane[column] = arcCorner === 0 ? column : arcCorner;
|
|
1323
|
+
dash[column] = lane[column] ?? column;
|
|
1324
|
+
break;
|
|
1325
|
+
case "node":
|
|
1326
|
+
lane[column] = column;
|
|
1327
|
+
dash[column] = column;
|
|
1328
|
+
arcCorner = 0;
|
|
1329
|
+
landingAnchor = 0;
|
|
1330
|
+
break;
|
|
1331
|
+
default:
|
|
1332
|
+
lane[column] = column;
|
|
1333
|
+
dash[column] = column;
|
|
1334
|
+
arcCorner = 0;
|
|
1335
|
+
landingAnchor = 0;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return {
|
|
1339
|
+
lane,
|
|
1340
|
+
connector,
|
|
1341
|
+
dash
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Resolve per-cell connector colours. Scanning right-to-left, a corner or an
|
|
1346
|
+
* intermediate tee anchors its own lane, but a tee's trailing dash leads into
|
|
1347
|
+
* the branch on its right.
|
|
1348
|
+
*/
|
|
1349
|
+
function resolveConnectorLaneColors(cells, startLane) {
|
|
1350
|
+
const glyph = new Array(cells.length);
|
|
1351
|
+
const dash = new Array(cells.length);
|
|
1352
|
+
let owner = 0;
|
|
1353
|
+
for (let column = cells.length - 1; column >= 0; column--) switch (cells[column]?.kind) {
|
|
1354
|
+
case "branch-corner":
|
|
1355
|
+
case "merge-corner":
|
|
1356
|
+
owner = column;
|
|
1357
|
+
glyph[column] = column;
|
|
1358
|
+
dash[column] = column;
|
|
1359
|
+
break;
|
|
1360
|
+
case "branch-tee":
|
|
1361
|
+
case "merge-tee":
|
|
1362
|
+
if (column === startLane) {
|
|
1363
|
+
const served = owner === 0 ? column : owner;
|
|
1364
|
+
glyph[column] = column;
|
|
1365
|
+
dash[column] = served;
|
|
1366
|
+
} else {
|
|
1367
|
+
dash[column] = owner === 0 ? column : owner;
|
|
1368
|
+
glyph[column] = column;
|
|
1369
|
+
owner = column;
|
|
1370
|
+
}
|
|
1371
|
+
break;
|
|
1372
|
+
case "arc-crossing":
|
|
1373
|
+
glyph[column] = column;
|
|
1374
|
+
dash[column] = owner === 0 ? column : owner;
|
|
1375
|
+
owner = column;
|
|
1376
|
+
break;
|
|
1377
|
+
case "horizontal-pass": {
|
|
1378
|
+
const served = owner === 0 ? column : owner;
|
|
1379
|
+
glyph[column] = served;
|
|
1380
|
+
dash[column] = served;
|
|
1381
|
+
break;
|
|
1382
|
+
}
|
|
1383
|
+
default:
|
|
1384
|
+
glyph[column] = column;
|
|
1385
|
+
dash[column] = column;
|
|
1386
|
+
}
|
|
1387
|
+
return {
|
|
1388
|
+
glyph,
|
|
1389
|
+
dash
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
function migrationListForwardArrow(glyphMode) {
|
|
1393
|
+
return glyphMode === "ascii" ? "->" : "→";
|
|
1394
|
+
}
|
|
1395
|
+
function migrationListEmptySource(glyphMode) {
|
|
1396
|
+
return glyphMode === "ascii" ? "-" : "∅";
|
|
1397
|
+
}
|
|
1398
|
+
function abbreviateContractHash(hash) {
|
|
1399
|
+
return (hash.startsWith("sha256:") ? hash.slice(7) : hash).slice(0, 7);
|
|
1400
|
+
}
|
|
1401
|
+
function padFromHashColumn(text, width) {
|
|
1402
|
+
const padding = Math.max(0, width - stringWidth(text));
|
|
1403
|
+
return `${" ".repeat(padding)}${text}`;
|
|
1404
|
+
}
|
|
1405
|
+
//#endregion
|
|
1406
|
+
//#region src/utils/formatters/migration-list-render.ts
|
|
1407
|
+
const IDENTITY_MIGRATION_LIST_STYLER = {
|
|
1408
|
+
kind: (text) => text,
|
|
1409
|
+
dirName: (text) => text,
|
|
1410
|
+
sourceHash: (text) => text,
|
|
1411
|
+
destHash: (text) => text,
|
|
1412
|
+
glyph: (text) => text,
|
|
1413
|
+
lane: (text) => text,
|
|
1414
|
+
invariants: (ids) => `{${ids.join(", ")}}`,
|
|
1415
|
+
refs: (names) => `(${names.join(", ")})`,
|
|
1416
|
+
spaceHeading: (text) => text,
|
|
1417
|
+
summary: (text) => text,
|
|
1418
|
+
emptyState: (text) => text
|
|
1419
|
+
};
|
|
1420
|
+
function canonicalFrom(from) {
|
|
1421
|
+
return from ?? EMPTY_CONTRACT_HASH;
|
|
1422
|
+
}
|
|
1423
|
+
function migrationGraphFromListEntries(entries) {
|
|
1424
|
+
const nodes = /* @__PURE__ */ new Set();
|
|
1425
|
+
const forwardChain = /* @__PURE__ */ new Map();
|
|
1426
|
+
const reverseChain = /* @__PURE__ */ new Map();
|
|
1427
|
+
const migrationByHash = /* @__PURE__ */ new Map();
|
|
1428
|
+
for (const entry of entries) {
|
|
1429
|
+
const from = canonicalFrom(entry.fromContract);
|
|
1430
|
+
const edge = {
|
|
1431
|
+
from,
|
|
1432
|
+
to: entry.toContract,
|
|
1433
|
+
migrationHash: entry.hash,
|
|
1434
|
+
dirName: entry.name,
|
|
1435
|
+
createdAt: entry.createdAt,
|
|
1436
|
+
invariants: entry.providedInvariants
|
|
1437
|
+
};
|
|
1438
|
+
nodes.add(from);
|
|
1439
|
+
nodes.add(entry.toContract);
|
|
1440
|
+
const forward = forwardChain.get(from);
|
|
1441
|
+
if (forward) forward.push(edge);
|
|
1442
|
+
else forwardChain.set(from, [edge]);
|
|
1443
|
+
const reverse = reverseChain.get(entry.toContract);
|
|
1444
|
+
if (reverse) reverse.push(edge);
|
|
1445
|
+
else reverseChain.set(entry.toContract, [edge]);
|
|
1446
|
+
migrationByHash.set(entry.hash, edge);
|
|
1447
|
+
}
|
|
1448
|
+
return {
|
|
1449
|
+
nodes,
|
|
1450
|
+
forwardChain,
|
|
1451
|
+
reverseChain,
|
|
1452
|
+
migrationByHash
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
function buildEdgeAnnotationsByHashFromListEntries(entries) {
|
|
1456
|
+
const annotations = /* @__PURE__ */ new Map();
|
|
1457
|
+
for (const entry of entries) annotations.set(entry.hash, {
|
|
1458
|
+
operationCount: entry.operationCount,
|
|
1459
|
+
invariants: entry.providedInvariants
|
|
1460
|
+
});
|
|
1461
|
+
return annotations;
|
|
1462
|
+
}
|
|
1463
|
+
function buildRefsByHashFromListEntries(entries) {
|
|
1464
|
+
const refsByHash = /* @__PURE__ */ new Map();
|
|
1465
|
+
for (const entry of entries) if (entry.refs.length > 0) refsByHash.set(entry.toContract, entry.refs);
|
|
1466
|
+
return refsByHash;
|
|
1467
|
+
}
|
|
1468
|
+
function formatEmptyStateLine(spaceId, style) {
|
|
1469
|
+
return style.emptyState(`There are no migrations in migrations/${spaceId}/ yet`);
|
|
1470
|
+
}
|
|
1471
|
+
function renderSpaceTreeBlock(spaceId, migrations, multiSpace, glyphMode, style, colorize, liveContractHash, graphForSpace, appSpaceId, globalMaxEdgeTreePrefixWidth, globalMaxDirNameWidth) {
|
|
1472
|
+
if (migrations.length === 0) {
|
|
1473
|
+
const emptyLine = formatEmptyStateLine(spaceId, style);
|
|
1474
|
+
if (!multiSpace) return [emptyLine];
|
|
1475
|
+
return [style.spaceHeading(`${spaceId}:`), ` ${emptyLine}`];
|
|
1476
|
+
}
|
|
1477
|
+
const graph = graphForSpace(spaceId) ?? migrationGraphFromListEntries(migrations);
|
|
1478
|
+
const isAppSpace = appSpaceId === void 0 ? void 0 : spaceId === appSpaceId;
|
|
1479
|
+
const treeOutput = renderMigrationGraphSpaceTree({
|
|
1480
|
+
graph,
|
|
1481
|
+
migrations,
|
|
1482
|
+
liveContractHash,
|
|
1483
|
+
glyphMode,
|
|
1484
|
+
colorize,
|
|
1485
|
+
refsByHash: buildRefsByHashFromListEntries(migrations),
|
|
1486
|
+
styler: style,
|
|
1487
|
+
...isAppSpace !== void 0 ? { isAppSpace } : {},
|
|
1488
|
+
...globalMaxEdgeTreePrefixWidth !== void 0 ? { globalMaxEdgeTreePrefixWidth } : {},
|
|
1489
|
+
...globalMaxDirNameWidth !== void 0 ? { globalMaxDirNameWidth } : {}
|
|
1490
|
+
});
|
|
1491
|
+
if (!multiSpace) return treeOutput.length === 0 ? [] : [treeOutput];
|
|
1492
|
+
const indented = indentMigrationGraphTreeBlock(treeOutput, " ");
|
|
1493
|
+
return [style.spaceHeading(`${spaceId}:`), indented];
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Compose the styled `migration list` human output via the shared tree
|
|
1497
|
+
* renderer. Each on-disk migration is one edge row with package-fact
|
|
1498
|
+
* annotations; refs decorate destination contract nodes.
|
|
1499
|
+
*
|
|
1500
|
+
* `options.colorize` must match whether `style` emits ANSI (e.g. both true for
|
|
1501
|
+
* `createAnsiMigrationListStyler({ useColor: true })`).
|
|
1502
|
+
*/
|
|
1503
|
+
function renderMigrationListWithStyle(result, style, glyphMode = "unicode", options = {}) {
|
|
1504
|
+
const multiSpace = result.spaces.length > 1;
|
|
1505
|
+
const colorize = options.colorize ?? false;
|
|
1506
|
+
const liveContractHash = options.liveContractHash ?? EMPTY_CONTRACT_HASH;
|
|
1507
|
+
const graphForSpace = options.graphForSpace ?? (() => void 0);
|
|
1508
|
+
const appSpaceId = options.appSpaceId;
|
|
1509
|
+
const globalLayoutInputs = multiSpace ? result.spaces.filter((space) => space.migrations.length > 0).map((space) => ({
|
|
1510
|
+
graph: graphForSpace(space.space) ?? migrationGraphFromListEntries(space.migrations),
|
|
1511
|
+
liveContractHash
|
|
1512
|
+
})) : [];
|
|
1513
|
+
const globalMaxEdgeTreePrefixWidth = globalLayoutInputs.length > 0 ? computeGlobalMaxEdgeTreePrefixWidth(globalLayoutInputs) : void 0;
|
|
1514
|
+
const globalMaxDirNameWidth = globalLayoutInputs.length > 0 ? computeGlobalMaxDirNameWidth(globalLayoutInputs) : void 0;
|
|
1515
|
+
const lines = [];
|
|
1516
|
+
for (let index = 0; index < result.spaces.length; index++) {
|
|
1517
|
+
const space = result.spaces[index];
|
|
1518
|
+
if (index > 0) lines.push("");
|
|
1519
|
+
lines.push(...renderSpaceTreeBlock(space.space, space.migrations, multiSpace, glyphMode, style, colorize, liveContractHash, graphForSpace, appSpaceId, globalMaxEdgeTreePrefixWidth, globalMaxDirNameWidth));
|
|
1520
|
+
}
|
|
1521
|
+
if (result.spaces.reduce((count, space) => count + space.migrations.length, 0) > 0) {
|
|
1522
|
+
lines.push("");
|
|
1523
|
+
lines.push(style.summary(result.summary));
|
|
1524
|
+
}
|
|
1525
|
+
return lines.join("\n");
|
|
1526
|
+
}
|
|
1527
|
+
//#endregion
|
|
1528
|
+
//#region src/utils/formatters/migration-list-styler.ts
|
|
1529
|
+
function hasMarkersFormatter(styler) {
|
|
1530
|
+
return "markers" in styler && typeof styler.markers === "function";
|
|
1531
|
+
}
|
|
1532
|
+
function styleMarkerName(name) {
|
|
1533
|
+
return name === "contract" ? bold(green(name)) : green(name);
|
|
1534
|
+
}
|
|
1535
|
+
function plainMarkers(names) {
|
|
1536
|
+
return names.map((name) => `@${name}`).join(" ");
|
|
1537
|
+
}
|
|
1538
|
+
function formatContractNodeOverlays(styler, markers, refs) {
|
|
1539
|
+
const parts = [];
|
|
1540
|
+
if (markers.length > 0) parts.push(hasMarkersFormatter(styler) ? styler.markers(markers) : plainMarkers(markers));
|
|
1541
|
+
if (refs.length > 0) parts.push(styler.refs(refs));
|
|
1542
|
+
return parts.join(" ");
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* The current contract overlay marker. Unlike user refs, this names the user's
|
|
1546
|
+
* declared desired state — the implicit base/target for `plan` / `migrate` —
|
|
1547
|
+
* not a stored label. It is emphasized (bold) so it stands out from plain refs
|
|
1548
|
+
* (including the live-database `db` marker, which is just another ref).
|
|
1549
|
+
*/
|
|
1550
|
+
const CONTRACT_MARKER_NAME = "contract";
|
|
1551
|
+
function styleRefName(name) {
|
|
1552
|
+
return green(name);
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Build a {@link MigrationListStyler} that decorates `migration list`
|
|
1556
|
+
* tokens with ANSI SGR codes. When `useColor` is `false` (non-TTY,
|
|
1557
|
+
* `--no-color`, `NO_COLOR=1`, piped output) the function returns the
|
|
1558
|
+
* shared identity styler so callers get plain text with zero ANSI
|
|
1559
|
+
* bytes — pipe-friendly by construction.
|
|
1560
|
+
*
|
|
1561
|
+
* Palette:
|
|
1562
|
+
*
|
|
1563
|
+
* - `dirName`: bold
|
|
1564
|
+
* - `sourceHash`: dim cyan
|
|
1565
|
+
* - `destHash`: bright cyan
|
|
1566
|
+
* - `kind` (`*` / `↩` / `⟲`): bright — the signal; lanes and arrows dim
|
|
1567
|
+
* - `glyph` (`→` / `⟲` / `∅`): dim
|
|
1568
|
+
* - `lane` (graph gutter lines `│` and fan/join connectors `├─┐` / `├─┘`): dim
|
|
1569
|
+
* - `invariants` (`{...}`): yellow
|
|
1570
|
+
* - `markers` (`@contract @db`): green; the `contract` desired-state marker is
|
|
1571
|
+
* green-bold (`db` is plain green); the `@` sigil is applied to each name
|
|
1572
|
+
* - `refs` (`(...)`): green (the active ref is bolded separately by the tree styler)
|
|
1573
|
+
* - `spaceHeading` (`<spaceId>:`): bold
|
|
1574
|
+
* - `summary`: dim
|
|
1575
|
+
* - `emptyState`: dim
|
|
1576
|
+
*/
|
|
1577
|
+
function createAnsiMigrationListStyler(opts) {
|
|
1578
|
+
if (!opts.useColor) return {
|
|
1579
|
+
...IDENTITY_MIGRATION_LIST_STYLER,
|
|
1580
|
+
markers: plainMarkers
|
|
1581
|
+
};
|
|
1582
|
+
return {
|
|
1583
|
+
kind: (text) => text,
|
|
1584
|
+
dirName: (text) => bold(text),
|
|
1585
|
+
sourceHash: (text) => dim(cyan(text)),
|
|
1586
|
+
destHash: (text) => cyanBright(text),
|
|
1587
|
+
glyph: (text) => dim(text),
|
|
1588
|
+
lane: (text) => dim(text),
|
|
1589
|
+
invariants: (ids) => yellow(`{${ids.join(", ")}}`),
|
|
1590
|
+
markers: (names) => {
|
|
1591
|
+
const sigil = green("@");
|
|
1592
|
+
return names.map((name) => sigil + styleMarkerName(name)).join(" ");
|
|
1593
|
+
},
|
|
1594
|
+
refs: (names) => {
|
|
1595
|
+
const open = green("(");
|
|
1596
|
+
const close = green(")");
|
|
1597
|
+
const separator = green(", ");
|
|
1598
|
+
return open + names.map(styleRefName).join(separator) + close;
|
|
1599
|
+
},
|
|
1600
|
+
spaceHeading: (text) => bold(text),
|
|
1601
|
+
summary: (text) => dim(text),
|
|
1602
|
+
emptyState: (text) => dim(text)
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
//#endregion
|
|
1606
|
+
//#region src/utils/formatters/migration-graph-tree-render.ts
|
|
1607
|
+
const LABEL_GAP = 2;
|
|
1608
|
+
/**
|
|
1609
|
+
* The live-database overlay marker. Just another ref as far as styling goes —
|
|
1610
|
+
* the only emphasized markers are the active ref and the `contract`
|
|
1611
|
+
* desired-state marker (see {@link CONTRACT_MARKER_NAME}).
|
|
1612
|
+
*/
|
|
1613
|
+
const DB_MARKER_NAME = "db";
|
|
1614
|
+
const UNICODE_PALETTE = {
|
|
1615
|
+
node: "○ ",
|
|
1616
|
+
arcLand: "○◂",
|
|
1617
|
+
arcTee: "○─",
|
|
1618
|
+
verticalPass: "│ ",
|
|
1619
|
+
branchTee: "├─",
|
|
1620
|
+
mergeTee: "├─",
|
|
1621
|
+
branchCorner: "╮ ",
|
|
1622
|
+
mergeCorner: "╯ ",
|
|
1623
|
+
arcBranchCorner: "╮ ",
|
|
1624
|
+
arcBranchTee: "┬─",
|
|
1625
|
+
arcLandCorner: "╯ ",
|
|
1626
|
+
arcLandTee: "┴─",
|
|
1627
|
+
arcCrossing: "┼─",
|
|
1628
|
+
arcLandBridge: "──",
|
|
1629
|
+
horizontalPass: "──",
|
|
1630
|
+
connectorBranchTee: "├─",
|
|
1631
|
+
connectorBranchTeeCo: "┬─",
|
|
1632
|
+
connectorMergeTeeCo: "┴─",
|
|
1633
|
+
edgeArrow: {
|
|
1634
|
+
forward: "↑",
|
|
1635
|
+
rollback: "↓",
|
|
1636
|
+
self: "⟲"
|
|
1637
|
+
},
|
|
1638
|
+
forwardArrow: migrationListForwardArrow("unicode"),
|
|
1639
|
+
emptySource: migrationListEmptySource("unicode")
|
|
1640
|
+
};
|
|
1641
|
+
const ASCII_PALETTE = {
|
|
1642
|
+
node: "* ",
|
|
1643
|
+
arcLand: "*<",
|
|
1644
|
+
arcTee: "*-",
|
|
1645
|
+
verticalPass: "| ",
|
|
1646
|
+
branchTee: "+-",
|
|
1647
|
+
mergeTee: "+-",
|
|
1648
|
+
branchCorner: "\\ ",
|
|
1649
|
+
mergeCorner: "/ ",
|
|
1650
|
+
arcBranchCorner: "\\ ",
|
|
1651
|
+
arcBranchTee: "+-",
|
|
1652
|
+
arcLandCorner: "/ ",
|
|
1653
|
+
arcLandTee: "+-",
|
|
1654
|
+
arcCrossing: "+-",
|
|
1655
|
+
arcLandBridge: "--",
|
|
1656
|
+
horizontalPass: "--",
|
|
1657
|
+
connectorBranchTee: "+-",
|
|
1658
|
+
connectorBranchTeeCo: "+-",
|
|
1659
|
+
connectorMergeTeeCo: "+-",
|
|
1660
|
+
edgeArrow: {
|
|
1661
|
+
forward: "^",
|
|
1662
|
+
rollback: "v",
|
|
1663
|
+
self: "@"
|
|
1664
|
+
},
|
|
1665
|
+
forwardArrow: migrationListForwardArrow("ascii"),
|
|
1666
|
+
emptySource: migrationListEmptySource("ascii")
|
|
1667
|
+
};
|
|
1668
|
+
function paletteFor(mode) {
|
|
1669
|
+
return mode === "ascii" ? ASCII_PALETTE : UNICODE_PALETTE;
|
|
1670
|
+
}
|
|
1671
|
+
function overlayStatusGlyphs(mode) {
|
|
1672
|
+
return mode === "ascii" ? {
|
|
1673
|
+
applied: "+",
|
|
1674
|
+
pending: ">"
|
|
1675
|
+
} : {
|
|
1676
|
+
applied: "✓",
|
|
1677
|
+
pending: "⧗"
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
function arrowForEdgeKind(kind, palette) {
|
|
1681
|
+
return palette.edgeArrow[kind];
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* Forced-color functions that always emit ANSI regardless of the ambient TTY
|
|
1685
|
+
* environment (NO_COLOR, piped output). Used for:
|
|
1686
|
+
*
|
|
1687
|
+
* - `forcedBold`: branch-coloured migration names pair their lane hue with bold;
|
|
1688
|
+
* both must emit so the name is deterministically bold + hue.
|
|
1689
|
+
* - `forcedDim`: off-path path-highlight override (migrate --show).
|
|
1690
|
+
* The renderer gates this behind `opts.colorize`; the forced variant ensures
|
|
1691
|
+
* ANSI is emitted in controlled environments (e.g. tests with `NO_COLOR=1`)
|
|
1692
|
+
* when the caller explicitly requests colour. Without forcing, `dim()` from
|
|
1693
|
+
* the ambient module-level import no-ops under NO_COLOR, making the
|
|
1694
|
+
* path-highlight unreachable in tests.
|
|
1695
|
+
*/
|
|
1696
|
+
const { bold: forcedBold, dim: forcedDim, greenBright: forcedGreen } = createColors({ useColor: true });
|
|
1697
|
+
/**
|
|
1698
|
+
* The two styles used in `migrate --show` path-highlight mode.
|
|
1699
|
+
*
|
|
1700
|
+
* In path-highlight mode the normal by-branch rotating-colour logic
|
|
1701
|
+
* (`LANE_COLOR_CYCLE` / `laneStylerForColumn`) is suppressed entirely.
|
|
1702
|
+
* Every glyph, name, and hash is styled by its on-path / off-path role,
|
|
1703
|
+
* never by lane column index.
|
|
1704
|
+
*
|
|
1705
|
+
* - `onPath`: neutral single-path style — exactly how a linear (no-branch)
|
|
1706
|
+
* section renders today. Lane glyphs are dim, names are bold, hashes use
|
|
1707
|
+
* the default `sourceHash`/`destHash` colours. No rotation hue is applied.
|
|
1708
|
+
* This is identical to how the pgvector single-path section renders.
|
|
1709
|
+
* - `offPath`: uniform dim grey on every cell (name, hashes, lane glyphs,
|
|
1710
|
+
* direction arrows).
|
|
1711
|
+
*
|
|
1712
|
+
* To change the on-path or off-path colour in future, edit this object only.
|
|
1713
|
+
*/
|
|
1714
|
+
const PATH_HIGHLIGHT_STYLES = {
|
|
1715
|
+
/**
|
|
1716
|
+
* Lane/glyph/arrow stylers for on-path cells.
|
|
1717
|
+
*
|
|
1718
|
+
* - lane: `forcedGreen` when colour is on — bright green so the on-path
|
|
1719
|
+
* branch glyphs (`│ ├ ╯ ↑`) and node markers (`○`/`∅`) are visually
|
|
1720
|
+
* distinct from off-path (dim grey). Uses forced ANSI so it survives
|
|
1721
|
+
* NO_COLOR in tests. Identity when `colorize` is false.
|
|
1722
|
+
* - arrow: identity (plain, no colouring)
|
|
1723
|
+
* - dirName: `bold` (ambient bold — name stays white/bold, not green)
|
|
1724
|
+
* - hashOverride: undefined — `style.sourceHash`/`style.destHash` apply
|
|
1725
|
+
* normally (cyan) so hashes keep their existing neutral colour.
|
|
1726
|
+
*
|
|
1727
|
+
* `style` is the same `MigrationListStyler` the tree renderer uses.
|
|
1728
|
+
* Rotation (`LANE_COLOR_CYCLE`) is never applied to on-path cells.
|
|
1729
|
+
*/
|
|
1730
|
+
onPath: (_style, colorize) => ({
|
|
1731
|
+
lane: colorize ? forcedGreen : (text) => text,
|
|
1732
|
+
arrow: (text) => text,
|
|
1733
|
+
dirName: (text) => bold(text),
|
|
1734
|
+
hashOverride: void 0
|
|
1735
|
+
}),
|
|
1736
|
+
/**
|
|
1737
|
+
* Lane/glyph/arrow/hash stylers for off-path cells.
|
|
1738
|
+
* Uniform dim grey on everything — uses `forcedDim` so ANSI is emitted even
|
|
1739
|
+
* under NO_COLOR (test environments use `colorize:true` + NO_COLOR=1 to verify dim).
|
|
1740
|
+
* Returns identity functions when colour is off (`colorize: false`).
|
|
1741
|
+
*/
|
|
1742
|
+
offPath: (colorize) => ({
|
|
1743
|
+
lane: colorize ? forcedDim : (text) => text,
|
|
1744
|
+
arrow: colorize ? forcedDim : (text) => text,
|
|
1745
|
+
dirName: colorize ? forcedDim : (text) => text,
|
|
1746
|
+
hashOverride: colorize ? forcedDim : void 0
|
|
1747
|
+
})
|
|
1748
|
+
};
|
|
1749
|
+
function laneStylerForColumn(colorColumn, colorize, style) {
|
|
1750
|
+
return stylerForLaneColumn(colorColumn, colorize, style.lane);
|
|
1751
|
+
}
|
|
1752
|
+
/**
|
|
1753
|
+
* Tint a branch-owned token (direction arrow, migration name) by its edge's
|
|
1754
|
+
* lane so the whole branch row reads in one colour. Column 0 has nothing to be
|
|
1755
|
+
* told apart from in the common linear chain, so it keeps the token's existing
|
|
1756
|
+
* default styling (`fallback`) rather than a palette hue; only lanes ≥ 1 take a
|
|
1757
|
+
* colour. With colour off, the fallback (also colourless) is used unchanged.
|
|
1758
|
+
*/
|
|
1759
|
+
function branchStylerOrDefault(column, colorize, fallback) {
|
|
1760
|
+
if (!colorize || column <= 0) return fallback;
|
|
1761
|
+
return laneColorForColumn(column);
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Render a crossing tee (`┼─`): the junction stays dim/neutral so neither arc
|
|
1765
|
+
* steals the cell; the trailing dash takes the served lane hue.
|
|
1766
|
+
*/
|
|
1767
|
+
function renderArcCrossing(pair, dashColumn, colorize, style) {
|
|
1768
|
+
const junction = colorize ? style.lane : (text) => text;
|
|
1769
|
+
const dash = laneStylerForColumn(dashColumn, colorize, style);
|
|
1770
|
+
return junction(pair.slice(0, 1)) + dash(pair.slice(1));
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* Render a connector tee (`├─` / `┬─` / `┴─`) with its junction glyph and its
|
|
1774
|
+
* trailing dash coloured independently: the junction anchors its own lane while
|
|
1775
|
+
* the dash leads into the branch on its right.
|
|
1776
|
+
*/
|
|
1777
|
+
function renderConnectorTee(pair, glyphColumn, dashColumn, colorize, style) {
|
|
1778
|
+
const glyph = laneStylerForColumn(glyphColumn, colorize, style);
|
|
1779
|
+
if (glyphColumn === dashColumn) return glyph(pair);
|
|
1780
|
+
return glyph(pair.slice(0, 1)) + laneStylerForColumn(dashColumn, colorize, style)(pair.slice(1));
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* A node-marker glyph pair (`○◂`, `○─`, `*<`, `*-`) is the contract node
|
|
1784
|
+
* marker (`○` / `*`) followed by an arc connector (`◂` / `─` / `<` / `-`). The
|
|
1785
|
+
* marker takes its own lane's hue (so each node visibly belongs to its branch);
|
|
1786
|
+
* the connector follows the arc it belongs to (its owning back-lane hue).
|
|
1787
|
+
* Direction arrows are handled elsewhere — they take their edge's lane hue too.
|
|
1788
|
+
*
|
|
1789
|
+
* When `laneOverride` is provided (for path-highlight rows), it replaces the
|
|
1790
|
+
* marker styler. `arcLaneOverride` (if provided) replaces the connector styler
|
|
1791
|
+
* independently — this matters when the node is on-path but the arc belongs to
|
|
1792
|
+
* an off-path rollback edge, which must render dim rather than green.
|
|
1793
|
+
*/
|
|
1794
|
+
function renderNodeMarkerPair(pair, nodeColumn, arcColumn, colorize, style, laneOverride, arcLaneOverride) {
|
|
1795
|
+
const marker = laneOverride ?? laneStylerForColumn(nodeColumn, colorize, style);
|
|
1796
|
+
const connector = arcLaneOverride ?? laneOverride ?? laneStylerForColumn(arcColumn, colorize, style);
|
|
1797
|
+
return marker(pair.slice(0, 1)) + connector(pair.slice(1));
|
|
1798
|
+
}
|
|
1799
|
+
function renderCellPair(cell, column, colors, colorize, style, palette, laneOverride, arrowOverride, arcLaneOverride) {
|
|
1800
|
+
const laneColumn = colors.lane[column] ?? column;
|
|
1801
|
+
const lane = laneOverride ?? laneStylerForColumn(laneColumn, colorize, style);
|
|
1802
|
+
const arrow = arrowOverride ?? ((text) => branchStylerOrDefault(column, colorize, style.kind)(text));
|
|
1803
|
+
switch (cell.kind) {
|
|
1804
|
+
case "node": {
|
|
1805
|
+
const arcColumn = colors.connector[column] ?? 0;
|
|
1806
|
+
if (cell.arcLand === true) return renderNodeMarkerPair(palette.arcLand, column, arcColumn, colorize, style, laneOverride, arcLaneOverride);
|
|
1807
|
+
if (cell.arcTee === true) return renderNodeMarkerPair(palette.arcTee, column, arcColumn, colorize, style, laneOverride, arcLaneOverride);
|
|
1808
|
+
return lane(palette.node);
|
|
1809
|
+
}
|
|
1810
|
+
case "vertical-pass": return lane(palette.verticalPass);
|
|
1811
|
+
case "edge-lane": return cell.ownsLabel ? lane(palette.verticalPass.trimEnd()) + arrow(arrowForEdgeKind(cell.edgeKind, palette)) : lane(palette.verticalPass);
|
|
1812
|
+
case "branch-tee": return lane(palette.branchTee);
|
|
1813
|
+
case "merge-tee": return lane(palette.mergeTee);
|
|
1814
|
+
case "branch-corner": return lane(palette.branchCorner);
|
|
1815
|
+
case "merge-corner": return lane(palette.mergeCorner);
|
|
1816
|
+
case "arc-branch-corner": return lane(palette.arcBranchCorner);
|
|
1817
|
+
case "arc-branch-tee": return lane(palette.arcBranchTee);
|
|
1818
|
+
case "arc-land-corner": return lane(palette.arcLandCorner);
|
|
1819
|
+
case "arc-land-tee": return laneOverride !== void 0 ? laneOverride(palette.arcLandTee) : renderConnectorTee(palette.arcLandTee, laneColumn, colors.dash[column] ?? laneColumn, colorize, style);
|
|
1820
|
+
case "arc-crossing": return lane(palette.arcLandBridge);
|
|
1821
|
+
case "arc-land-bridge": return lane(palette.arcLandBridge);
|
|
1822
|
+
case "horizontal-pass": return lane(palette.horizontalPass);
|
|
1823
|
+
case "empty": return " ";
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Render a branch-connector or merge-connector row.
|
|
1828
|
+
*
|
|
1829
|
+
* `columnLaneOverride` is an optional per-column map populated when path-highlight
|
|
1830
|
+
* annotations are active (`migrate --show`). For each column in the connector's
|
|
1831
|
+
* lane range, the map supplies the override styler (dim for off-path) that should
|
|
1832
|
+
* replace the normal rotating-lane colour for that column. Columns absent from the
|
|
1833
|
+
* map (on-path or unannotated) use the standard `laneStylerForColumn` logic unchanged.
|
|
1834
|
+
* This ensures off-path branch connectors appear dim rather than in their rotation
|
|
1835
|
+
* colour (e.g. magenta).
|
|
1836
|
+
*/
|
|
1837
|
+
function renderConnectorRow(row, gridWidth, colorize, style, palette, columnLaneOverride) {
|
|
1838
|
+
const resolvedLane = (column) => columnLaneOverride?.get(column) ?? laneStylerForColumn(column, colorize, style);
|
|
1839
|
+
const isMerge = row.kind === "merge-connector";
|
|
1840
|
+
if (row.cells.length > 0) {
|
|
1841
|
+
const colors = resolveConnectorLaneColors(row.cells, row.startLane ?? 0);
|
|
1842
|
+
let seenTee = false;
|
|
1843
|
+
let out = "";
|
|
1844
|
+
for (let column = 0; column < row.cells.length; column++) {
|
|
1845
|
+
const cell = row.cells[column];
|
|
1846
|
+
if (cell === void 0) continue;
|
|
1847
|
+
const glyphColumn = colors.glyph[column] ?? column;
|
|
1848
|
+
const dashColumn = colors.dash[column] ?? glyphColumn;
|
|
1849
|
+
const override = columnLaneOverride?.get(glyphColumn);
|
|
1850
|
+
const dashOverrideForPathHighlight = columnLaneOverride?.get(dashColumn) ?? override;
|
|
1851
|
+
if (override !== void 0 || columnLaneOverride !== void 0 && dashOverrideForPathHighlight !== void 0) {
|
|
1852
|
+
const effectiveOverride = override ?? dashOverrideForPathHighlight;
|
|
1853
|
+
if (effectiveOverride === void 0) {
|
|
1854
|
+
out += " ";
|
|
1855
|
+
continue;
|
|
1856
|
+
}
|
|
1857
|
+
switch (cell.kind) {
|
|
1858
|
+
case "branch-tee":
|
|
1859
|
+
case "merge-tee": {
|
|
1860
|
+
const pair = seenTee ? palette.connectorBranchTeeCo : palette.connectorBranchTee;
|
|
1861
|
+
out += effectiveOverride(pair.slice(0, 1)) + effectiveOverride(pair.slice(1));
|
|
1862
|
+
seenTee = true;
|
|
1863
|
+
break;
|
|
1864
|
+
}
|
|
1865
|
+
case "branch-corner":
|
|
1866
|
+
out += effectiveOverride(palette.branchCorner);
|
|
1867
|
+
break;
|
|
1868
|
+
case "merge-corner":
|
|
1869
|
+
out += effectiveOverride(palette.mergeCorner);
|
|
1870
|
+
break;
|
|
1871
|
+
case "vertical-pass":
|
|
1872
|
+
out += effectiveOverride(palette.verticalPass);
|
|
1873
|
+
break;
|
|
1874
|
+
case "horizontal-pass":
|
|
1875
|
+
out += effectiveOverride(palette.horizontalPass);
|
|
1876
|
+
break;
|
|
1877
|
+
case "arc-crossing": {
|
|
1878
|
+
const arcCrossingDashOverride = columnLaneOverride?.get(dashColumn) ?? effectiveOverride;
|
|
1879
|
+
out += effectiveOverride(palette.arcCrossing.slice(0, 1)) + arcCrossingDashOverride(palette.arcCrossing.slice(1));
|
|
1880
|
+
break;
|
|
1881
|
+
}
|
|
1882
|
+
default: out += " ";
|
|
1883
|
+
}
|
|
1884
|
+
continue;
|
|
1885
|
+
}
|
|
1886
|
+
const lane = laneStylerForColumn(glyphColumn, colorize, style);
|
|
1887
|
+
switch (cell.kind) {
|
|
1888
|
+
case "branch-tee":
|
|
1889
|
+
out += renderConnectorTee(seenTee ? palette.connectorBranchTeeCo : palette.connectorBranchTee, glyphColumn, dashColumn, colorize, style);
|
|
1890
|
+
seenTee = true;
|
|
1891
|
+
break;
|
|
1892
|
+
case "merge-tee":
|
|
1893
|
+
out += renderConnectorTee(seenTee ? palette.connectorMergeTeeCo : palette.connectorBranchTee, glyphColumn, dashColumn, colorize, style);
|
|
1894
|
+
seenTee = true;
|
|
1895
|
+
break;
|
|
1896
|
+
case "branch-corner":
|
|
1897
|
+
out += lane(palette.branchCorner);
|
|
1898
|
+
break;
|
|
1899
|
+
case "merge-corner":
|
|
1900
|
+
out += lane(palette.mergeCorner);
|
|
1901
|
+
break;
|
|
1902
|
+
case "vertical-pass":
|
|
1903
|
+
out += lane(palette.verticalPass);
|
|
1904
|
+
break;
|
|
1905
|
+
case "horizontal-pass":
|
|
1906
|
+
out += lane(palette.horizontalPass);
|
|
1907
|
+
break;
|
|
1908
|
+
case "arc-crossing":
|
|
1909
|
+
out += renderArcCrossing(palette.arcCrossing, dashColumn, colorize, style);
|
|
1910
|
+
break;
|
|
1911
|
+
default: out += " ";
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
for (let column = row.cells.length; column < gridWidth; column++) out += " ";
|
|
1915
|
+
return out;
|
|
1916
|
+
}
|
|
1917
|
+
const start = row.startLane ?? 0;
|
|
1918
|
+
const end = row.endLane ?? start;
|
|
1919
|
+
const runLane = resolvedLane(end);
|
|
1920
|
+
let out = "";
|
|
1921
|
+
for (let column = 0; column < gridWidth; column++) if (column < start || column > end) out += " ";
|
|
1922
|
+
else if (column === start) out += runLane(palette.connectorBranchTee);
|
|
1923
|
+
else if (column === end) out += runLane(isMerge ? palette.mergeCorner : palette.branchCorner);
|
|
1924
|
+
else out += runLane(isMerge ? palette.connectorMergeTeeCo : palette.connectorBranchTeeCo);
|
|
1925
|
+
return out;
|
|
1926
|
+
}
|
|
1927
|
+
function abbreviateHash(hash, hashLength, emptySource) {
|
|
1928
|
+
if (hash === EMPTY_CONTRACT_HASH) return emptySource;
|
|
1929
|
+
return (hash.startsWith("sha256:") ? hash.slice(7) : hash).slice(0, hashLength);
|
|
1930
|
+
}
|
|
1931
|
+
const MIN_HASH_DATA_COLUMN = 25;
|
|
1932
|
+
function overlayNamesForContract(contractHash, opts) {
|
|
1933
|
+
const markers = [];
|
|
1934
|
+
const refs = [];
|
|
1935
|
+
const userRefs = opts.refsByHash?.get(contractHash);
|
|
1936
|
+
if (userRefs) refs.push(...[...userRefs].sort((a, b) => a.localeCompare(b)));
|
|
1937
|
+
if (opts.isAppSpace !== false && opts.contractHash === contractHash && contractHash !== EMPTY_CONTRACT_HASH) markers.push(CONTRACT_MARKER_NAME);
|
|
1938
|
+
if (opts.dbHash === contractHash) markers.push(DB_MARKER_NAME);
|
|
1939
|
+
markers.sort((a, b) => {
|
|
1940
|
+
if (a === "contract") return -1;
|
|
1941
|
+
if (b === "contract") return 1;
|
|
1942
|
+
return a.localeCompare(b);
|
|
1943
|
+
});
|
|
1944
|
+
return {
|
|
1945
|
+
markers,
|
|
1946
|
+
refs
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
function createTreeStyler(opts) {
|
|
1950
|
+
const base = opts.styler ?? createAnsiMigrationListStyler({ useColor: opts.colorize });
|
|
1951
|
+
const activeRefName = opts.activeRefName;
|
|
1952
|
+
if (!opts.colorize || activeRefName === void 0) return base;
|
|
1953
|
+
return {
|
|
1954
|
+
...base,
|
|
1955
|
+
refs: (names) => {
|
|
1956
|
+
const styledNames = names.map((name) => name === activeRefName ? bold(name) : name);
|
|
1957
|
+
return base.refs(styledNames);
|
|
1958
|
+
}
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
function formatEdgeAnnotationSuffix(migrationHash, opts, style) {
|
|
1962
|
+
const annotation = opts.edgeAnnotationsByHash?.get(migrationHash);
|
|
1963
|
+
if (annotation === void 0) return "";
|
|
1964
|
+
const isOffPath = annotation.pathHighlight === "off-path";
|
|
1965
|
+
const segments = [];
|
|
1966
|
+
if (annotation.operationCount !== void 0) segments.push(`${annotation.operationCount} ops`);
|
|
1967
|
+
if (annotation.invariants !== void 0 && annotation.invariants.length > 0) segments.push(style.invariants(annotation.invariants));
|
|
1968
|
+
const status = annotation.status;
|
|
1969
|
+
if (status !== void 0) {
|
|
1970
|
+
const glyphs = overlayStatusGlyphs(opts.glyphMode ?? "unicode");
|
|
1971
|
+
const glyph = status === "applied" ? glyphs.applied : glyphs.pending;
|
|
1972
|
+
const label = status === "applied" ? "applied" : "pending";
|
|
1973
|
+
if (!opts.colorize) segments.push(`${glyph} ${label}`);
|
|
1974
|
+
else {
|
|
1975
|
+
const styler = status === "applied" ? green : yellow;
|
|
1976
|
+
segments.push(styler(`${glyph} ${label}`));
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
if (annotation.pathHighlight === "on-path") {
|
|
1980
|
+
const glyph = opts.glyphMode === "ascii" ? ">" : "↑";
|
|
1981
|
+
segments.push(`${glyph} will run`);
|
|
1982
|
+
}
|
|
1983
|
+
if (segments.length === 0) return "";
|
|
1984
|
+
const suffix = ` ${segments.join(" ")}`;
|
|
1985
|
+
return opts.colorize && isOffPath ? forcedDim(suffix) : suffix;
|
|
1986
|
+
}
|
|
1987
|
+
/**
|
|
1988
|
+
* Format the `from → to` hash data column for an edge row.
|
|
1989
|
+
*
|
|
1990
|
+
* When `hashOverride` is provided (off-path → `dim`), it replaces ALL sub-stylers
|
|
1991
|
+
* (`sourceHash`, `destHash`, arrow `glyph`) so dim reaches every character without
|
|
1992
|
+
* inner ANSI codes (e.g. the dim+cyan of `sourceHash`) overriding it. On-path edges
|
|
1993
|
+
* carry no override. Without an override, the normal `style` sub-stylers apply.
|
|
1994
|
+
*/
|
|
1995
|
+
function formatEdgeHashColumn(edge, style, hashLength, palette, hashOverride) {
|
|
1996
|
+
const src = hashOverride ?? style.sourceHash;
|
|
1997
|
+
const dst = hashOverride ?? style.destHash;
|
|
1998
|
+
const glyph = hashOverride ?? style.glyph;
|
|
1999
|
+
if (edge.kind === "self") {
|
|
2000
|
+
const hash = abbreviateHash(edge.from, hashLength, palette.emptySource);
|
|
2001
|
+
return `${padFromHashColumn(src(hash), hashLength)} ${glyph(palette.forwardArrow)} ${dst(hash)}`;
|
|
2002
|
+
}
|
|
2003
|
+
return `${edge.from === EMPTY_CONTRACT_HASH ? padFromHashColumn(glyph(palette.emptySource), hashLength) : padFromHashColumn(src(abbreviateHash(edge.from, hashLength, palette.emptySource)), hashLength)} ${glyph(palette.forwardArrow)} ${dst(abbreviateHash(edge.to, hashLength, palette.emptySource))}`;
|
|
2004
|
+
}
|
|
2005
|
+
function padVisible(text, targetWidth) {
|
|
2006
|
+
const padding = Math.max(0, targetWidth - stringWidth(text));
|
|
2007
|
+
return text + " ".repeat(padding);
|
|
2008
|
+
}
|
|
2009
|
+
const ANSI_ESCAPE = "\x1B";
|
|
2010
|
+
function trimTrailingWhitespace(line) {
|
|
2011
|
+
const trailingSpaceBeforeReset = new RegExp(`[\\t ]+((?:${ANSI_ESCAPE}\\[[0-9;]*m)+)$`);
|
|
2012
|
+
return line.replace(trailingSpaceBeforeReset, "$1").replace(/\s+$/, "");
|
|
2013
|
+
}
|
|
2014
|
+
function gridWidthForModel(rows) {
|
|
2015
|
+
return rows.reduce((max, row) => row.kind === "node" || row.kind === "edge" ? Math.max(max, row.cells.length) : max, 1);
|
|
2016
|
+
}
|
|
2017
|
+
function maxDirNameLength(edges) {
|
|
2018
|
+
if (edges.length === 0) return 0;
|
|
2019
|
+
return Math.max(...edges.map((edge) => edge.dirName.length));
|
|
2020
|
+
}
|
|
2021
|
+
function rowDirNameWidth(labelColumn, maxDirNameLen, dirNameGap) {
|
|
2022
|
+
return Math.max(maxDirNameLen + dirNameGap, MIN_HASH_DATA_COLUMN - labelColumn);
|
|
2023
|
+
}
|
|
2024
|
+
function gridUsesSkipRollbackArcs(rows) {
|
|
2025
|
+
return rows.some((row) => row.cells.some((cell) => cell.kind === "edge-lane" && cell.adjacency === "node-skipping-rollback"));
|
|
2026
|
+
}
|
|
2027
|
+
function edgeLabelColumn(row, wideLabelColumn) {
|
|
2028
|
+
if (wideLabelColumn !== void 0) return wideLabelColumn;
|
|
2029
|
+
const laneIndex = row.laneIndex ?? 0;
|
|
2030
|
+
if (row.edge?.from === EMPTY_CONTRACT_HASH && laneIndex === 0) return (laneIndex + 1) * 2 + LABEL_GAP;
|
|
2031
|
+
return row.cells.some((cell, index) => index > laneIndex && cell.kind === "vertical-pass") ? row.cells.length * 2 + LABEL_GAP : (laneIndex + 1) * 2 + LABEL_GAP;
|
|
2032
|
+
}
|
|
2033
|
+
function maxEdgeTreePrefixWidth(rows, wideLabelColumn) {
|
|
2034
|
+
let max = 0;
|
|
2035
|
+
for (const row of rows) {
|
|
2036
|
+
if (row.kind !== "edge" || row.edge === void 0) continue;
|
|
2037
|
+
max = Math.max(max, edgeLabelColumn(row, wideLabelColumn));
|
|
2038
|
+
}
|
|
2039
|
+
return max;
|
|
2040
|
+
}
|
|
2041
|
+
function computeMaxEdgeTreePrefixWidthForLayout(model) {
|
|
2042
|
+
const wideLabelColumn = gridUsesSkipRollbackArcs(model.rows) ? gridWidthForModel(model.rows) * 2 + 4 : void 0;
|
|
2043
|
+
return maxEdgeTreePrefixWidth(model.rows, wideLabelColumn);
|
|
2044
|
+
}
|
|
2045
|
+
function computeMaxDirNameLengthForLayout(model) {
|
|
2046
|
+
return maxDirNameLength(model.rows.filter((row) => row.kind === "edge" && row.edge !== void 0).map((row) => row.edge));
|
|
2047
|
+
}
|
|
2048
|
+
function nodeHasArcDecoration(row) {
|
|
2049
|
+
return row.cells.some((cell) => cell.kind === "node" && (cell.arcTee === true || cell.arcLand === true));
|
|
2050
|
+
}
|
|
2051
|
+
function renderMigrationGraphTree(model, opts) {
|
|
2052
|
+
const palette = paletteFor(opts.glyphMode ?? "unicode");
|
|
2053
|
+
const style = createTreeStyler(opts);
|
|
2054
|
+
const hashLength = opts.hashLength ?? 7;
|
|
2055
|
+
const gridWidth = gridWidthForModel(model.rows);
|
|
2056
|
+
const wideLabelColumn = gridUsesSkipRollbackArcs(model.rows) ? gridWidth * 2 + 4 : void 0;
|
|
2057
|
+
const dirNameGap = wideLabelColumn !== void 0 ? 3 : LABEL_GAP;
|
|
2058
|
+
const maxDirNameLen = maxDirNameLength(model.rows.filter((row) => row.kind === "edge" && row.edge !== void 0).map((row) => row.edge));
|
|
2059
|
+
const effectiveMaxDirNameLen = opts.globalMaxDirNameWidth ?? maxDirNameLen;
|
|
2060
|
+
const maxEdgePrefixWidth = opts.globalMaxEdgeTreePrefixWidth ?? maxEdgeTreePrefixWidth(model.rows, wideLabelColumn);
|
|
2061
|
+
const edgeDirNameWidth = rowDirNameWidth(maxEdgePrefixWidth, effectiveMaxDirNameLen, dirNameGap);
|
|
2062
|
+
const contractHighlights = /* @__PURE__ */ new Map();
|
|
2063
|
+
if (opts.edgeAnnotationsByHash) for (const row of model.rows) {
|
|
2064
|
+
if (row.kind !== "edge" || row.edge === void 0) continue;
|
|
2065
|
+
const annotation = opts.edgeAnnotationsByHash.get(row.edge.migrationHash);
|
|
2066
|
+
if (annotation?.pathHighlight === void 0) continue;
|
|
2067
|
+
const highlight = annotation.pathHighlight;
|
|
2068
|
+
for (const hash of [row.edge.from, row.edge.to]) {
|
|
2069
|
+
if (hash === EMPTY_CONTRACT_HASH) continue;
|
|
2070
|
+
if (contractHighlights.get(hash) !== "on-path") contractHighlights.set(hash, highlight);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
const pathHighlightActive = opts.edgeAnnotationsByHash !== void 0;
|
|
2074
|
+
/**
|
|
2075
|
+
* Resolve the lane and arrow overrides for a row in path-highlight mode.
|
|
2076
|
+
* - on-path → neutral single-path style (style.lane for glyphs, plain arrow, bold name).
|
|
2077
|
+
* Rotation colour is suppressed; `style.sourceHash`/`style.destHash` apply for hashes.
|
|
2078
|
+
* - off-path → uniform dim grey (forcedDim) on every glyph, arrow, name, and hash.
|
|
2079
|
+
* - undefined → `undefined` (no override). Unannotated rows use normal rotation. This covers
|
|
2080
|
+
* both non-path-highlight commands (graph/status/list) and any annotation without pathHighlight.
|
|
2081
|
+
* - When pathHighlightActive is false: always returns undefined, preserving normal rotation.
|
|
2082
|
+
*/
|
|
2083
|
+
function pathStyleForHighlight(highlight) {
|
|
2084
|
+
if (!pathHighlightActive || highlight === void 0) return void 0;
|
|
2085
|
+
if (highlight === "off-path") {
|
|
2086
|
+
const s = PATH_HIGHLIGHT_STYLES.offPath(opts.colorize);
|
|
2087
|
+
return {
|
|
2088
|
+
lane: s.lane,
|
|
2089
|
+
arrow: s.arrow,
|
|
2090
|
+
dirName: s.dirName,
|
|
2091
|
+
hashOverride: s.hashOverride
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
const s = PATH_HIGHLIGHT_STYLES.onPath(style, opts.colorize);
|
|
2095
|
+
return {
|
|
2096
|
+
lane: s.lane,
|
|
2097
|
+
arrow: s.arrow,
|
|
2098
|
+
dirName: s.dirName,
|
|
2099
|
+
hashOverride: s.hashOverride
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
/**
|
|
2103
|
+
* Lane override for a given highlight in path-highlight mode.
|
|
2104
|
+
* Returns the `lane` part only — used for per-cell overrides.
|
|
2105
|
+
*/
|
|
2106
|
+
function pathLaneFor(highlight) {
|
|
2107
|
+
return pathStyleForHighlight(highlight)?.lane;
|
|
2108
|
+
}
|
|
2109
|
+
/**
|
|
2110
|
+
* Arrow override for a given highlight in path-highlight mode.
|
|
2111
|
+
* Returns the `arrow` part only — used for edge-lane cell arrow rendering.
|
|
2112
|
+
*/
|
|
2113
|
+
function pathArrowFor(highlight) {
|
|
2114
|
+
return pathStyleForHighlight(highlight)?.arrow;
|
|
2115
|
+
}
|
|
2116
|
+
const lines = [];
|
|
2117
|
+
for (let rowIndex = 0; rowIndex < model.rows.length; rowIndex++) {
|
|
2118
|
+
const row = model.rows[rowIndex];
|
|
2119
|
+
if (row === void 0) continue;
|
|
2120
|
+
if (row.kind === "component-separator") {
|
|
2121
|
+
lines.push("");
|
|
2122
|
+
continue;
|
|
2123
|
+
}
|
|
2124
|
+
if (row.kind === "branch-connector" || row.kind === "merge-connector") {
|
|
2125
|
+
let connectorColumnOverride;
|
|
2126
|
+
if (pathHighlightActive && opts.colorize) {
|
|
2127
|
+
connectorColumnOverride = /* @__PURE__ */ new Map();
|
|
2128
|
+
for (let col = 0; col < row.cells.length; col++) {
|
|
2129
|
+
const cell = row.cells[col];
|
|
2130
|
+
if (cell === void 0 || cell.kind === "empty") continue;
|
|
2131
|
+
const hashForCell = "migrationHash" in cell && cell.migrationHash !== void 0 ? cell.migrationHash : void 0;
|
|
2132
|
+
if (hashForCell === void 0) continue;
|
|
2133
|
+
const highlight = opts.edgeAnnotationsByHash?.get(hashForCell)?.pathHighlight;
|
|
2134
|
+
const override = pathLaneFor(highlight);
|
|
2135
|
+
if (override !== void 0) connectorColumnOverride.set(col, override);
|
|
2136
|
+
}
|
|
2137
|
+
if (connectorColumnOverride.size === 0) connectorColumnOverride = void 0;
|
|
2138
|
+
}
|
|
2139
|
+
lines.push(trimTrailingWhitespace(renderConnectorRow(row, gridWidth, opts.colorize, style, palette, connectorColumnOverride)));
|
|
2140
|
+
continue;
|
|
2141
|
+
}
|
|
2142
|
+
let rowPathHighlight;
|
|
2143
|
+
if (row.kind === "edge" && row.edge !== void 0) rowPathHighlight = opts.edgeAnnotationsByHash?.get(row.edge.migrationHash)?.pathHighlight;
|
|
2144
|
+
else if (row.kind === "node" && row.contractHash !== void 0) rowPathHighlight = contractHighlights.get(row.contractHash);
|
|
2145
|
+
const rowStyle = pathStyleForHighlight(rowPathHighlight);
|
|
2146
|
+
const rowLaneOverride = rowStyle?.lane;
|
|
2147
|
+
const rowArrowOverride = rowStyle?.arrow;
|
|
2148
|
+
const cellColors = resolveRowArcLaneColors(row.cells);
|
|
2149
|
+
let gutter = row.cells.map((cell, column) => {
|
|
2150
|
+
let laneOverride = rowLaneOverride;
|
|
2151
|
+
let arrowOverride = rowArrowOverride;
|
|
2152
|
+
let arcLaneOverride;
|
|
2153
|
+
if (pathHighlightActive) {
|
|
2154
|
+
if (cell.kind === "edge-lane") {
|
|
2155
|
+
const cellHighlight = opts.edgeAnnotationsByHash?.get(cell.migrationHash)?.pathHighlight;
|
|
2156
|
+
laneOverride = pathLaneFor(cellHighlight);
|
|
2157
|
+
arrowOverride = pathArrowFor(cellHighlight);
|
|
2158
|
+
} else if (cell.kind === "node" && (cell.arcTee === true || cell.arcLand === true)) {
|
|
2159
|
+
const arcColumn = cellColors.connector[column] ?? 0;
|
|
2160
|
+
const arcCell = row.cells[arcColumn];
|
|
2161
|
+
const arcHash = arcCell !== void 0 && "migrationHash" in arcCell ? arcCell.migrationHash : void 0;
|
|
2162
|
+
if (arcHash !== void 0) {
|
|
2163
|
+
const arcHighlight = opts.edgeAnnotationsByHash?.get(arcHash)?.pathHighlight;
|
|
2164
|
+
arcLaneOverride = pathLaneFor(arcHighlight);
|
|
2165
|
+
}
|
|
2166
|
+
} else if (cell.kind !== "node" && cell.kind !== "empty") {
|
|
2167
|
+
const hashForCell = cell.kind === "arc-crossing" && "arcMigrationHash" in cell && cell.arcMigrationHash !== void 0 ? cell.arcMigrationHash : "migrationHash" in cell && cell.migrationHash !== void 0 ? cell.migrationHash : void 0;
|
|
2168
|
+
if (hashForCell !== void 0) {
|
|
2169
|
+
const cellHighlight = opts.edgeAnnotationsByHash?.get(hashForCell)?.pathHighlight;
|
|
2170
|
+
laneOverride = pathLaneFor(cellHighlight);
|
|
2171
|
+
arrowOverride = pathArrowFor(cellHighlight);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
return renderCellPair(cell, column, cellColors, opts.colorize, style, palette, laneOverride, arrowOverride, arcLaneOverride);
|
|
2176
|
+
}).join("");
|
|
2177
|
+
let laneSpan = row.cells.length;
|
|
2178
|
+
if (row.kind === "node") if ((row.contractHash ?? EMPTY_CONTRACT_HASH) === EMPTY_CONTRACT_HASH) laneSpan = 1;
|
|
2179
|
+
else {
|
|
2180
|
+
let lastActiveColumn = -1;
|
|
2181
|
+
for (let column = row.cells.length - 1; column >= 0; column--) if (row.cells[column]?.kind !== "empty") {
|
|
2182
|
+
lastActiveColumn = column;
|
|
2183
|
+
break;
|
|
2184
|
+
}
|
|
2185
|
+
laneSpan = lastActiveColumn >= 0 ? lastActiveColumn + 1 : 1;
|
|
2186
|
+
}
|
|
2187
|
+
const labelColumn = row.kind === "edge" ? maxEdgePrefixWidth : wideLabelColumn !== void 0 && (nodeHasArcDecoration(row) || row.contractHash !== void 0) ? wideLabelColumn : laneSpan * 2 + LABEL_GAP;
|
|
2188
|
+
if (row.kind === "edge" && row.edge?.from === EMPTY_CONTRACT_HASH && (row.laneIndex ?? 0) === 0) gutter = row.cells.slice(0, 1).map((cell, column) => renderCellPair(cell, column, cellColors, opts.colorize, style, palette, rowLaneOverride, rowArrowOverride)).join("");
|
|
2189
|
+
else if (row.kind === "node" && laneSpan < row.cells.length && !nodeHasArcDecoration(row)) gutter = row.cells.slice(0, laneSpan).map((cell, column) => {
|
|
2190
|
+
let cellLaneOverride = rowLaneOverride;
|
|
2191
|
+
let cellArrowOverride = rowArrowOverride;
|
|
2192
|
+
if (pathHighlightActive && cell.kind !== "node" && cell.kind !== "empty") {
|
|
2193
|
+
const hashForCell = cell.kind === "arc-crossing" && "arcMigrationHash" in cell && cell.arcMigrationHash !== void 0 ? cell.arcMigrationHash : "migrationHash" in cell && cell.migrationHash !== void 0 ? cell.migrationHash : void 0;
|
|
2194
|
+
if (hashForCell !== void 0) {
|
|
2195
|
+
const cellHighlight = opts.edgeAnnotationsByHash?.get(hashForCell)?.pathHighlight;
|
|
2196
|
+
cellLaneOverride = pathLaneFor(cellHighlight);
|
|
2197
|
+
cellArrowOverride = pathArrowFor(cellHighlight);
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
return renderCellPair(cell, column, cellColors, opts.colorize, style, palette, cellLaneOverride, cellArrowOverride);
|
|
2201
|
+
}).join("");
|
|
2202
|
+
else if (gutter.length < laneSpan * 2) gutter = gutter.padEnd(laneSpan * 2, " ");
|
|
2203
|
+
const dirNameWidth = row.kind === "edge" ? edgeDirNameWidth : rowDirNameWidth(labelColumn, maxDirNameLen, dirNameGap);
|
|
2204
|
+
const gutterPad = padVisible(gutter, labelColumn);
|
|
2205
|
+
if (row.kind === "node") {
|
|
2206
|
+
const contractHash = row.contractHash ?? EMPTY_CONTRACT_HASH;
|
|
2207
|
+
if (contractHash === EMPTY_CONTRACT_HASH) {
|
|
2208
|
+
const trailingLanes = row.cells.slice(1).map((cell, offset) => {
|
|
2209
|
+
let cellLaneOverride = rowLaneOverride;
|
|
2210
|
+
let cellArrowOverride = rowArrowOverride;
|
|
2211
|
+
if (pathHighlightActive && cell.kind !== "node" && cell.kind !== "empty") {
|
|
2212
|
+
const hashForCell = cell.kind === "arc-crossing" && "arcMigrationHash" in cell && cell.arcMigrationHash !== void 0 ? cell.arcMigrationHash : "migrationHash" in cell && cell.migrationHash !== void 0 ? cell.migrationHash : void 0;
|
|
2213
|
+
if (hashForCell !== void 0) {
|
|
2214
|
+
const cellHighlight = opts.edgeAnnotationsByHash?.get(hashForCell)?.pathHighlight;
|
|
2215
|
+
cellLaneOverride = pathLaneFor(cellHighlight);
|
|
2216
|
+
cellArrowOverride = pathArrowFor(cellHighlight);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
return renderCellPair(cell, offset + 1, cellColors, opts.colorize, style, palette, cellLaneOverride, cellArrowOverride);
|
|
2220
|
+
}).join("");
|
|
2221
|
+
const emptyGutter = palette.emptySource.padEnd(2, " ") + trailingLanes;
|
|
2222
|
+
const overlays = overlayNamesForContract(contractHash, opts);
|
|
2223
|
+
if (overlays.markers.length === 0 && overlays.refs.length === 0) {
|
|
2224
|
+
lines.push(trimTrailingWhitespace(emptyGutter));
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
const overlay = formatContractNodeOverlays(style, overlays.markers, overlays.refs);
|
|
2228
|
+
lines.push(trimTrailingWhitespace(`${emptyGutter}${" ".repeat(LABEL_GAP)}${overlay}`));
|
|
2229
|
+
continue;
|
|
2230
|
+
}
|
|
2231
|
+
const hashText = (rowStyle?.hashOverride ?? style.sourceHash)(abbreviateHash(contractHash, hashLength, palette.emptySource));
|
|
2232
|
+
const overlays = overlayNamesForContract(contractHash, opts);
|
|
2233
|
+
const hasOverlays = overlays.markers.length > 0 || overlays.refs.length > 0;
|
|
2234
|
+
const overlayPad = hasOverlays ? " ".repeat(LABEL_GAP) : "";
|
|
2235
|
+
const overlay = hasOverlays ? formatContractNodeOverlays(style, overlays.markers, overlays.refs) : "";
|
|
2236
|
+
lines.push(trimTrailingWhitespace(`${gutterPad}${hashText}${overlayPad}${overlay}`));
|
|
2237
|
+
continue;
|
|
2238
|
+
}
|
|
2239
|
+
const edge = row.edge;
|
|
2240
|
+
if (edge === void 0) continue;
|
|
2241
|
+
const dirNamePadding = " ".repeat(Math.max(0, dirNameWidth - edge.dirName.length));
|
|
2242
|
+
const laneIndex = row.laneIndex ?? 0;
|
|
2243
|
+
const edgeGutterPad = padVisible(gutter, labelColumn);
|
|
2244
|
+
let dirName;
|
|
2245
|
+
if (rowStyle !== void 0) dirName = `${(rowStyle.dirName ?? style.dirName)(edge.dirName)}${dirNamePadding}`;
|
|
2246
|
+
else dirName = `${(opts.colorize && laneIndex > 0 ? (text) => forcedBold(laneColorForColumn(laneIndex)(text)) : style.dirName)(edge.dirName)}${dirNamePadding}`;
|
|
2247
|
+
const hashColumnOverride = rowStyle?.hashOverride;
|
|
2248
|
+
const hashColumn = formatEdgeHashColumn(edge, style, hashLength, palette, hashColumnOverride);
|
|
2249
|
+
const annotationSuffix = formatEdgeAnnotationSuffix(edge.migrationHash, opts, style);
|
|
2250
|
+
lines.push(trimTrailingWhitespace(`${edgeGutterPad}${dirName}${hashColumn}${annotationSuffix}`));
|
|
2251
|
+
}
|
|
2252
|
+
return lines.join("\n");
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* Format a single on-path migration row for the `migrate --show` run-list.
|
|
2256
|
+
*
|
|
2257
|
+
* Uses the SAME styling as the tree renderer's on-path rows (PATH_HIGHLIGHT_STYLES.onPath)
|
|
2258
|
+
* so the run-list and graph tree are byte-for-byte identical in their name/hash columns.
|
|
2259
|
+
* The gutter is omitted — the list has no graph structure.
|
|
2260
|
+
*
|
|
2261
|
+
* This is the SINGLE code path for on-path row styling shared by both the graph tree
|
|
2262
|
+
* and the "Will run, in order:" list. To change the on-path colour, edit PATH_HIGHLIGHT_STYLES.
|
|
2263
|
+
*/
|
|
2264
|
+
function formatOnPathMigrationRow(dirName, from, to, dirNameWidth, colorize, glyphMode) {
|
|
2265
|
+
const palette = paletteFor(glyphMode);
|
|
2266
|
+
const style = createAnsiMigrationListStyler({ useColor: colorize });
|
|
2267
|
+
const styledDirName = `${PATH_HIGHLIGHT_STYLES.onPath(style, colorize).dirName(dirName)}${" ".repeat(Math.max(0, dirNameWidth - dirName.length))}`;
|
|
2268
|
+
const hashLength = 7;
|
|
2269
|
+
const emptySource = palette.emptySource;
|
|
2270
|
+
const fromAbbr = from === EMPTY_CONTRACT_HASH ? padFromHashColumn(style.glyph(emptySource), hashLength) : padFromHashColumn(style.sourceHash(abbreviateHashShort(from, hashLength)), hashLength);
|
|
2271
|
+
const toAbbr = to === EMPTY_CONTRACT_HASH ? style.glyph(emptySource) : style.destHash(abbreviateHashShort(to, hashLength));
|
|
2272
|
+
return `${styledDirName} ${fromAbbr} ${style.glyph(palette.forwardArrow)} ${toAbbr}`;
|
|
2273
|
+
}
|
|
2274
|
+
function abbreviateHashShort(hash, length) {
|
|
2275
|
+
return (hash.startsWith("sha256:") ? hash.slice(7) : hash).slice(0, length);
|
|
2276
|
+
}
|
|
2277
|
+
function formatLegendExampleMarkers(colorize) {
|
|
2278
|
+
if (!colorize) return "@contract @db";
|
|
2279
|
+
const sigil = green("@");
|
|
2280
|
+
return `${sigil + bold(green("contract"))} ${sigil}${green("db")}`;
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* A compact key for the tree visual language: the contract node glyph, the
|
|
2284
|
+
* in-lane direction arrows, the empty baseline, the system-marker `<…>` and
|
|
2285
|
+
* user-ref `(…)` bracket conventions (two illustrative example lines), and a
|
|
2286
|
+
* worked sample of the data-column `from → to` migration hash arrow.
|
|
2287
|
+
*
|
|
2288
|
+
* Honors the same glyph palette (unicode vs ASCII) and `colorize` gate as the
|
|
2289
|
+
* tree renderer, so the key matches whatever the graph itself drew and stays
|
|
2290
|
+
* pipe-safe (zero ANSI when color is off). The caller adds the trailing blank
|
|
2291
|
+
* line that separates this stderr key from the tree on stdout.
|
|
2292
|
+
*/
|
|
2293
|
+
function renderMigrationGraphLegend(opts) {
|
|
2294
|
+
const palette = paletteFor(opts.glyphMode ?? "unicode");
|
|
2295
|
+
const style = createAnsiMigrationListStyler({ useColor: opts.colorize });
|
|
2296
|
+
const node = palette.node.trimEnd();
|
|
2297
|
+
const sampleArrow = `${style.sourceHash("aaaaaa")} ${style.glyph(palette.forwardArrow)} ${style.destHash("bbbbbb")}`;
|
|
2298
|
+
const statusGlyphs = overlayStatusGlyphs(opts.glyphMode ?? "unicode");
|
|
2299
|
+
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")}`;
|
|
2300
|
+
const exampleMarkers = formatLegendExampleMarkers(opts.colorize);
|
|
2301
|
+
const exampleRefs = opts.colorize ? style.refs(["prod", "staging"]) : "(prod, staging)";
|
|
2302
|
+
return [
|
|
2303
|
+
"Legend:",
|
|
2304
|
+
` ${style.kind(node)} ${style.summary("contract")} ${style.kind(palette.edgeArrow.forward)} ${style.summary("forward")} ${style.kind(palette.edgeArrow.rollback)} ${style.summary("rollback")}`,
|
|
2305
|
+
` ${style.kind(palette.edgeArrow.self)} ${style.summary("migration without schema change")}`,
|
|
2306
|
+
appliedPending,
|
|
2307
|
+
` ${style.kind(palette.emptySource)} ${style.summary("empty database (baseline)")}`,
|
|
2308
|
+
` ${exampleMarkers} ${style.summary("reserved markers — also typeable as --from/--to tokens")}`,
|
|
2309
|
+
` ${exampleRefs} ${style.summary("user-defined refs")}`,
|
|
2310
|
+
` ${sampleArrow} ${style.summary("migration from contract aaaaaa to bbbbbb")}`
|
|
2311
|
+
].join("\n");
|
|
2312
|
+
}
|
|
2313
|
+
//#endregion
|
|
2314
|
+
//#region src/utils/formatters/migration-graph-space-render.ts
|
|
2315
|
+
function mergeMigrationEdgeAnnotations(listOverlay, statusOverlay) {
|
|
2316
|
+
const merged = /* @__PURE__ */ new Map();
|
|
2317
|
+
for (const [migrationHash, listAnnotation] of listOverlay) {
|
|
2318
|
+
const statusAnnotation = statusOverlay.get(migrationHash);
|
|
2319
|
+
merged.set(migrationHash, {
|
|
2320
|
+
...listAnnotation,
|
|
2321
|
+
...statusAnnotation?.status !== void 0 ? { status: statusAnnotation.status } : {}
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
return merged;
|
|
2325
|
+
}
|
|
2326
|
+
function computeGlobalMaxEdgeTreePrefixWidth(inputs) {
|
|
2327
|
+
let globalMax = 0;
|
|
2328
|
+
for (const input of inputs) {
|
|
2329
|
+
const layout = buildMigrationGraphLayout(buildMigrationGraphRows(input.graph, { contractHash: input.liveContractHash }));
|
|
2330
|
+
globalMax = Math.max(globalMax, computeMaxEdgeTreePrefixWidthForLayout(layout));
|
|
2331
|
+
}
|
|
2332
|
+
return globalMax;
|
|
2333
|
+
}
|
|
2334
|
+
function computeGlobalMaxDirNameWidth(inputs) {
|
|
2335
|
+
let globalMax = 0;
|
|
2336
|
+
for (const input of inputs) {
|
|
2337
|
+
const layout = buildMigrationGraphLayout(buildMigrationGraphRows(input.graph, { contractHash: input.liveContractHash }));
|
|
2338
|
+
globalMax = Math.max(globalMax, computeMaxDirNameLengthForLayout(layout));
|
|
2339
|
+
}
|
|
2340
|
+
return globalMax;
|
|
2341
|
+
}
|
|
2342
|
+
function renderMigrationGraphSpaceTreeInternal(input) {
|
|
2343
|
+
const appSpace = input.isAppSpace !== false;
|
|
2344
|
+
const layout = buildMigrationGraphLayout(buildMigrationGraphRows(input.graph, { ...appSpace ? { contractHash: input.liveContractHash } : {} }));
|
|
2345
|
+
const listOverlay = buildEdgeAnnotationsByHashFromListEntries(input.migrations);
|
|
2346
|
+
const edgeAnnotationsByHash = input.statusOverlayByHash === void 0 ? listOverlay : mergeMigrationEdgeAnnotations(listOverlay, input.statusOverlayByHash);
|
|
2347
|
+
return renderMigrationGraphTree(layout, {
|
|
2348
|
+
refsByHash: input.refsByHash ?? buildRefsByHashFromListEntries(input.migrations),
|
|
2349
|
+
contractHash: input.liveContractHash,
|
|
2350
|
+
isAppSpace: appSpace,
|
|
2351
|
+
edgeAnnotationsByHash,
|
|
2352
|
+
colorize: input.colorize,
|
|
2353
|
+
glyphMode: input.glyphMode,
|
|
2354
|
+
...input.dbHash !== void 0 ? { dbHash: input.dbHash } : {},
|
|
2355
|
+
...input.styler !== void 0 ? { styler: input.styler } : {},
|
|
2356
|
+
...input.globalMaxEdgeTreePrefixWidth !== void 0 ? { globalMaxEdgeTreePrefixWidth: input.globalMaxEdgeTreePrefixWidth } : {},
|
|
2357
|
+
...input.globalMaxDirNameWidth !== void 0 ? { globalMaxDirNameWidth: input.globalMaxDirNameWidth } : {}
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
function renderMigrationGraphSpaceTree(input) {
|
|
2361
|
+
return renderMigrationGraphSpaceTreeInternal(input);
|
|
2362
|
+
}
|
|
2363
|
+
function indentMigrationGraphTreeBlock(treeOutput, indent) {
|
|
2364
|
+
if (treeOutput.length === 0) return treeOutput;
|
|
2365
|
+
return treeOutput.split("\n").map((line) => line.length === 0 ? line : `${indent}${line}`).join("\n");
|
|
2366
|
+
}
|
|
2367
|
+
//#endregion
|
|
2368
|
+
export { buildMigrationGraphLayout as _, computeMaxDirNameLengthForLayout as a, renderMigrationGraphLegend as c, IDENTITY_MIGRATION_LIST_STYLER as d, renderMigrationListWithStyle as f, buildMigrationGraphRows as g, migrationListForwardArrow as h, renderMigrationGraphSpaceTree as i, renderMigrationGraphTree as l, migrationListEmptySource as m, computeGlobalMaxEdgeTreePrefixWidth as n, computeMaxEdgeTreePrefixWidthForLayout as o, abbreviateContractHash as p, indentMigrationGraphTreeBlock as r, formatOnPathMigrationRow as s, computeGlobalMaxDirNameWidth as t, createAnsiMigrationListStyler as u };
|
|
2369
|
+
|
|
2370
|
+
//# sourceMappingURL=migration-graph-space-render-Cpg0ql8v.mjs.map
|