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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.mjs +180 -163
  3. package/dist/cli.mjs.map +1 -1
  4. package/dist/{client-KgJorIvG.mjs → client-CJzuo5wX.mjs} +222 -107
  5. package/dist/client-CJzuo5wX.mjs.map +1 -0
  6. package/dist/{command-helpers-Bbw1GbwL.mjs → command-helpers-DGMvGBeX.mjs} +318 -25
  7. package/dist/command-helpers-DGMvGBeX.mjs.map +1 -0
  8. package/dist/commands/contract-emit.d.mts.map +1 -1
  9. package/dist/commands/contract-emit.mjs +1 -1
  10. package/dist/commands/contract-infer.d.mts.map +1 -1
  11. package/dist/commands/contract-infer.mjs +1 -1
  12. package/dist/commands/db-init.d.mts.map +1 -1
  13. package/dist/commands/db-init.mjs +4 -5
  14. package/dist/commands/db-init.mjs.map +1 -1
  15. package/dist/commands/db-schema.d.mts.map +1 -1
  16. package/dist/commands/db-schema.mjs +3 -3
  17. package/dist/commands/db-schema.mjs.map +1 -1
  18. package/dist/commands/db-sign.d.mts.map +1 -1
  19. package/dist/commands/db-sign.mjs +6 -6
  20. package/dist/commands/db-sign.mjs.map +1 -1
  21. package/dist/commands/db-update.d.mts.map +1 -1
  22. package/dist/commands/db-update.mjs +10 -7
  23. package/dist/commands/db-update.mjs.map +1 -1
  24. package/dist/commands/db-verify.d.mts.map +1 -1
  25. package/dist/commands/db-verify.mjs +1 -1
  26. package/dist/commands/migrate.d.mts +37 -3
  27. package/dist/commands/migrate.d.mts.map +1 -1
  28. package/dist/commands/migrate.mjs +298 -12
  29. package/dist/commands/migrate.mjs.map +1 -1
  30. package/dist/commands/migration-check.d.mts +55 -13
  31. package/dist/commands/migration-check.d.mts.map +1 -1
  32. package/dist/commands/migration-check.mjs +3 -2
  33. package/dist/commands/migration-graph.d.mts +17 -8
  34. package/dist/commands/migration-graph.d.mts.map +1 -1
  35. package/dist/commands/migration-graph.mjs +185 -2
  36. package/dist/commands/migration-graph.mjs.map +1 -0
  37. package/dist/commands/migration-list.d.mts +26 -27
  38. package/dist/commands/migration-list.d.mts.map +1 -1
  39. package/dist/commands/migration-list.mjs +2 -190
  40. package/dist/commands/migration-log.d.mts +9 -19
  41. package/dist/commands/migration-log.d.mts.map +1 -1
  42. package/dist/commands/migration-log.mjs +1 -137
  43. package/dist/commands/migration-new.d.mts.map +1 -1
  44. package/dist/commands/migration-new.mjs +6 -5
  45. package/dist/commands/migration-new.mjs.map +1 -1
  46. package/dist/commands/migration-plan.d.mts +1 -1
  47. package/dist/commands/migration-plan.d.mts.map +1 -1
  48. package/dist/commands/migration-plan.mjs +1 -1
  49. package/dist/commands/migration-show.d.mts +17 -21
  50. package/dist/commands/migration-show.d.mts.map +1 -1
  51. package/dist/commands/migration-show.mjs +24 -36
  52. package/dist/commands/migration-show.mjs.map +1 -1
  53. package/dist/commands/migration-status.d.mts +42 -144
  54. package/dist/commands/migration-status.d.mts.map +1 -1
  55. package/dist/commands/migration-status.mjs +3 -759
  56. package/dist/commands/ref.d.mts +1 -1
  57. package/dist/commands/ref.d.mts.map +1 -1
  58. package/dist/commands/ref.mjs +4 -4
  59. package/dist/commands/ref.mjs.map +1 -1
  60. package/dist/commands/telemetry/index.d.mts +7 -0
  61. package/dist/commands/telemetry/index.d.mts.map +1 -0
  62. package/dist/commands/telemetry/index.mjs +2 -0
  63. package/dist/{config-loader-B6sJjXTv.mjs → config-loader-p9JMrekQ.mjs} +1 -1
  64. package/dist/{config-loader-B6sJjXTv.mjs.map → config-loader-p9JMrekQ.mjs.map} +1 -1
  65. package/dist/config-loader.mjs +1 -1
  66. package/dist/{contract-at-errors-BxP-TOMl.mjs → contract-at-errors-CFXsstzm.mjs} +2 -2
  67. package/dist/{contract-at-errors-BxP-TOMl.mjs.map → contract-at-errors-CFXsstzm.mjs.map} +1 -1
  68. package/dist/{contract-emit-DxcGl4Uq.mjs → contract-emit-B_qriF8B.mjs} +5 -5
  69. package/dist/{contract-emit-DxcGl4Uq.mjs.map → contract-emit-B_qriF8B.mjs.map} +1 -1
  70. package/dist/{contract-emit-D-4jrNve.mjs → contract-emit-C8HmtboH.mjs} +12 -7
  71. package/dist/contract-emit-C8HmtboH.mjs.map +1 -0
  72. package/dist/{contract-enrichment-a0V5Y_mL.mjs → contract-enrichment-gn9sWbPw.mjs} +1 -1
  73. package/dist/{contract-enrichment-a0V5Y_mL.mjs.map → contract-enrichment-gn9sWbPw.mjs.map} +1 -1
  74. package/dist/{contract-infer-D8uEbJuu.mjs → contract-infer-BYT_ra_U.mjs} +5 -5
  75. package/dist/contract-infer-BYT_ra_U.mjs.map +1 -0
  76. package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs → contract-space-aggregate-loader-ClI1KN6d.mjs} +5 -5
  77. package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs.map → contract-space-aggregate-loader-ClI1KN6d.mjs.map} +1 -1
  78. package/dist/{db-verify-v_vUKXTU.mjs → db-verify-C24FKhb7.mjs} +6 -6
  79. package/dist/{db-verify-v_vUKXTU.mjs.map → db-verify-C24FKhb7.mjs.map} +1 -1
  80. package/dist/exports/control-api.d.mts +5 -3
  81. package/dist/exports/control-api.d.mts.map +1 -1
  82. package/dist/exports/control-api.mjs +3 -3
  83. package/dist/exports/index.mjs +1 -1
  84. package/dist/exports/index.mjs.map +1 -1
  85. package/dist/exports/init-output.d.mts +1 -3
  86. package/dist/exports/init-output.d.mts.map +1 -1
  87. package/dist/exports/init-output.mjs +1 -1
  88. package/dist/{extension-pack-inputs-IDvjRCi3.mjs → extension-pack-inputs-1ySHqxKG.mjs} +1 -1
  89. package/dist/{extension-pack-inputs-IDvjRCi3.mjs.map → extension-pack-inputs-1ySHqxKG.mjs.map} +1 -1
  90. package/dist/{framework-components-fYXjz_in.mjs → framework-components-YVQHhPH7.mjs} +2 -2
  91. package/dist/{framework-components-fYXjz_in.mjs.map → framework-components-YVQHhPH7.mjs.map} +1 -1
  92. package/dist/{global-flags-DEHjV8_s.d.mts → global-flags-BpoOYtNZ.d.mts} +1 -1
  93. package/dist/{global-flags-DEHjV8_s.d.mts.map → global-flags-BpoOYtNZ.d.mts.map} +1 -1
  94. package/dist/{init-Cv9UzWL5.mjs → init-0HwB-Vh8.mjs} +5 -58
  95. package/dist/init-0HwB-Vh8.mjs.map +1 -0
  96. package/dist/{inspect-live-schema-C6ohV_oQ.mjs → inspect-live-schema-DF6IwcDl.mjs} +7 -5
  97. package/dist/inspect-live-schema-DF6IwcDl.mjs.map +1 -0
  98. package/dist/migration-check-VwM8xCZV.mjs +574 -0
  99. package/dist/migration-check-VwM8xCZV.mjs.map +1 -0
  100. package/dist/migration-cli.mjs +1 -1
  101. package/dist/migration-cli.mjs.map +1 -1
  102. package/dist/{migration-command-scaffold-CjvwO6at.mjs → migration-command-scaffold-DA-Lhx6o.mjs} +5 -5
  103. package/dist/{migration-command-scaffold-CjvwO6at.mjs.map → migration-command-scaffold-DA-Lhx6o.mjs.map} +1 -1
  104. package/dist/migration-graph-command-render-CEez7YUK.mjs +1960 -0
  105. package/dist/migration-graph-command-render-CEez7YUK.mjs.map +1 -0
  106. package/dist/migration-list-DlJJ_38Z.mjs +230 -0
  107. package/dist/migration-list-DlJJ_38Z.mjs.map +1 -0
  108. package/dist/migration-log-CG0qQAFm.mjs +222 -0
  109. package/dist/migration-log-CG0qQAFm.mjs.map +1 -0
  110. package/dist/migration-path-target-Ce6OZImp.mjs +38 -0
  111. package/dist/migration-path-target-Ce6OZImp.mjs.map +1 -0
  112. package/dist/{migration-plan-9DJ7q7_z.mjs → migration-plan-z5Ing-TD.mjs} +9 -8
  113. package/dist/migration-plan-z5Ing-TD.mjs.map +1 -0
  114. package/dist/migration-status-CD-LC2Ip.mjs +447 -0
  115. package/dist/migration-status-CD-LC2Ip.mjs.map +1 -0
  116. package/dist/{output-B60Gw5fu.mjs → output-mEQ74_nd.mjs} +1 -1
  117. package/dist/{output-B60Gw5fu.mjs.map → output-mEQ74_nd.mjs.map} +1 -1
  118. package/dist/{progress-adapter-C644QK8l.mjs → progress-adapter-CjAeTxY_.mjs} +1 -1
  119. package/dist/{progress-adapter-C644QK8l.mjs.map → progress-adapter-CjAeTxY_.mjs.map} +1 -1
  120. package/dist/{ref-advancement-DUZqsue6.mjs → ref-advancement-BkXlikCA.mjs} +1 -1
  121. package/dist/{ref-advancement-DUZqsue6.mjs.map → ref-advancement-BkXlikCA.mjs.map} +1 -1
  122. package/dist/schemas-CeGMYFYX.d.mts +191 -0
  123. package/dist/schemas-CeGMYFYX.d.mts.map +1 -0
  124. package/dist/schemas-KhXMzNA_.mjs +112 -0
  125. package/dist/schemas-KhXMzNA_.mjs.map +1 -0
  126. package/dist/telemetry-BIM4beEO.mjs +122 -0
  127. package/dist/telemetry-BIM4beEO.mjs.map +1 -0
  128. package/dist/{terminal-ui-5Y6mrg93.d.mts → terminal-ui-DGRNFWna.d.mts} +1 -1
  129. package/dist/terminal-ui-DGRNFWna.d.mts.map +1 -0
  130. package/dist/{types-Dt_SfqFm.d.mts → types-C_tYiJYx.d.mts} +53 -31
  131. package/dist/types-C_tYiJYx.d.mts.map +1 -0
  132. package/dist/{verify-DCA9Sldu.mjs → verify-DcOYZ1tH.mjs} +2 -2
  133. package/dist/{verify-DCA9Sldu.mjs.map → verify-DcOYZ1tH.mjs.map} +1 -1
  134. package/package.json +26 -22
  135. package/src/cli.ts +5 -0
  136. package/src/commands/contract-infer.ts +2 -2
  137. package/src/commands/db-update.ts +7 -1
  138. package/src/commands/init/index.ts +6 -35
  139. package/src/commands/init/init.ts +1 -14
  140. package/src/commands/init/inputs.ts +0 -75
  141. package/src/commands/inspect-live-schema.ts +10 -0
  142. package/src/commands/json/schemas.ts +195 -0
  143. package/src/commands/migrate.ts +527 -8
  144. package/src/commands/migration-check.ts +469 -134
  145. package/src/commands/migration-graph.ts +164 -91
  146. package/src/commands/migration-list.ts +72 -39
  147. package/src/commands/migration-log.ts +52 -102
  148. package/src/commands/migration-new.ts +2 -1
  149. package/src/commands/migration-plan.ts +2 -1
  150. package/src/commands/migration-show.ts +31 -66
  151. package/src/commands/migration-status-overlay.ts +61 -0
  152. package/src/commands/migration-status.ts +458 -1066
  153. package/src/commands/telemetry/index.ts +107 -0
  154. package/src/commands/telemetry/status.ts +67 -0
  155. package/src/control-api/client.ts +70 -9
  156. package/src/control-api/operations/contract-emit.ts +22 -2
  157. package/src/control-api/operations/db-init.ts +6 -3
  158. package/src/control-api/operations/{db-apply.ts → db-run.ts} +55 -14
  159. package/src/control-api/operations/db-update.ts +7 -4
  160. package/src/control-api/operations/db-verify.ts +15 -5
  161. package/src/control-api/operations/{migration-apply.ts → migrate.ts} +181 -80
  162. package/src/control-api/operations/{apply.ts → run-migration.ts} +33 -27
  163. package/src/control-api/types.ts +56 -29
  164. package/src/utils/cli-errors.ts +70 -2
  165. package/src/utils/formatters/errors.ts +11 -0
  166. package/src/utils/formatters/migration-graph-command-render.ts +239 -0
  167. package/src/utils/formatters/migration-graph-grid-layout.ts +1134 -0
  168. package/src/utils/formatters/migration-graph-labels.ts +408 -0
  169. package/src/utils/formatters/migration-graph-model.ts +103 -0
  170. package/src/utils/formatters/migration-graph-occlusion-render.ts +258 -0
  171. package/src/utils/formatters/migration-graph-rows.ts +128 -15
  172. package/src/utils/formatters/migration-graph-space-render.ts +188 -0
  173. package/src/utils/formatters/migration-list-data-column.ts +4 -91
  174. package/src/utils/formatters/migration-list-graph-topology.ts +72 -94
  175. package/src/utils/formatters/migration-list-render.ts +135 -71
  176. package/src/utils/formatters/migration-list-styler.ts +46 -5
  177. package/src/utils/formatters/migration-list-types.ts +5 -21
  178. package/src/utils/formatters/migration-log-table.ts +205 -0
  179. package/src/utils/formatters/migrations.ts +33 -11
  180. package/src/utils/global-flags.ts +35 -0
  181. package/src/utils/integrity-violation-to-check-failure.ts +28 -19
  182. package/src/utils/legend.ts +38 -0
  183. package/src/utils/migration-path-target.ts +60 -0
  184. package/src/utils/telemetry.ts +68 -32
  185. package/dist/client-KgJorIvG.mjs.map +0 -1
  186. package/dist/command-helpers-Bbw1GbwL.mjs.map +0 -1
  187. package/dist/commands/migration-list.mjs.map +0 -1
  188. package/dist/commands/migration-log.mjs.map +0 -1
  189. package/dist/commands/migration-status.mjs.map +0 -1
  190. package/dist/contract-emit-D-4jrNve.mjs.map +0 -1
  191. package/dist/contract-infer-D8uEbJuu.mjs.map +0 -1
  192. package/dist/graph-render-rFAqZujX.mjs +0 -1081
  193. package/dist/graph-render-rFAqZujX.mjs.map +0 -1
  194. package/dist/init-Cv9UzWL5.mjs.map +0 -1
  195. package/dist/inspect-live-schema-C6ohV_oQ.mjs.map +0 -1
  196. package/dist/migration-check-BiBJoYYW.mjs +0 -341
  197. package/dist/migration-check-BiBJoYYW.mjs.map +0 -1
  198. package/dist/migration-graph-D7DVUElV.mjs +0 -1232
  199. package/dist/migration-graph-D7DVUElV.mjs.map +0 -1
  200. package/dist/migration-list-styler-BRwF4-gy.mjs +0 -399
  201. package/dist/migration-list-styler-BRwF4-gy.mjs.map +0 -1
  202. package/dist/migration-plan-9DJ7q7_z.mjs.map +0 -1
  203. package/dist/migration-types-D2FW63pr.d.mts +0 -15
  204. package/dist/migration-types-D2FW63pr.d.mts.map +0 -1
  205. package/dist/migrations-Cv2jxNNK.mjs +0 -228
  206. package/dist/migrations-Cv2jxNNK.mjs.map +0 -1
  207. package/dist/terminal-ui-5Y6mrg93.d.mts.map +0 -1
  208. package/dist/types-Dt_SfqFm.d.mts.map +0 -1
  209. package/src/utils/formatters/graph-migration-mapper.ts +0 -235
  210. package/src/utils/formatters/graph-render.ts +0 -1323
  211. package/src/utils/formatters/graph-types.ts +0 -120
  212. package/src/utils/formatters/migration-graph-layout.ts +0 -1119
  213. package/src/utils/formatters/migration-graph-tree-render.ts +0 -459
@@ -1,1119 +0,0 @@
1
- import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
2
- import type { ClassifiedEdge, MigrationGraphRowModel } from './migration-graph-rows';
3
- import type { MigrationEdgeKind } from './migration-list-graph-topology';
4
-
5
- export type EdgeAdjacency = 'adjacent' | 'node-skipping-forward' | 'node-skipping-rollback';
6
-
7
- export type StructuralCell =
8
- | { readonly kind: 'empty' }
9
- | {
10
- readonly kind: 'node';
11
- readonly contractHash: string;
12
- readonly arcTee?: boolean;
13
- readonly arcLand?: boolean;
14
- }
15
- | { readonly kind: 'vertical-pass' }
16
- | { readonly kind: 'horizontal-pass' }
17
- | { readonly kind: 'branch-tee' }
18
- | { readonly kind: 'branch-corner' }
19
- | { readonly kind: 'merge-tee' }
20
- | { readonly kind: 'merge-corner' }
21
- | { readonly kind: 'arc-branch-corner' }
22
- | { readonly kind: 'arc-branch-tee' }
23
- | { readonly kind: 'arc-land-corner' }
24
- | { readonly kind: 'arc-crossing' }
25
- | { readonly kind: 'arc-land-bridge' }
26
- | {
27
- readonly kind: 'edge-lane';
28
- readonly migrationHash: string;
29
- readonly edgeKind: MigrationEdgeKind;
30
- readonly ownsLabel: boolean;
31
- readonly adjacency: EdgeAdjacency;
32
- };
33
-
34
- export type GridRowKind =
35
- | 'node'
36
- | 'edge'
37
- | 'branch-connector'
38
- | 'merge-connector'
39
- | 'component-separator';
40
-
41
- export interface MigrationGraphGridRow {
42
- readonly kind: GridRowKind;
43
- readonly contractHash?: string;
44
- readonly edge?: ClassifiedEdge;
45
- readonly laneIndex?: number;
46
- readonly passThroughLanes?: readonly number[];
47
- readonly startLane?: number;
48
- readonly endLane?: number;
49
- readonly branchCount?: number;
50
- readonly convergenceProducer?: boolean;
51
- readonly cells: readonly StructuralCell[];
52
- }
53
-
54
- export interface MigrationGraphGridModel {
55
- readonly rows: readonly MigrationGraphGridRow[];
56
- readonly nodeColumn: ReadonlyMap<string, number>;
57
- readonly edgeColumn: ReadonlyMap<string, number>;
58
- }
59
-
60
- // ---------------------------------------------------------------------------
61
- // Edge bucketing helpers
62
- // ---------------------------------------------------------------------------
63
-
64
- function forwardEdges(edges: readonly ClassifiedEdge[]): ClassifiedEdge[] {
65
- return edges.filter((e) => e.kind === 'forward');
66
- }
67
-
68
- function buildForwardProducersByTo(
69
- edges: readonly ClassifiedEdge[],
70
- ): Map<string, ClassifiedEdge[]> {
71
- const byTo = new Map<string, ClassifiedEdge[]>();
72
- for (const edge of edges) {
73
- if (edge.kind !== 'forward') continue;
74
- const bucket = byTo.get(edge.to);
75
- if (bucket) bucket.push(edge);
76
- else byTo.set(edge.to, [edge]);
77
- }
78
- return byTo;
79
- }
80
-
81
- function buildForwardOutDegree(edges: readonly ClassifiedEdge[]): Map<string, number> {
82
- const out = new Map<string, number>();
83
- for (const edge of edges) {
84
- if (edge.kind !== 'forward' || edge.from === edge.to) continue;
85
- out.set(edge.from, (out.get(edge.from) ?? 0) + 1);
86
- }
87
- return out;
88
- }
89
-
90
- function buildForwardInDegree(edges: readonly ClassifiedEdge[]): Map<string, number> {
91
- const indeg = new Map<string, number>();
92
- for (const edge of forwardEdges(edges)) {
93
- if (edge.from === edge.to) continue;
94
- indeg.set(edge.to, (indeg.get(edge.to) ?? 0) + 1);
95
- }
96
- return indeg;
97
- }
98
-
99
- /**
100
- * Distinct source contracts among a contract's forward producers. A contract is
101
- * a *convergence* when this count is >= 2. Multiple migrations sharing one
102
- * source (a multi-edge) count once — they stack in a single lane rather than
103
- * fanning into a convergence.
104
- */
105
- function buildDistinctSourceCountByTo(edges: readonly ClassifiedEdge[]): Map<string, number> {
106
- const sources = new Map<string, Set<string>>();
107
- for (const edge of edges) {
108
- if (edge.kind !== 'forward' || edge.from === edge.to) continue;
109
- const set = sources.get(edge.to);
110
- if (set) set.add(edge.from);
111
- else sources.set(edge.to, new Set([edge.from]));
112
- }
113
- const counts = new Map<string, number>();
114
- for (const [to, set] of sources) counts.set(to, set.size);
115
- return counts;
116
- }
117
-
118
- function splitComponents(nodes: readonly (string | null)[]): readonly (readonly string[])[] {
119
- const components: string[][] = [];
120
- let current: string[] = [];
121
- for (const node of nodes) {
122
- if (node === null) {
123
- if (current.length > 0) {
124
- components.push(current);
125
- current = [];
126
- }
127
- continue;
128
- }
129
- current.push(node);
130
- }
131
- if (current.length > 0) components.push(current);
132
- return components;
133
- }
134
-
135
- // ---------------------------------------------------------------------------
136
- // Adjacency refinement (operates on the emitted rows)
137
- // ---------------------------------------------------------------------------
138
-
139
- function classifyForwardShortConvergenceAdjacency(
140
- rows: readonly MigrationGraphGridRow[],
141
- edgeRowIndex: number,
142
- edge: ClassifiedEdge,
143
- laneIndex: number,
144
- ): EdgeAdjacency {
145
- for (let index = edgeRowIndex + 1; index < rows.length; index++) {
146
- const row = rows[index];
147
- if (row === undefined) break;
148
- if (row.kind === 'component-separator' || row.kind === 'branch-connector') continue;
149
- if (row.kind === 'merge-connector') continue;
150
- if (row.kind === 'edge') {
151
- if (row.laneIndex === laneIndex) return 'node-skipping-forward';
152
- continue;
153
- }
154
- if (row.kind === 'node' && row.contractHash === edge.from) {
155
- return 'adjacent';
156
- }
157
- }
158
- return 'node-skipping-forward';
159
- }
160
-
161
- function convergenceProducerUsesShortAdjacency(
162
- edge: ClassifiedEdge,
163
- laneIndex: number,
164
- forwardProducersByTo: ReadonlyMap<string, readonly ClassifiedEdge[]>,
165
- producerLaneByHash: ReadonlyMap<string, number>,
166
- ): boolean {
167
- const producers = (forwardProducersByTo.get(edge.to) ?? []).filter(
168
- (candidate) => candidate.kind === 'forward',
169
- );
170
- if (producers.length < 2) return false;
171
-
172
- const fanLanes = [
173
- ...new Set(
174
- producers
175
- .map((producer) => producerLaneByHash.get(producer.migrationHash))
176
- .filter((candidate): candidate is number => candidate !== undefined),
177
- ),
178
- ].sort((a, b) => a - b);
179
- const fanStart = fanLanes[0];
180
- if (fanStart === undefined) return false;
181
-
182
- return laneIndex === fanStart;
183
- }
184
-
185
- function classifyForwardLayoutAdjacency(
186
- rows: readonly MigrationGraphGridRow[],
187
- edgeRowIndex: number,
188
- edge: ClassifiedEdge,
189
- laneIndex: number,
190
- passThroughLanes: readonly number[],
191
- nodeColumn: ReadonlyMap<string, number>,
192
- convergenceProducer: boolean,
193
- divergenceBranchEdge: boolean,
194
- ): EdgeAdjacency {
195
- let sawObstruction = false;
196
- const passThroughLaneSet = new Set(passThroughLanes);
197
-
198
- for (let index = edgeRowIndex + 1; index < rows.length; index++) {
199
- const row = rows[index];
200
- if (row === undefined) break;
201
- if (row.kind === 'component-separator') continue;
202
- if (row.kind === 'merge-connector') {
203
- if (convergenceProducer) {
204
- if (row.contractHash === edge.from) sawObstruction = true;
205
- } else if (!divergenceBranchEdge && row.contractHash !== edge.from) {
206
- sawObstruction = true;
207
- }
208
- continue;
209
- }
210
- if (row.kind === 'branch-connector') continue;
211
- if (row.kind === 'edge') {
212
- if (row.laneIndex === laneIndex) return 'node-skipping-forward';
213
- if (!divergenceBranchEdge && row.edge !== undefined && row.edge.to !== edge.to) {
214
- sawObstruction = true;
215
- }
216
- continue;
217
- }
218
- if (row.kind === 'node' && row.contractHash !== undefined) {
219
- if (row.contractHash === edge.from) {
220
- return sawObstruction ? 'node-skipping-forward' : 'adjacent';
221
- }
222
- const nodeCol = nodeColumn.get(row.contractHash) ?? 0;
223
- // A divergence-branch lane runs unobstructed to its convergence point;
224
- // sibling-branch nodes sit in parallel lanes and never block it.
225
- if (!divergenceBranchEdge && !passThroughLaneSet.has(nodeCol)) {
226
- sawObstruction = true;
227
- }
228
- }
229
- }
230
-
231
- return 'node-skipping-forward';
232
- }
233
-
234
- function classifyLayoutAdjacency(
235
- rows: readonly MigrationGraphGridRow[],
236
- edgeRowIndex: number,
237
- edge: ClassifiedEdge,
238
- laneIndex: number,
239
- passThroughLanes: readonly number[],
240
- nodeColumn: ReadonlyMap<string, number>,
241
- position: ReadonlyMap<string, number>,
242
- forwardInDegree: ReadonlyMap<string, number>,
243
- convergenceProducer: boolean,
244
- divergenceBranchEdge: boolean,
245
- ): EdgeAdjacency {
246
- if (edge.kind === 'self') return 'adjacent';
247
-
248
- const fromPos = position.get(edge.from);
249
- const toPos = position.get(edge.to);
250
-
251
- if (edge.kind === 'forward') {
252
- const inDegree = forwardInDegree.get(edge.to) ?? 0;
253
- if (inDegree <= 1 && fromPos !== undefined && toPos !== undefined && fromPos === toPos + 1) {
254
- return 'adjacent';
255
- }
256
- return classifyForwardLayoutAdjacency(
257
- rows,
258
- edgeRowIndex,
259
- edge,
260
- laneIndex,
261
- passThroughLanes,
262
- nodeColumn,
263
- convergenceProducer,
264
- divergenceBranchEdge,
265
- );
266
- }
267
-
268
- if (fromPos !== undefined && toPos !== undefined && toPos === fromPos + 1) {
269
- return 'adjacent';
270
- }
271
-
272
- for (let index = edgeRowIndex + 1; index < rows.length; index++) {
273
- const row = rows[index];
274
- if (row === undefined) break;
275
- if (
276
- row.kind === 'component-separator' ||
277
- row.kind === 'branch-connector' ||
278
- row.kind === 'merge-connector'
279
- ) {
280
- continue;
281
- }
282
- if (row.kind === 'edge') continue;
283
- if (row.kind === 'node') {
284
- return row.contractHash === edge.to ? 'adjacent' : 'node-skipping-rollback';
285
- }
286
- }
287
- return 'node-skipping-rollback';
288
- }
289
-
290
- function refineAdjacency(
291
- rows: readonly MigrationGraphGridRow[],
292
- nodeColumn: ReadonlyMap<string, number>,
293
- position: ReadonlyMap<string, number>,
294
- forwardInDegree: ReadonlyMap<string, number>,
295
- forwardOutDegree: ReadonlyMap<string, number>,
296
- edges: readonly ClassifiedEdge[],
297
- producerLaneByHash: ReadonlyMap<string, number>,
298
- ): MigrationGraphGridRow[] {
299
- const forwardProducersByTo = buildForwardProducersByTo(edges);
300
- function branchLaneForEdge(producer: ClassifiedEdge): number | undefined {
301
- const children = edges.filter(
302
- (edge) => edge.from === producer.from && edge.kind === 'forward' && edge.from !== edge.to,
303
- );
304
- if (children.length < 2) return undefined;
305
- const index = children.findIndex((child) => child.migrationHash === producer.migrationHash);
306
- return index >= 0 ? index : undefined;
307
- }
308
-
309
- return rows.map((row, rowIndex) => {
310
- if (row.kind !== 'edge' || row.edge === undefined || row.laneIndex === undefined) {
311
- return row;
312
- }
313
- const divergenceBranchEdge =
314
- row.edge.kind === 'forward' &&
315
- !(row.convergenceProducer ?? false) &&
316
- (forwardOutDegree.get(row.edge.from) ?? 0) >= 2 &&
317
- branchLaneForEdge(row.edge) !== undefined;
318
- const adjacency =
319
- row.convergenceProducer === true &&
320
- convergenceProducerUsesShortAdjacency(
321
- row.edge,
322
- row.laneIndex,
323
- forwardProducersByTo,
324
- producerLaneByHash,
325
- )
326
- ? classifyForwardShortConvergenceAdjacency(rows, rowIndex, row.edge, row.laneIndex)
327
- : classifyLayoutAdjacency(
328
- rows,
329
- rowIndex,
330
- row.edge,
331
- row.laneIndex,
332
- row.passThroughLanes ?? [],
333
- nodeColumn,
334
- position,
335
- forwardInDegree,
336
- row.convergenceProducer ?? false,
337
- divergenceBranchEdge,
338
- );
339
- return {
340
- ...row,
341
- cells: buildEdgeCells(
342
- row.edge,
343
- row.laneIndex,
344
- row.passThroughLanes ?? [],
345
- adjacency,
346
- row.cells.length,
347
- ),
348
- };
349
- });
350
- }
351
-
352
- function classifyEdgeAdjacency(
353
- edge: ClassifiedEdge,
354
- position: ReadonlyMap<string, number>,
355
- ): EdgeAdjacency {
356
- if (edge.kind === 'self') return 'adjacent';
357
-
358
- const fromPos = position.get(edge.from);
359
- const toPos = position.get(edge.to);
360
- if (fromPos === undefined || toPos === undefined) return 'adjacent';
361
-
362
- if (edge.kind === 'forward') {
363
- if (toPos >= fromPos) return 'adjacent';
364
- return fromPos === toPos + 1 ? 'adjacent' : 'node-skipping-forward';
365
- }
366
-
367
- if (toPos <= fromPos) return 'adjacent';
368
- return toPos === fromPos + 1 ? 'adjacent' : 'node-skipping-rollback';
369
- }
370
-
371
- // ---------------------------------------------------------------------------
372
- // Cell builders
373
- // ---------------------------------------------------------------------------
374
-
375
- function emptyCells(width: number): StructuralCell[] {
376
- return Array.from({ length: width }, () => ({ kind: 'empty' as const }));
377
- }
378
-
379
- function buildBranchConnectorCells(
380
- startLane: number,
381
- endLane: number,
382
- activeLanes: ReadonlySet<number>,
383
- gridWidth: number,
384
- ): StructuralCell[] {
385
- const cells = emptyCells(gridWidth);
386
- for (let lane = 0; lane < gridWidth; lane++) {
387
- if (activeLanes.has(lane) && (lane < startLane || lane > endLane)) {
388
- cells[lane] = { kind: 'vertical-pass' };
389
- continue;
390
- }
391
- if (lane === startLane) {
392
- cells[lane] = { kind: 'branch-tee' };
393
- } else if (lane === endLane) {
394
- cells[lane] = { kind: 'branch-corner' };
395
- } else if (lane > startLane && lane < endLane) {
396
- cells[lane] = { kind: 'branch-tee' };
397
- }
398
- }
399
- return cells;
400
- }
401
-
402
- function buildMergeConnectorCells(
403
- startLane: number,
404
- endLane: number,
405
- activeLanes: ReadonlySet<number>,
406
- gridWidth: number,
407
- ): StructuralCell[] {
408
- const cells = emptyCells(gridWidth);
409
- for (let lane = 0; lane < gridWidth; lane++) {
410
- if (activeLanes.has(lane) && (lane < startLane || lane > endLane)) {
411
- cells[lane] = { kind: 'vertical-pass' };
412
- continue;
413
- }
414
- if (lane === startLane) {
415
- cells[lane] = { kind: 'merge-tee' };
416
- } else if (lane === endLane) {
417
- cells[lane] = { kind: 'merge-corner' };
418
- } else if (lane > startLane && lane < endLane) {
419
- cells[lane] = activeLanes.has(lane) ? { kind: 'merge-tee' } : { kind: 'horizontal-pass' };
420
- }
421
- }
422
- return cells;
423
- }
424
-
425
- function buildNodeCells(
426
- contractHash: string,
427
- nodeColumn: number,
428
- activeLanes: readonly number[],
429
- gridWidth: number,
430
- ): StructuralCell[] {
431
- const cells = emptyCells(gridWidth);
432
- for (const lane of activeLanes) {
433
- if (lane !== nodeColumn && lane < gridWidth) {
434
- cells[lane] = { kind: 'vertical-pass' };
435
- }
436
- }
437
- if (nodeColumn < gridWidth) {
438
- cells[nodeColumn] = { kind: 'node', contractHash };
439
- }
440
- return cells;
441
- }
442
-
443
- function buildEdgeCells(
444
- edge: ClassifiedEdge,
445
- laneIndex: number,
446
- passThroughLanes: readonly number[],
447
- adjacency: EdgeAdjacency,
448
- gridWidth: number,
449
- ): StructuralCell[] {
450
- const cells = emptyCells(gridWidth);
451
- for (const lane of passThroughLanes) {
452
- if (lane < gridWidth) cells[lane] = { kind: 'vertical-pass' };
453
- }
454
- if (laneIndex < gridWidth) {
455
- cells[laneIndex] = {
456
- kind: 'edge-lane',
457
- migrationHash: edge.migrationHash,
458
- edgeKind: edge.kind,
459
- ownsLabel: true,
460
- adjacency,
461
- };
462
- }
463
- return cells;
464
- }
465
-
466
- // ---------------------------------------------------------------------------
467
- // Vertical ordering: tips-first DFS post-order over forward edges
468
- // ---------------------------------------------------------------------------
469
-
470
- /**
471
- * Compute the vertical node order for a component: tips at the top (index 0),
472
- * roots at the bottom. This is a DFS post-order over forward edges starting
473
- * from forward roots, visiting children in their input (insertion) order. A
474
- * node is emitted only after all of its forward children, so convergence nodes
475
- * sit below every branch that feeds them and the longest contiguous chain reads
476
- * top-to-bottom without braiding.
477
- */
478
- function computeVerticalOrder(
479
- componentNodes: readonly string[],
480
- forwardChildren: ReadonlyMap<string, readonly ClassifiedEdge[]>,
481
- forwardInDegree: ReadonlyMap<string, number>,
482
- ): string[] {
483
- const WHITE = 0;
484
- const GRAY = 1;
485
- const BLACK = 2;
486
- const color = new Map<string, number>();
487
- for (const node of componentNodes) color.set(node, WHITE);
488
-
489
- const sortRoots = (roots: readonly string[]): string[] =>
490
- [...roots].sort((a, b) => {
491
- if (a === EMPTY_CONTRACT_HASH) return -1;
492
- if (b === EMPTY_CONTRACT_HASH) return 1;
493
- return a.localeCompare(b);
494
- });
495
-
496
- let roots = sortRoots(componentNodes.filter((n) => (forwardInDegree.get(n) ?? 0) === 0));
497
- if (roots.length === 0) roots = sortRoots(componentNodes);
498
-
499
- const result: string[] = [];
500
-
501
- interface Frame {
502
- node: string;
503
- children: readonly ClassifiedEdge[];
504
- index: number;
505
- }
506
-
507
- function runDfs(root: string): void {
508
- if (color.get(root) !== WHITE) return;
509
- const stack: Frame[] = [{ node: root, children: forwardChildren.get(root) ?? [], index: 0 }];
510
- color.set(root, GRAY);
511
-
512
- while (stack.length > 0) {
513
- const frame = stack[stack.length - 1];
514
- if (frame === undefined) break;
515
- if (frame.index >= frame.children.length) {
516
- color.set(frame.node, BLACK);
517
- result.push(frame.node);
518
- stack.pop();
519
- continue;
520
- }
521
- const child = frame.children[frame.index];
522
- frame.index += 1;
523
- if (child === undefined) continue;
524
- if (color.get(child.to) === WHITE) {
525
- color.set(child.to, GRAY);
526
- stack.push({ node: child.to, children: forwardChildren.get(child.to) ?? [], index: 0 });
527
- }
528
- }
529
- }
530
-
531
- for (const root of roots) runDfs(root);
532
- // Nodes unreachable via forward edges (e.g. rollback-only sources) follow in
533
- // component order.
534
- for (const node of componentNodes) {
535
- if (color.get(node) === WHITE) runDfs(node);
536
- }
537
-
538
- return result;
539
- }
540
-
541
- // ---------------------------------------------------------------------------
542
- // Routed back-arcs for node-skipping rollbacks
543
- // ---------------------------------------------------------------------------
544
-
545
- interface SkipRollbackRoute {
546
- readonly edge: ClassifiedEdge;
547
- readonly backLane: number;
548
- }
549
-
550
- function rollbackSpan(
551
- edge: ClassifiedEdge,
552
- position: ReadonlyMap<string, number>,
553
- ): { readonly top: number; readonly bottom: number } {
554
- const top = position.get(edge.from) ?? 0;
555
- const bottom = position.get(edge.to) ?? top;
556
- return { top, bottom };
557
- }
558
-
559
- function spansOverlap(
560
- a: { readonly top: number; readonly bottom: number },
561
- b: { readonly top: number; readonly bottom: number },
562
- ): boolean {
563
- return a.top <= b.bottom && b.top <= a.bottom;
564
- }
565
-
566
- function forwardMaxLane(
567
- rows: readonly MigrationGraphGridRow[],
568
- skipMigrationHashes: ReadonlySet<string>,
569
- ): number {
570
- let max = 0;
571
- for (const row of rows) {
572
- if (
573
- row.kind === 'edge' &&
574
- row.edge !== undefined &&
575
- skipMigrationHashes.has(row.edge.migrationHash)
576
- ) {
577
- continue;
578
- }
579
- max = Math.max(max, row.laneIndex ?? 0);
580
- for (const lane of row.passThroughLanes ?? []) {
581
- max = Math.max(max, lane);
582
- }
583
- if (row.startLane !== undefined) {
584
- max = Math.max(max, row.startLane, row.endLane ?? row.startLane);
585
- }
586
- }
587
- return max;
588
- }
589
-
590
- function allocateSkipRollbackBackLanes(
591
- skipRollbacks: readonly ClassifiedEdge[],
592
- position: ReadonlyMap<string, number>,
593
- forwardMax: number,
594
- ): Map<string, number> {
595
- const sorted = [...skipRollbacks].sort((a, b) => {
596
- const aTop = position.get(a.from) ?? 0;
597
- const bTop = position.get(b.from) ?? 0;
598
- if (aTop !== bTop) return aTop - bTop;
599
- return b.dirName.localeCompare(a.dirName);
600
- });
601
-
602
- const occupied: { readonly top: number; readonly bottom: number; readonly lane: number }[] = [];
603
- const lanes = new Map<string, number>();
604
- let nextLane = forwardMax + 1;
605
-
606
- for (const edge of sorted) {
607
- const span = rollbackSpan(edge, position);
608
- let lane = nextLane;
609
- while (occupied.some((entry) => entry.lane === lane && spansOverlap(entry, span))) {
610
- lane += 1;
611
- }
612
- occupied.push({ ...span, lane });
613
- lanes.set(edge.migrationHash, lane);
614
- nextLane = Math.max(nextLane, lane + 1);
615
- }
616
-
617
- return lanes;
618
- }
619
-
620
- function findNodeRowIndex(rows: readonly MigrationGraphGridRow[], contractHash: string): number {
621
- return rows.findIndex((row) => row.kind === 'node' && row.contractHash === contractHash);
622
- }
623
-
624
- function findEdgeRowIndex(rows: readonly MigrationGraphGridRow[], migrationHash: string): number {
625
- return rows.findIndex((row) => row.kind === 'edge' && row.edge?.migrationHash === migrationHash);
626
- }
627
-
628
- // A grid row with a mutable `cells` array. The routing pass clones the
629
- // immutable rows into this shape so it can paint arc cells in place without
630
- // stripping `readonly` with a cast.
631
- type MutableGridRow = Omit<MigrationGraphGridRow, 'cells'> & { cells: StructuralCell[] };
632
-
633
- function ensureCellWidth(cells: StructuralCell[], width: number): void {
634
- while (cells.length < width) {
635
- cells.push({ kind: 'empty' });
636
- }
637
- }
638
-
639
- function cloneRow(row: MigrationGraphGridRow): MutableGridRow {
640
- return { ...row, cells: [...row.cells] };
641
- }
642
-
643
- function routeCrossesRow(
644
- route: SkipRollbackRoute,
645
- rowIndex: number,
646
- rows: readonly MigrationGraphGridRow[],
647
- ): boolean {
648
- const sourceRow = findNodeRowIndex(rows, route.edge.from);
649
- const targetRow = findNodeRowIndex(rows, route.edge.to);
650
- if (sourceRow < 0 || targetRow < 0) return false;
651
- return rowIndex > sourceRow && rowIndex <= targetRow;
652
- }
653
-
654
- function applySkipRollbackRouting(
655
- rows: readonly MigrationGraphGridRow[],
656
- skipRollbacks: readonly ClassifiedEdge[],
657
- position: ReadonlyMap<string, number>,
658
- nodeColumn: ReadonlyMap<string, number>,
659
- edgeColumn: Map<string, number>,
660
- ): MigrationGraphGridRow[] {
661
- if (skipRollbacks.length === 0) return [...rows];
662
-
663
- const skipHashes = new Set(skipRollbacks.map((edge) => edge.migrationHash));
664
- const forwardMax = forwardMaxLane(rows, skipHashes);
665
- const backLaneByHash = allocateSkipRollbackBackLanes(skipRollbacks, position, forwardMax);
666
- const routes: SkipRollbackRoute[] = skipRollbacks.map((edge) => ({
667
- edge,
668
- backLane: backLaneByHash.get(edge.migrationHash) ?? forwardMax + 1,
669
- }));
670
-
671
- const result = rows.map(cloneRow);
672
-
673
- for (const route of routes) {
674
- const { edge, backLane } = route;
675
- const nodeCol = nodeColumn.get(edge.from) ?? 0;
676
- const targetCol = nodeColumn.get(edge.to) ?? 0;
677
- const sourceRowIndex = findNodeRowIndex(result, edge.from);
678
- const targetRowIndex = findNodeRowIndex(result, edge.to);
679
- const edgeRowIndex = findEdgeRowIndex(result, edge.migrationHash);
680
- if (sourceRowIndex < 0 || targetRowIndex < 0 || edgeRowIndex < 0) continue;
681
-
682
- edgeColumn.set(edge.migrationHash, backLane);
683
-
684
- // Back-lanes of arcs that tee off this same source node. They share the
685
- // node's tee row, so each inner lane reads as a `┬` junction and only the
686
- // outermost gets the closing `╮`.
687
- const coSourcedLanes = routes
688
- .filter((other) => other.edge.from === edge.from)
689
- .map((other) => other.backLane);
690
- const maxCoSourcedLane = Math.max(...coSourcedLanes);
691
-
692
- const sourceRow = result[sourceRowIndex];
693
- if (sourceRow !== undefined) {
694
- const cells = sourceRow.cells;
695
- ensureCellWidth(cells, backLane + 1);
696
- const contractHash = sourceRow.contractHash ?? EMPTY_CONTRACT_HASH;
697
- cells[nodeCol] = { kind: 'node', contractHash, arcTee: true };
698
- for (let lane = nodeCol + 1; lane < backLane; lane += 1) {
699
- if (coSourcedLanes.includes(lane)) {
700
- cells[lane] = { kind: 'arc-branch-tee' };
701
- continue;
702
- }
703
- const existing = cells[lane];
704
- const occupied =
705
- existing !== undefined &&
706
- existing.kind !== 'empty' &&
707
- existing.kind !== 'horizontal-pass' &&
708
- existing.kind !== 'arc-land-bridge';
709
- const crossed =
710
- occupied ||
711
- routes.some(
712
- (other) =>
713
- other.edge.migrationHash !== edge.migrationHash &&
714
- other.backLane === lane &&
715
- routeCrossesRow(other, sourceRowIndex, result),
716
- );
717
- cells[lane] = crossed ? { kind: 'arc-crossing' } : { kind: 'horizontal-pass' };
718
- }
719
- cells[backLane] =
720
- backLane < maxCoSourcedLane ? { kind: 'arc-branch-tee' } : { kind: 'arc-branch-corner' };
721
- }
722
-
723
- const edgeRow = result[edgeRowIndex];
724
- if (edgeRow !== undefined) {
725
- // Mutate in place rather than rebuild from empty: a co-sourced arc's body
726
- // lane may already cross this row, and rebuilding would clobber it.
727
- const cells = edgeRow.cells;
728
- ensureCellWidth(cells, backLane + 1);
729
- cells[nodeCol] = { kind: 'vertical-pass' };
730
- cells[backLane] = {
731
- kind: 'edge-lane',
732
- migrationHash: edge.migrationHash,
733
- edgeKind: edge.kind,
734
- ownsLabel: true,
735
- adjacency: 'node-skipping-rollback',
736
- };
737
- result[edgeRowIndex] = { ...edgeRow, laneIndex: backLane, passThroughLanes: [nodeCol] };
738
- }
739
-
740
- // Fill the arc body vertically from just below the source tee down to the
741
- // row above the landing, skipping the rollback's own labelled edge row.
742
- // Starting below the source (rather than below the edge row) keeps a
743
- // co-sourced arc's lane connected across an earlier co-sourced edge row.
744
- for (let index = sourceRowIndex + 1; index < targetRowIndex; index += 1) {
745
- if (index === edgeRowIndex) continue;
746
- const row = result[index];
747
- if (row === undefined) continue;
748
- const cells = row.cells;
749
- ensureCellWidth(cells, backLane + 1);
750
- const existing = cells[backLane];
751
- if (
752
- existing?.kind !== 'arc-land-corner' &&
753
- existing?.kind !== 'arc-land-bridge' &&
754
- existing?.kind !== 'arc-branch-corner' &&
755
- existing?.kind !== 'arc-branch-tee' &&
756
- existing?.kind !== 'arc-crossing'
757
- ) {
758
- cells[backLane] = { kind: 'vertical-pass' };
759
- }
760
- }
761
-
762
- const targetRow = result[targetRowIndex];
763
- if (targetRow !== undefined) {
764
- const cells = targetRow.cells;
765
- ensureCellWidth(cells, backLane + 1);
766
- const contractHash = targetRow.contractHash ?? EMPTY_CONTRACT_HASH;
767
- cells[targetCol] = { kind: 'node', contractHash, arcLand: true };
768
- for (let lane = targetCol + 1; lane < backLane; lane += 1) {
769
- // A bridged lane that carries another arc OR a forward vertical still
770
- // active at this row must cross over it (`┼`) rather than overwrite it
771
- // with a bare bridge (`──`).
772
- const existing = cells[lane];
773
- const occupied =
774
- existing !== undefined &&
775
- existing.kind !== 'empty' &&
776
- existing.kind !== 'horizontal-pass' &&
777
- existing.kind !== 'arc-land-bridge';
778
- const crossed =
779
- occupied ||
780
- routes.some(
781
- (other) =>
782
- other.edge.migrationHash !== edge.migrationHash &&
783
- other.backLane === lane &&
784
- routeCrossesRow(other, targetRowIndex, result),
785
- );
786
- cells[lane] = crossed ? { kind: 'arc-crossing' } : { kind: 'arc-land-bridge' };
787
- }
788
- cells[backLane] = { kind: 'arc-land-corner' };
789
- for (const other of routes) {
790
- if (other.backLane <= backLane) continue;
791
- if (!routeCrossesRow(other, targetRowIndex, result)) continue;
792
- ensureCellWidth(cells, other.backLane + 1);
793
- const existing = cells[other.backLane];
794
- if (
795
- existing?.kind !== 'arc-land-corner' &&
796
- existing?.kind !== 'arc-land-bridge' &&
797
- existing?.kind !== 'node'
798
- ) {
799
- cells[other.backLane] = { kind: 'vertical-pass' };
800
- }
801
- }
802
- }
803
- }
804
-
805
- return result;
806
- }
807
-
808
- function collectNodeSkippingRollbacks(
809
- edges: readonly ClassifiedEdge[],
810
- position: ReadonlyMap<string, number>,
811
- ): ClassifiedEdge[] {
812
- return edges.filter(
813
- (edge) =>
814
- edge.kind === 'rollback' &&
815
- classifyEdgeAdjacency(edge, position) === 'node-skipping-rollback',
816
- );
817
- }
818
-
819
- // ---------------------------------------------------------------------------
820
- // Lane allocation: one rule for all topologies
821
- // ---------------------------------------------------------------------------
822
-
823
- interface DownwardGroup {
824
- readonly target: string;
825
- readonly edges: ClassifiedEdge[];
826
- }
827
-
828
- function layoutComponent(
829
- componentNodes: readonly string[],
830
- allEdges: readonly ClassifiedEdge[],
831
- ): {
832
- rows: MigrationGraphGridRow[];
833
- nodeColumn: Map<string, number>;
834
- edgeColumn: Map<string, number>;
835
- } {
836
- const componentSet = new Set(componentNodes);
837
- const edges = allEdges.filter((e) => componentSet.has(e.from) && componentSet.has(e.to));
838
-
839
- const forwardChildren = new Map<string, ClassifiedEdge[]>();
840
- const producersByTo = new Map<string, ClassifiedEdge[]>();
841
- const rollbacksByFrom = new Map<string, ClassifiedEdge[]>();
842
- const selfByFrom = new Map<string, ClassifiedEdge[]>();
843
- for (const edge of edges) {
844
- if (edge.kind === 'self' || edge.from === edge.to) {
845
- const bucket = selfByFrom.get(edge.from);
846
- if (bucket) bucket.push(edge);
847
- else selfByFrom.set(edge.from, [edge]);
848
- continue;
849
- }
850
- if (edge.kind === 'forward') {
851
- const children = forwardChildren.get(edge.from);
852
- if (children) children.push(edge);
853
- else forwardChildren.set(edge.from, [edge]);
854
- const producers = producersByTo.get(edge.to);
855
- if (producers) producers.push(edge);
856
- else producersByTo.set(edge.to, [edge]);
857
- continue;
858
- }
859
- // rollback
860
- const bucket = rollbacksByFrom.get(edge.from);
861
- if (bucket) bucket.push(edge);
862
- else rollbacksByFrom.set(edge.from, [edge]);
863
- }
864
-
865
- const forwardInDegree = buildForwardInDegree(edges);
866
- const forwardOutDegree = buildForwardOutDegree(edges);
867
- const distinctSourceCountByTo = buildDistinctSourceCountByTo(edges);
868
-
869
- const order = computeVerticalOrder(componentNodes, forwardChildren, forwardInDegree);
870
- const position = new Map<string, number>();
871
- for (let index = 0; index < order.length; index++) {
872
- const node = order[index];
873
- if (node !== undefined) position.set(node, index);
874
- }
875
-
876
- const lanes: (string | null)[] = [];
877
- const rows: MigrationGraphGridRow[] = [];
878
- const nodeColumn = new Map<string, number>();
879
- const edgeColumn = new Map<string, number>();
880
- const producerLaneByHash = new Map<string, number>();
881
- let gridWidth = 1;
882
-
883
- function ensureGridWidth(minWidth: number): void {
884
- if (minWidth > gridWidth) gridWidth = minWidth;
885
- }
886
-
887
- function setLane(index: number, want: string | null): void {
888
- while (lanes.length <= index) lanes.push(null);
889
- lanes[index] = want;
890
- if (want !== null) ensureGridWidth(index + 1);
891
- }
892
-
893
- function activeLaneIndices(): number[] {
894
- const indices: number[] = [];
895
- for (let index = 0; index < lanes.length; index++) {
896
- if (lanes[index] !== null) indices.push(index);
897
- }
898
- return indices;
899
- }
900
-
901
- function passThroughExcept(lane: number): number[] {
902
- return activeLaneIndices().filter((index) => index !== lane);
903
- }
904
-
905
- function leftmostFreeLane(): number {
906
- for (let index = 0; index < lanes.length; index++) {
907
- if (lanes[index] === null) return index;
908
- }
909
- return lanes.length;
910
- }
911
-
912
- function lanesWanting(contract: string): number[] {
913
- const indices: number[] = [];
914
- for (let index = 0; index < lanes.length; index++) {
915
- if (lanes[index] === contract) indices.push(index);
916
- }
917
- return indices;
918
- }
919
-
920
- function emitMergeConnector(contractHash: string, laneIndices: readonly number[]): number {
921
- const startLane = Math.min(...laneIndices);
922
- const endLane = Math.max(...laneIndices);
923
- ensureGridWidth(endLane + 1);
924
- const activeLanes = new Set(activeLaneIndices());
925
- rows.push({
926
- kind: 'merge-connector',
927
- contractHash,
928
- startLane,
929
- endLane,
930
- branchCount: laneIndices.length,
931
- cells: buildMergeConnectorCells(startLane, endLane, activeLanes, gridWidth),
932
- });
933
- for (const index of laneIndices) {
934
- if (index !== startLane) setLane(index, null);
935
- }
936
- return startLane;
937
- }
938
-
939
- function emitBranchConnector(
940
- contractHash: string,
941
- startLane: number,
942
- endLane: number,
943
- branchCount: number,
944
- ): void {
945
- ensureGridWidth(endLane + 1);
946
- const activeLanes = new Set(activeLaneIndices());
947
- rows.push({
948
- kind: 'branch-connector',
949
- contractHash,
950
- startLane,
951
- endLane,
952
- branchCount,
953
- cells: buildBranchConnectorCells(startLane, endLane, activeLanes, gridWidth),
954
- });
955
- }
956
-
957
- function emitEdgeRow(edge: ClassifiedEdge, lane: number, convergenceProducer: boolean): void {
958
- const passThrough = passThroughExcept(lane);
959
- const adjacency = classifyEdgeAdjacency(edge, position);
960
- ensureGridWidth(Math.max(lane, ...passThrough, 0) + 1);
961
- const row: MigrationGraphGridRow = {
962
- kind: 'edge',
963
- edge,
964
- laneIndex: lane,
965
- passThroughLanes: passThrough,
966
- cells: buildEdgeCells(edge, lane, passThrough, adjacency, gridWidth),
967
- };
968
- rows.push(convergenceProducer ? { ...row, convergenceProducer: true } : row);
969
- edgeColumn.set(edge.migrationHash, lane);
970
- if (convergenceProducer) producerLaneByHash.set(edge.migrationHash, lane);
971
- }
972
-
973
- function emitNodeRow(contractHash: string, column: number): void {
974
- ensureGridWidth(column + 1);
975
- const passThrough = activeLaneIndices().filter((index) => index !== column);
976
- rows.push({
977
- kind: 'node',
978
- contractHash,
979
- cells: buildNodeCells(contractHash, column, passThrough, gridWidth),
980
- });
981
- nodeColumn.set(contractHash, column);
982
- }
983
-
984
- function producerGroups(node: string): DownwardGroup[] {
985
- const byTarget = new Map<string, DownwardGroup>();
986
- for (const producer of producersByTo.get(node) ?? []) {
987
- const group = byTarget.get(producer.from);
988
- if (group) group.edges.push(producer);
989
- else byTarget.set(producer.from, { target: producer.from, edges: [producer] });
990
- }
991
- const groups = [...byTarget.values()];
992
- // Lanes are ordered by where their target node lands vertically (soonest →
993
- // leftmost), which keeps lanes from crossing.
994
- groups.sort((a, b) => (position.get(a.target) ?? 0) - (position.get(b.target) ?? 0));
995
- for (const group of groups) {
996
- group.edges.sort((a, b) => b.dirName.localeCompare(a.dirName));
997
- }
998
- return groups;
999
- }
1000
-
1001
- function processNode(node: string): void {
1002
- const wanting = lanesWanting(node);
1003
- let column: number;
1004
- if (wanting.length >= 2) {
1005
- column = emitMergeConnector(node, wanting);
1006
- } else if (wanting.length === 1) {
1007
- column = wanting[0] ?? 0;
1008
- } else {
1009
- column = leftmostFreeLane();
1010
- }
1011
-
1012
- // Self-edges sit immediately above their node, in its column.
1013
- const selfEdges = [...(selfByFrom.get(node) ?? [])].sort((a, b) =>
1014
- b.dirName.localeCompare(a.dirName),
1015
- );
1016
- for (const selfEdge of selfEdges) emitEdgeRow(selfEdge, column, false);
1017
-
1018
- emitNodeRow(node, column);
1019
-
1020
- const rollbacks = [...(rollbacksByFrom.get(node) ?? [])].sort((a, b) =>
1021
- b.dirName.localeCompare(a.dirName),
1022
- );
1023
- const skipRollbacks: ClassifiedEdge[] = [];
1024
- const adjacentRollbacks: ClassifiedEdge[] = [];
1025
- for (const rollback of rollbacks) {
1026
- if (classifyEdgeAdjacency(rollback, position) === 'node-skipping-rollback') {
1027
- skipRollbacks.push(rollback);
1028
- } else {
1029
- adjacentRollbacks.push(rollback);
1030
- }
1031
- }
1032
- for (const rollback of skipRollbacks) {
1033
- emitEdgeRow(rollback, column, false);
1034
- }
1035
-
1036
- const groups = producerGroups(node);
1037
- const isConvergence = (distinctSourceCountByTo.get(node) ?? 0) >= 2;
1038
- const laneForGroup: number[] = [];
1039
- for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
1040
- const group = groups[groupIndex];
1041
- if (group === undefined) continue;
1042
- const lane = groupIndex === 0 ? column : leftmostFreeLane();
1043
- laneForGroup[groupIndex] = lane;
1044
- setLane(lane, group.target);
1045
- }
1046
-
1047
- if (groups.length >= 2) {
1048
- const endLane = Math.max(...laneForGroup);
1049
- emitBranchConnector(node, column, endLane, groups.length);
1050
- }
1051
-
1052
- for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
1053
- const group = groups[groupIndex];
1054
- const lane = laneForGroup[groupIndex];
1055
- if (group === undefined || lane === undefined) continue;
1056
- for (const edge of group.edges) {
1057
- emitEdgeRow(edge, lane, isConvergence);
1058
- }
1059
- }
1060
-
1061
- for (const rollback of adjacentRollbacks) {
1062
- emitEdgeRow(rollback, column, false);
1063
- }
1064
-
1065
- if (groups.length === 0) {
1066
- // A root / leaf: its column lane terminates here.
1067
- setLane(column, null);
1068
- }
1069
- }
1070
-
1071
- for (const node of order) processNode(node);
1072
-
1073
- const refined = refineAdjacency(
1074
- rows,
1075
- nodeColumn,
1076
- position,
1077
- forwardInDegree,
1078
- forwardOutDegree,
1079
- edges,
1080
- producerLaneByHash,
1081
- );
1082
- const skipRollbacks = collectNodeSkippingRollbacks(edges, position);
1083
- const routed = applySkipRollbackRouting(refined, skipRollbacks, position, nodeColumn, edgeColumn);
1084
-
1085
- return {
1086
- rows: routed,
1087
- nodeColumn,
1088
- edgeColumn,
1089
- };
1090
- }
1091
-
1092
- export function buildMigrationGraphLayout(
1093
- rowModel: MigrationGraphRowModel,
1094
- ): MigrationGraphGridModel {
1095
- if (rowModel.nodes.length === 0) {
1096
- return { rows: [], nodeColumn: new Map(), edgeColumn: new Map() };
1097
- }
1098
-
1099
- const components = splitComponents(rowModel.nodes);
1100
- const allRows: MigrationGraphGridRow[] = [];
1101
- const nodeColumn = new Map<string, number>();
1102
- const edgeColumn = new Map<string, number>();
1103
-
1104
- for (let componentIndex = 0; componentIndex < components.length; componentIndex++) {
1105
- if (componentIndex > 0) {
1106
- allRows.push({ kind: 'component-separator', cells: [] });
1107
- }
1108
-
1109
- const component = components[componentIndex];
1110
- if (component === undefined || component.length === 0) continue;
1111
-
1112
- const result = layoutComponent(component, rowModel.edges);
1113
- allRows.push(...result.rows);
1114
- for (const [hash, column] of result.nodeColumn) nodeColumn.set(hash, column);
1115
- for (const [hash, column] of result.edgeColumn) edgeColumn.set(hash, column);
1116
- }
1117
-
1118
- return { rows: allRows, nodeColumn, edgeColumn };
1119
- }