@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.
Files changed (202) 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-ROablRVC.mjs} +198 -105
  5. package/dist/client-ROablRVC.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 +292 -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 +4 -4
  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-Bsp46T8u.mjs} +3 -3
  75. package/dist/{contract-infer-D8uEbJuu.mjs.map → contract-infer-Bsp46T8u.mjs.map} +1 -1
  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-CMKyBJZH.mjs} +6 -6
  79. package/dist/{db-verify-v_vUKXTU.mjs.map → db-verify-CMKyBJZH.mjs.map} +1 -1
  80. package/dist/exports/control-api.d.mts +2 -2
  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-CDXkYGh0.mjs} +5 -5
  97. package/dist/{inspect-live-schema-C6ohV_oQ.mjs.map → inspect-live-schema-CDXkYGh0.mjs.map} +1 -1
  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-BC3X6KBg.mjs} +5 -5
  103. package/dist/{migration-command-scaffold-CjvwO6at.mjs.map → migration-command-scaffold-BC3X6KBg.mjs.map} +1 -1
  104. package/dist/migration-graph-space-render-Cpg0ql8v.mjs +2370 -0
  105. package/dist/migration-graph-space-render-Cpg0ql8v.mjs.map +1 -0
  106. package/dist/migration-list-CyLslAtv.mjs +230 -0
  107. package/dist/migration-list-CyLslAtv.mjs.map +1 -0
  108. package/dist/migration-log-DvC-Iq_k.mjs +222 -0
  109. package/dist/migration-log-DvC-Iq_k.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-DUBRTJEl.mjs} +7 -7
  113. package/dist/{migration-plan-9DJ7q7_z.mjs.map → migration-plan-DUBRTJEl.mjs.map} +1 -1
  114. package/dist/migration-status-DnEW9YQn.mjs +447 -0
  115. package/dist/migration-status-DnEW9YQn.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-BepB6ydp.d.mts} +44 -31
  131. package/dist/types-BepB6ydp.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 +25 -22
  135. package/src/cli.ts +5 -0
  136. package/src/commands/db-update.ts +7 -1
  137. package/src/commands/init/index.ts +6 -35
  138. package/src/commands/init/init.ts +1 -14
  139. package/src/commands/init/inputs.ts +0 -75
  140. package/src/commands/json/schemas.ts +195 -0
  141. package/src/commands/migrate.ts +518 -8
  142. package/src/commands/migration-check.ts +469 -134
  143. package/src/commands/migration-graph.ts +164 -91
  144. package/src/commands/migration-list.ts +72 -39
  145. package/src/commands/migration-log.ts +52 -102
  146. package/src/commands/migration-show.ts +31 -66
  147. package/src/commands/migration-status-overlay.ts +61 -0
  148. package/src/commands/migration-status.ts +457 -1067
  149. package/src/commands/telemetry/index.ts +107 -0
  150. package/src/commands/telemetry/status.ts +67 -0
  151. package/src/control-api/client.ts +40 -9
  152. package/src/control-api/operations/contract-emit.ts +22 -2
  153. package/src/control-api/operations/db-init.ts +3 -3
  154. package/src/control-api/operations/{db-apply.ts → db-run.ts} +51 -13
  155. package/src/control-api/operations/db-update.ts +4 -4
  156. package/src/control-api/operations/db-verify.ts +15 -5
  157. package/src/control-api/operations/{migration-apply.ts → migrate.ts} +181 -80
  158. package/src/control-api/operations/{apply.ts → run-migration.ts} +33 -27
  159. package/src/control-api/types.ts +46 -29
  160. package/src/utils/cli-errors.ts +70 -2
  161. package/src/utils/formatters/errors.ts +11 -0
  162. package/src/utils/formatters/migration-graph-lane-colors.ts +194 -0
  163. package/src/utils/formatters/migration-graph-layout.ts +227 -38
  164. package/src/utils/formatters/migration-graph-rows.ts +128 -15
  165. package/src/utils/formatters/migration-graph-space-render.ts +148 -0
  166. package/src/utils/formatters/migration-graph-tree-render.ts +959 -81
  167. package/src/utils/formatters/migration-list-data-column.ts +4 -91
  168. package/src/utils/formatters/migration-list-graph-topology.ts +72 -94
  169. package/src/utils/formatters/migration-list-render.ts +135 -71
  170. package/src/utils/formatters/migration-list-styler.ts +46 -5
  171. package/src/utils/formatters/migration-list-types.ts +5 -21
  172. package/src/utils/formatters/migration-log-table.ts +205 -0
  173. package/src/utils/formatters/migrations.ts +33 -11
  174. package/src/utils/global-flags.ts +35 -0
  175. package/src/utils/integrity-violation-to-check-failure.ts +28 -19
  176. package/src/utils/legend.ts +38 -0
  177. package/src/utils/migration-path-target.ts +60 -0
  178. package/src/utils/telemetry.ts +68 -32
  179. package/dist/client-KgJorIvG.mjs.map +0 -1
  180. package/dist/command-helpers-Bbw1GbwL.mjs.map +0 -1
  181. package/dist/commands/migration-list.mjs.map +0 -1
  182. package/dist/commands/migration-log.mjs.map +0 -1
  183. package/dist/commands/migration-status.mjs.map +0 -1
  184. package/dist/contract-emit-D-4jrNve.mjs.map +0 -1
  185. package/dist/graph-render-rFAqZujX.mjs +0 -1081
  186. package/dist/graph-render-rFAqZujX.mjs.map +0 -1
  187. package/dist/init-Cv9UzWL5.mjs.map +0 -1
  188. package/dist/migration-check-BiBJoYYW.mjs +0 -341
  189. package/dist/migration-check-BiBJoYYW.mjs.map +0 -1
  190. package/dist/migration-graph-D7DVUElV.mjs +0 -1232
  191. package/dist/migration-graph-D7DVUElV.mjs.map +0 -1
  192. package/dist/migration-list-styler-BRwF4-gy.mjs +0 -399
  193. package/dist/migration-list-styler-BRwF4-gy.mjs.map +0 -1
  194. package/dist/migration-types-D2FW63pr.d.mts +0 -15
  195. package/dist/migration-types-D2FW63pr.d.mts.map +0 -1
  196. package/dist/migrations-Cv2jxNNK.mjs +0 -228
  197. package/dist/migrations-Cv2jxNNK.mjs.map +0 -1
  198. package/dist/terminal-ui-5Y6mrg93.d.mts.map +0 -1
  199. package/dist/types-Dt_SfqFm.d.mts.map +0 -1
  200. package/src/utils/formatters/graph-migration-mapper.ts +0 -235
  201. package/src/utils/formatters/graph-render.ts +0 -1323
  202. package/src/utils/formatters/graph-types.ts +0 -120
@@ -12,17 +12,24 @@ export type StructuralCell =
12
12
  readonly arcTee?: boolean;
13
13
  readonly arcLand?: boolean;
14
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' }
15
+ | { readonly kind: 'vertical-pass'; readonly migrationHash?: string }
16
+ | { readonly kind: 'horizontal-pass'; readonly migrationHash?: string }
17
+ | { readonly kind: 'branch-tee'; readonly migrationHash?: string }
18
+ | { readonly kind: 'branch-corner'; readonly migrationHash?: string }
19
+ | { readonly kind: 'merge-tee'; readonly migrationHash?: string }
20
+ | { readonly kind: 'merge-corner'; readonly migrationHash?: string }
21
+ | { readonly kind: 'arc-branch-corner'; readonly migrationHash?: string }
22
+ | { readonly kind: 'arc-branch-tee'; readonly migrationHash?: string }
23
+ | { readonly kind: 'arc-land-corner'; readonly migrationHash?: string }
24
+ | { readonly kind: 'arc-land-tee'; readonly migrationHash?: string }
25
+ | {
26
+ readonly kind: 'arc-crossing';
27
+ /** Hash of the edge whose vertical lane passes through this cell. */
28
+ readonly migrationHash?: string;
29
+ /** Hash of the arc edge that crosses over the vertical lane. */
30
+ readonly arcMigrationHash?: string;
31
+ }
32
+ | { readonly kind: 'arc-land-bridge'; readonly migrationHash?: string }
26
33
  | {
27
34
  readonly kind: 'edge-lane';
28
35
  readonly migrationHash: string;
@@ -336,6 +343,15 @@ function refineAdjacency(
336
343
  row.convergenceProducer ?? false,
337
344
  divergenceBranchEdge,
338
345
  );
346
+ // Reconstruct lane owners from the existing cells so the refined row
347
+ // preserves per-cell identity on its pass-through vertical-pass cells.
348
+ const existingLaneEdge = new Map<number, string>();
349
+ for (const lane of row.passThroughLanes ?? []) {
350
+ const cell = row.cells[lane];
351
+ if (cell !== undefined && 'migrationHash' in cell && cell.migrationHash !== undefined) {
352
+ existingLaneEdge.set(lane, cell.migrationHash);
353
+ }
354
+ }
339
355
  return {
340
356
  ...row,
341
357
  cells: buildEdgeCells(
@@ -344,6 +360,7 @@ function refineAdjacency(
344
360
  row.passThroughLanes ?? [],
345
361
  adjacency,
346
362
  row.cells.length,
363
+ existingLaneEdge,
347
364
  ),
348
365
  };
349
366
  });
@@ -376,24 +393,51 @@ function emptyCells(width: number): StructuralCell[] {
376
393
  return Array.from({ length: width }, () => ({ kind: 'empty' as const }));
377
394
  }
378
395
 
396
+ /** Returns `{ migrationHash: hash }` when hash is defined, otherwise `{}`. */
397
+ function hashProp(hash: string | undefined): { readonly migrationHash: string } | object {
398
+ return hash !== undefined ? { migrationHash: hash } : {};
399
+ }
400
+
401
+ /** Returns `{ arcMigrationHash: hash }` when hash is defined, otherwise `{}`. */
402
+ function arcHashProp(hash: string | undefined): { readonly arcMigrationHash: string } | object {
403
+ return hash !== undefined ? { arcMigrationHash: hash } : {};
404
+ }
405
+
379
406
  function buildBranchConnectorCells(
380
407
  startLane: number,
381
408
  endLane: number,
409
+ fanTargetLanes: ReadonlySet<number>,
382
410
  activeLanes: ReadonlySet<number>,
383
411
  gridWidth: number,
412
+ /** Hash of the edge whose lane is at startLane (the source/trunk edge). */
413
+ trunkEdgeHash: string | undefined,
414
+ /** Hash of the fan edge for each fan-target lane. */
415
+ fanEdgeHashByLane: ReadonlyMap<number, string>,
416
+ /** Hash of the edge occupying each active pass-through lane. */
417
+ laneEdgeByIndex: ReadonlyMap<number, string>,
384
418
  ): StructuralCell[] {
385
419
  const cells = emptyCells(gridWidth);
386
420
  for (let lane = 0; lane < gridWidth; lane++) {
387
421
  if (activeLanes.has(lane) && (lane < startLane || lane > endLane)) {
388
- cells[lane] = { kind: 'vertical-pass' };
422
+ cells[lane] = { kind: 'vertical-pass', ...hashProp(laneEdgeByIndex.get(lane)) };
389
423
  continue;
390
424
  }
391
425
  if (lane === startLane) {
392
- cells[lane] = { kind: 'branch-tee' };
426
+ cells[lane] = { kind: 'branch-tee', ...hashProp(trunkEdgeHash) };
393
427
  } else if (lane === endLane) {
394
- cells[lane] = { kind: 'branch-corner' };
428
+ cells[lane] = { kind: 'branch-corner', ...hashProp(fanEdgeHashByLane.get(lane)) };
395
429
  } else if (lane > startLane && lane < endLane) {
396
- cells[lane] = { kind: 'branch-tee' };
430
+ if (fanTargetLanes.has(lane)) {
431
+ cells[lane] = { kind: 'branch-tee', ...hashProp(fanEdgeHashByLane.get(lane)) };
432
+ } else if (activeLanes.has(lane)) {
433
+ cells[lane] = {
434
+ kind: 'arc-crossing',
435
+ ...hashProp(laneEdgeByIndex.get(lane)),
436
+ ...arcHashProp(fanEdgeHashByLane.get(endLane)),
437
+ };
438
+ } else {
439
+ cells[lane] = { kind: 'branch-tee', ...hashProp(fanEdgeHashByLane.get(lane)) };
440
+ }
397
441
  }
398
442
  }
399
443
  return cells;
@@ -402,21 +446,34 @@ function buildBranchConnectorCells(
402
446
  function buildMergeConnectorCells(
403
447
  startLane: number,
404
448
  endLane: number,
449
+ fanTargetLanes: ReadonlySet<number>,
405
450
  activeLanes: ReadonlySet<number>,
406
451
  gridWidth: number,
452
+ /** Hash of the edge occupying each active lane (fan lanes + pass-throughs). */
453
+ laneEdgeByIndex: ReadonlyMap<number, string>,
407
454
  ): StructuralCell[] {
408
455
  const cells = emptyCells(gridWidth);
409
456
  for (let lane = 0; lane < gridWidth; lane++) {
410
457
  if (activeLanes.has(lane) && (lane < startLane || lane > endLane)) {
411
- cells[lane] = { kind: 'vertical-pass' };
458
+ cells[lane] = { kind: 'vertical-pass', ...hashProp(laneEdgeByIndex.get(lane)) };
412
459
  continue;
413
460
  }
414
461
  if (lane === startLane) {
415
- cells[lane] = { kind: 'merge-tee' };
462
+ cells[lane] = { kind: 'merge-tee', ...hashProp(laneEdgeByIndex.get(lane)) };
416
463
  } else if (lane === endLane) {
417
- cells[lane] = { kind: 'merge-corner' };
464
+ cells[lane] = { kind: 'merge-corner', ...hashProp(laneEdgeByIndex.get(lane)) };
418
465
  } else if (lane > startLane && lane < endLane) {
419
- cells[lane] = activeLanes.has(lane) ? { kind: 'merge-tee' } : { kind: 'horizontal-pass' };
466
+ if (fanTargetLanes.has(lane)) {
467
+ cells[lane] = { kind: 'merge-tee', ...hashProp(laneEdgeByIndex.get(lane)) };
468
+ } else if (activeLanes.has(lane)) {
469
+ cells[lane] = {
470
+ kind: 'arc-crossing',
471
+ ...hashProp(laneEdgeByIndex.get(lane)),
472
+ ...arcHashProp(laneEdgeByIndex.get(endLane)),
473
+ };
474
+ } else {
475
+ cells[lane] = { kind: 'horizontal-pass', ...hashProp(laneEdgeByIndex.get(startLane)) };
476
+ }
420
477
  }
421
478
  }
422
479
  return cells;
@@ -427,11 +484,13 @@ function buildNodeCells(
427
484
  nodeColumn: number,
428
485
  activeLanes: readonly number[],
429
486
  gridWidth: number,
487
+ /** Hash of the edge occupying each active pass-through lane. */
488
+ laneEdgeByIndex: ReadonlyMap<number, string>,
430
489
  ): StructuralCell[] {
431
490
  const cells = emptyCells(gridWidth);
432
491
  for (const lane of activeLanes) {
433
492
  if (lane !== nodeColumn && lane < gridWidth) {
434
- cells[lane] = { kind: 'vertical-pass' };
493
+ cells[lane] = { kind: 'vertical-pass', ...hashProp(laneEdgeByIndex.get(lane)) };
435
494
  }
436
495
  }
437
496
  if (nodeColumn < gridWidth) {
@@ -446,10 +505,14 @@ function buildEdgeCells(
446
505
  passThroughLanes: readonly number[],
447
506
  adjacency: EdgeAdjacency,
448
507
  gridWidth: number,
508
+ /** Hash of the edge occupying each active pass-through lane. */
509
+ laneEdgeByIndex: ReadonlyMap<number, string>,
449
510
  ): StructuralCell[] {
450
511
  const cells = emptyCells(gridWidth);
451
512
  for (const lane of passThroughLanes) {
452
- if (lane < gridWidth) cells[lane] = { kind: 'vertical-pass' };
513
+ if (lane < gridWidth) {
514
+ cells[lane] = { kind: 'vertical-pass', ...hashProp(laneEdgeByIndex.get(lane)) };
515
+ }
453
516
  }
454
517
  if (laneIndex < gridWidth) {
455
518
  cells[laneIndex] = {
@@ -689,6 +752,17 @@ function applySkipRollbackRouting(
689
752
  .map((other) => other.backLane);
690
753
  const maxCoSourcedLane = Math.max(...coSourcedLanes);
691
754
 
755
+ // Back-lanes of arcs that converge on this same target node. They share the
756
+ // node's landing row, so each inner lane reads as a `┴` junction (the outer
757
+ // arcs' horizontal bridge passes through it on the way to the node) and only
758
+ // the outermost closes the corner with `╯`.
759
+ const coLandingLanes = routes
760
+ .filter((other) => other.edge.to === edge.to)
761
+ .map((other) => other.backLane);
762
+ const maxCoLandingLane = Math.max(...coLandingLanes);
763
+
764
+ const { migrationHash: arcHash } = edge;
765
+
692
766
  const sourceRow = result[sourceRowIndex];
693
767
  if (sourceRow !== undefined) {
694
768
  const cells = sourceRow.cells;
@@ -697,7 +771,12 @@ function applySkipRollbackRouting(
697
771
  cells[nodeCol] = { kind: 'node', contractHash, arcTee: true };
698
772
  for (let lane = nodeCol + 1; lane < backLane; lane += 1) {
699
773
  if (coSourcedLanes.includes(lane)) {
700
- cells[lane] = { kind: 'arc-branch-tee' };
774
+ // A co-sourced arc tees off at this lane; tag it with that arc's hash.
775
+ const coSourcedArc = routes.find((r) => r.backLane === lane && r.edge.from === edge.from);
776
+ cells[lane] = {
777
+ kind: 'arc-branch-tee',
778
+ ...hashProp(coSourcedArc?.edge.migrationHash),
779
+ };
701
780
  continue;
702
781
  }
703
782
  const existing = cells[lane];
@@ -710,14 +789,30 @@ function applySkipRollbackRouting(
710
789
  occupied ||
711
790
  routes.some(
712
791
  (other) =>
713
- other.edge.migrationHash !== edge.migrationHash &&
792
+ other.edge.migrationHash !== arcHash &&
714
793
  other.backLane === lane &&
715
794
  routeCrossesRow(other, sourceRowIndex, result),
716
795
  );
717
- cells[lane] = crossed ? { kind: 'arc-crossing' } : { kind: 'horizontal-pass' };
796
+ if (crossed) {
797
+ // The vertical lane was already occupied; tag the crossing with the
798
+ // existing vertical owner's hash and the arc that crosses over it.
799
+ const verticalHash =
800
+ existing !== undefined && 'migrationHash' in existing
801
+ ? existing.migrationHash
802
+ : undefined;
803
+ cells[lane] = {
804
+ kind: 'arc-crossing',
805
+ ...hashProp(verticalHash),
806
+ arcMigrationHash: arcHash,
807
+ };
808
+ } else {
809
+ cells[lane] = { kind: 'horizontal-pass', migrationHash: arcHash };
810
+ }
718
811
  }
719
812
  cells[backLane] =
720
- backLane < maxCoSourcedLane ? { kind: 'arc-branch-tee' } : { kind: 'arc-branch-corner' };
813
+ backLane < maxCoSourcedLane
814
+ ? { kind: 'arc-branch-tee', migrationHash: arcHash }
815
+ : { kind: 'arc-branch-corner', migrationHash: arcHash };
721
816
  }
722
817
 
723
818
  const edgeRow = result[edgeRowIndex];
@@ -726,10 +821,17 @@ function applySkipRollbackRouting(
726
821
  // lane may already cross this row, and rebuilding would clobber it.
727
822
  const cells = edgeRow.cells;
728
823
  ensureCellWidth(cells, backLane + 1);
729
- cells[nodeCol] = { kind: 'vertical-pass' };
824
+ // The forward lane at nodeCol is now interrupted by this rollback; tag the
825
+ // vertical-pass with the edge that owns that forward lane.
826
+ const forwardLaneCell = cells[nodeCol];
827
+ const forwardLaneHash =
828
+ forwardLaneCell !== undefined && 'migrationHash' in forwardLaneCell
829
+ ? forwardLaneCell.migrationHash
830
+ : undefined;
831
+ cells[nodeCol] = { kind: 'vertical-pass', ...hashProp(forwardLaneHash) };
730
832
  cells[backLane] = {
731
833
  kind: 'edge-lane',
732
- migrationHash: edge.migrationHash,
834
+ migrationHash: arcHash,
733
835
  edgeKind: edge.kind,
734
836
  ownsLabel: true,
735
837
  adjacency: 'node-skipping-rollback',
@@ -750,12 +852,13 @@ function applySkipRollbackRouting(
750
852
  const existing = cells[backLane];
751
853
  if (
752
854
  existing?.kind !== 'arc-land-corner' &&
855
+ existing?.kind !== 'arc-land-tee' &&
753
856
  existing?.kind !== 'arc-land-bridge' &&
754
857
  existing?.kind !== 'arc-branch-corner' &&
755
858
  existing?.kind !== 'arc-branch-tee' &&
756
859
  existing?.kind !== 'arc-crossing'
757
860
  ) {
758
- cells[backLane] = { kind: 'vertical-pass' };
861
+ cells[backLane] = { kind: 'vertical-pass', migrationHash: arcHash };
759
862
  }
760
863
  }
761
864
 
@@ -766,6 +869,14 @@ function applySkipRollbackRouting(
766
869
  const contractHash = targetRow.contractHash ?? EMPTY_CONTRACT_HASH;
767
870
  cells[targetCol] = { kind: 'node', contractHash, arcLand: true };
768
871
  for (let lane = targetCol + 1; lane < backLane; lane += 1) {
872
+ // An inner converging arc's own landing junction: the outer arcs' bridge
873
+ // passes through it (`┴`) while its own vertical run closes here.
874
+ if (coLandingLanes.includes(lane)) {
875
+ // Tag the landing tee with the inner arc that closes here.
876
+ const innerArc = routes.find((r) => r.backLane === lane && r.edge.to === edge.to);
877
+ cells[lane] = { kind: 'arc-land-tee', ...hashProp(innerArc?.edge.migrationHash) };
878
+ continue;
879
+ }
769
880
  // A bridged lane that carries another arc OR a forward vertical still
770
881
  // active at this row must cross over it (`┼`) rather than overwrite it
771
882
  // with a bare bridge (`──`).
@@ -774,18 +885,36 @@ function applySkipRollbackRouting(
774
885
  existing !== undefined &&
775
886
  existing.kind !== 'empty' &&
776
887
  existing.kind !== 'horizontal-pass' &&
777
- existing.kind !== 'arc-land-bridge';
888
+ existing.kind !== 'arc-land-bridge' &&
889
+ existing.kind !== 'arc-land-tee';
778
890
  const crossed =
779
891
  occupied ||
780
892
  routes.some(
781
893
  (other) =>
782
- other.edge.migrationHash !== edge.migrationHash &&
894
+ other.edge.migrationHash !== arcHash &&
783
895
  other.backLane === lane &&
784
896
  routeCrossesRow(other, targetRowIndex, result),
785
897
  );
786
- cells[lane] = crossed ? { kind: 'arc-crossing' } : { kind: 'arc-land-bridge' };
898
+ if (crossed) {
899
+ const verticalHash =
900
+ existing !== undefined && 'migrationHash' in existing
901
+ ? existing.migrationHash
902
+ : undefined;
903
+ cells[lane] = {
904
+ kind: 'arc-crossing',
905
+ ...hashProp(verticalHash),
906
+ arcMigrationHash: arcHash,
907
+ };
908
+ } else {
909
+ cells[lane] = { kind: 'arc-land-bridge', migrationHash: arcHash };
910
+ }
787
911
  }
788
- cells[backLane] = { kind: 'arc-land-corner' };
912
+ // Inner converging arcs close as a landing tee so the outermost arc's
913
+ // bridge reads through to the node; only the outermost arc draws `╯`.
914
+ cells[backLane] =
915
+ backLane < maxCoLandingLane
916
+ ? { kind: 'arc-land-tee', migrationHash: arcHash }
917
+ : { kind: 'arc-land-corner', migrationHash: arcHash };
789
918
  for (const other of routes) {
790
919
  if (other.backLane <= backLane) continue;
791
920
  if (!routeCrossesRow(other, targetRowIndex, result)) continue;
@@ -793,10 +922,16 @@ function applySkipRollbackRouting(
793
922
  const existing = cells[other.backLane];
794
923
  if (
795
924
  existing?.kind !== 'arc-land-corner' &&
925
+ existing?.kind !== 'arc-land-tee' &&
796
926
  existing?.kind !== 'arc-land-bridge' &&
797
927
  existing?.kind !== 'node'
798
928
  ) {
799
- cells[other.backLane] = { kind: 'vertical-pass' };
929
+ // This is a pass-through from another arc still in flight; tag with
930
+ // that arc's hash.
931
+ cells[other.backLane] = {
932
+ kind: 'vertical-pass',
933
+ migrationHash: other.edge.migrationHash,
934
+ };
800
935
  }
801
936
  }
802
937
  }
@@ -878,6 +1013,9 @@ function layoutComponent(
878
1013
  const nodeColumn = new Map<string, number>();
879
1014
  const edgeColumn = new Map<string, number>();
880
1015
  const producerLaneByHash = new Map<string, number>();
1016
+ // Tracks which edge's migrationHash last occupied each lane, so pass-through
1017
+ // cells on node/edge/connector rows can carry per-cell identity.
1018
+ const laneEdgeByIndex = new Map<number, string>();
881
1019
  let gridWidth = 1;
882
1020
 
883
1021
  function ensureGridWidth(minWidth: number): void {
@@ -922,13 +1060,21 @@ function layoutComponent(
922
1060
  const endLane = Math.max(...laneIndices);
923
1061
  ensureGridWidth(endLane + 1);
924
1062
  const activeLanes = new Set(activeLaneIndices());
1063
+ const fanTargetLanes = new Set(laneIndices);
925
1064
  rows.push({
926
1065
  kind: 'merge-connector',
927
1066
  contractHash,
928
1067
  startLane,
929
1068
  endLane,
930
1069
  branchCount: laneIndices.length,
931
- cells: buildMergeConnectorCells(startLane, endLane, activeLanes, gridWidth),
1070
+ cells: buildMergeConnectorCells(
1071
+ startLane,
1072
+ endLane,
1073
+ fanTargetLanes,
1074
+ activeLanes,
1075
+ gridWidth,
1076
+ laneEdgeByIndex,
1077
+ ),
932
1078
  });
933
1079
  for (const index of laneIndices) {
934
1080
  if (index !== startLane) setLane(index, null);
@@ -941,16 +1087,32 @@ function layoutComponent(
941
1087
  startLane: number,
942
1088
  endLane: number,
943
1089
  branchCount: number,
1090
+ fanTargetLanes: readonly number[],
1091
+ /** Hash of the first/representative edge for each fan lane (keyed by lane index). */
1092
+ fanEdgeHashByLane: ReadonlyMap<number, string>,
944
1093
  ): void {
945
1094
  ensureGridWidth(endLane + 1);
946
1095
  const activeLanes = new Set(activeLaneIndices());
1096
+ // Prefer the fanEdgeHashByLane entry for startLane (the downward fanout edge
1097
+ // leaving this node) over laneEdgeByIndex, which may still hold the hash of
1098
+ // the last skip-rollback emitted into that lane before the branch-connector.
1099
+ const trunkEdgeHash = fanEdgeHashByLane.get(startLane) ?? laneEdgeByIndex.get(startLane);
947
1100
  rows.push({
948
1101
  kind: 'branch-connector',
949
1102
  contractHash,
950
1103
  startLane,
951
1104
  endLane,
952
1105
  branchCount,
953
- cells: buildBranchConnectorCells(startLane, endLane, activeLanes, gridWidth),
1106
+ cells: buildBranchConnectorCells(
1107
+ startLane,
1108
+ endLane,
1109
+ new Set(fanTargetLanes),
1110
+ activeLanes,
1111
+ gridWidth,
1112
+ trunkEdgeHash,
1113
+ fanEdgeHashByLane,
1114
+ laneEdgeByIndex,
1115
+ ),
954
1116
  });
955
1117
  }
956
1118
 
@@ -963,11 +1125,14 @@ function layoutComponent(
963
1125
  edge,
964
1126
  laneIndex: lane,
965
1127
  passThroughLanes: passThrough,
966
- cells: buildEdgeCells(edge, lane, passThrough, adjacency, gridWidth),
1128
+ cells: buildEdgeCells(edge, lane, passThrough, adjacency, gridWidth, laneEdgeByIndex),
967
1129
  };
968
1130
  rows.push(convergenceProducer ? { ...row, convergenceProducer: true } : row);
969
1131
  edgeColumn.set(edge.migrationHash, lane);
970
1132
  if (convergenceProducer) producerLaneByHash.set(edge.migrationHash, lane);
1133
+ // Record this edge as the current occupant of its lane so subsequent rows
1134
+ // can tag their pass-through cells with the correct owner.
1135
+ laneEdgeByIndex.set(lane, edge.migrationHash);
971
1136
  }
972
1137
 
973
1138
  function emitNodeRow(contractHash: string, column: number): void {
@@ -976,7 +1141,7 @@ function layoutComponent(
976
1141
  rows.push({
977
1142
  kind: 'node',
978
1143
  contractHash,
979
- cells: buildNodeCells(contractHash, column, passThrough, gridWidth),
1144
+ cells: buildNodeCells(contractHash, column, passThrough, gridWidth, laneEdgeByIndex),
980
1145
  });
981
1146
  nodeColumn.set(contractHash, column);
982
1147
  }
@@ -1046,7 +1211,31 @@ function layoutComponent(
1046
1211
 
1047
1212
  if (groups.length >= 2) {
1048
1213
  const endLane = Math.max(...laneForGroup);
1049
- emitBranchConnector(node, column, endLane, groups.length);
1214
+ // Map each fan lane to the representative edge (first in the group) so
1215
+ // the branch-connector cells can carry per-cell identity.
1216
+ const fanEdgeHashByLane = new Map<number, string>();
1217
+ for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
1218
+ const group = groups[groupIndex];
1219
+ const lane = laneForGroup[groupIndex];
1220
+ if (group === undefined || lane === undefined) continue;
1221
+ const firstEdge = group.edges[0];
1222
+ if (firstEdge !== undefined) fanEdgeHashByLane.set(lane, firstEdge.migrationHash);
1223
+ }
1224
+ emitBranchConnector(node, column, endLane, groups.length, laneForGroup, fanEdgeHashByLane);
1225
+
1226
+ // Pre-populate laneEdgeByIndex for every fan lane (including lane 0 / trunk) with the
1227
+ // representative edge hash BEFORE emitting any edge rows. Without this, when groupIndex=0's
1228
+ // edge rows are emitted first, the pass-through cells for groupIndex≥1 lanes carry no hash
1229
+ // (laneEdgeByIndex has no entry yet for those lanes) and fall through to whatever annotation
1230
+ // the row's default override is — often the wrong colour.
1231
+ for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
1232
+ const fanLane = laneForGroup[groupIndex];
1233
+ if (fanLane === undefined) continue;
1234
+ const fanHash = fanEdgeHashByLane.get(fanLane);
1235
+ if (fanHash !== undefined) {
1236
+ laneEdgeByIndex.set(fanLane, fanHash);
1237
+ }
1238
+ }
1050
1239
  }
1051
1240
 
1052
1241
  for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
@@ -173,10 +173,19 @@ function compareNodesTipsFirst(a: string, b: string, rank: ReadonlyMap<string, n
173
173
  * at the same rank — stable across edge-insertion order and correct under
174
174
  * diamonds, cross-links, and rollbacks.
175
175
  */
176
+ function maxRank(rank: ReadonlyMap<string, number>): number {
177
+ let max = 0;
178
+ for (const value of rank.values()) {
179
+ if (value > max) max = value;
180
+ }
181
+ return max;
182
+ }
183
+
176
184
  function layerNodesByLongestForwardPath(
177
185
  componentNodes: ReadonlySet<string>,
178
186
  topology: MigrationListGraphTopology,
179
187
  graph: MigrationGraph,
188
+ contractHash: string | undefined,
180
189
  ): readonly string[] {
181
190
  const forwardOut = new Map<string, string[]>();
182
191
 
@@ -224,6 +233,15 @@ function layerNodesByLongestForwardPath(
224
233
  }
225
234
  }
226
235
 
236
+ if (
237
+ contractHash !== undefined &&
238
+ contractHash !== EMPTY_CONTRACT_HASH &&
239
+ componentNodes.has(contractHash) &&
240
+ (forwardOut.get(contractHash) ?? []).length === 0
241
+ ) {
242
+ rank.set(contractHash, maxRank(rank) + 1);
243
+ }
244
+
227
245
  return [...componentNodes].sort((a, b) => compareNodesTipsFirst(a, b, rank));
228
246
  }
229
247
 
@@ -262,6 +280,99 @@ function detachedContractHash(
262
280
  : undefined;
263
281
  }
264
282
 
283
+ function isForwardLeaf(node: string, edges: readonly ClassifiedEdge[]): boolean {
284
+ return !edges.some((e) => e.kind === 'forward' && e.from === node && e.from !== e.to);
285
+ }
286
+
287
+ function forwardReachableFrom(
288
+ start: string,
289
+ forwardTo: ReadonlyMap<string, readonly string[]>,
290
+ ): ReadonlySet<string> {
291
+ const reachable = new Set<string>([start]);
292
+ const queue = [start];
293
+ while (queue.length > 0) {
294
+ const node = queue.shift();
295
+ if (node === undefined) continue;
296
+ for (const next of forwardTo.get(node) ?? []) {
297
+ if (!reachable.has(next)) {
298
+ reachable.add(next);
299
+ queue.push(next);
300
+ }
301
+ }
302
+ }
303
+ return reachable;
304
+ }
305
+
306
+ function buildForwardToMap(edges: readonly ClassifiedEdge[]): Map<string, string[]> {
307
+ const forwardTo = new Map<string, string[]>();
308
+ for (const edge of edges) {
309
+ if (edge.kind !== 'forward' || edge.from === edge.to) continue;
310
+ const bucket = forwardTo.get(edge.from);
311
+ if (bucket) bucket.push(edge.to);
312
+ else forwardTo.set(edge.from, [edge.to]);
313
+ }
314
+ return forwardTo;
315
+ }
316
+
317
+ function sortEdgesForContractHashTrunk(
318
+ edges: ClassifiedEdge[],
319
+ contractHash: string | undefined,
320
+ ): ClassifiedEdge[] {
321
+ if (
322
+ contractHash === undefined ||
323
+ contractHash === EMPTY_CONTRACT_HASH ||
324
+ !isForwardLeaf(contractHash, edges)
325
+ ) {
326
+ return edges;
327
+ }
328
+
329
+ const preferredLeaf = contractHash;
330
+ const forwardTo = buildForwardToMap(edges);
331
+ const reachability = new Map<string, ReadonlySet<string>>();
332
+ function canReachContractHash(from: string): boolean {
333
+ let cached = reachability.get(from);
334
+ if (cached === undefined) {
335
+ cached = forwardReachableFrom(from, forwardTo);
336
+ reachability.set(from, cached);
337
+ }
338
+ return cached.has(preferredLeaf);
339
+ }
340
+
341
+ function trunkBias(edge: ClassifiedEdge): number {
342
+ if (edge.kind !== 'forward' || edge.from === edge.to) return 0;
343
+ if (edge.to === preferredLeaf) return 2;
344
+ if (canReachContractHash(edge.to)) return 1;
345
+ return 0;
346
+ }
347
+
348
+ return edges
349
+ .map((edge, index) => ({ edge, index, bias: trunkBias(edge) }))
350
+ .sort((a, b) => {
351
+ if (a.edge.from !== b.edge.from) return a.index - b.index;
352
+ if (a.bias !== b.bias) return b.bias - a.bias;
353
+ return a.index - b.index;
354
+ })
355
+ .map(({ edge }) => edge);
356
+ }
357
+
358
+ function rebuildEdgeLookupMaps(edges: readonly ClassifiedEdge[]): {
359
+ edgesByFrom: Map<string, ClassifiedEdge[]>;
360
+ edgesByTo: Map<string, ClassifiedEdge[]>;
361
+ } {
362
+ const edgesByFrom = new Map<string, ClassifiedEdge[]>();
363
+ const edgesByTo = new Map<string, ClassifiedEdge[]>();
364
+ for (const classified of edges) {
365
+ const fromBucket = edgesByFrom.get(classified.from);
366
+ if (fromBucket) fromBucket.push(classified);
367
+ else edgesByFrom.set(classified.from, [classified]);
368
+
369
+ const toBucket = edgesByTo.get(classified.to);
370
+ if (toBucket) toBucket.push(classified);
371
+ else edgesByTo.set(classified.to, [classified]);
372
+ }
373
+ return { edgesByFrom, edgesByTo };
374
+ }
375
+
265
376
  export function buildMigrationGraphRows(
266
377
  graph: MigrationGraph,
267
378
  options: BuildMigrationGraphRowsOptions = {},
@@ -284,31 +395,23 @@ export function buildMigrationGraphRows(
284
395
 
285
396
  // 2. Build classified edge list
286
397
  const edges: ClassifiedEdge[] = [];
287
- const edgesByFrom = new Map<string, ClassifiedEdge[]>();
288
- const edgesByTo = new Map<string, ClassifiedEdge[]>();
289
398
 
290
399
  for (const edgeList of graph.forwardChain.values()) {
291
400
  for (const edge of edgeList) {
292
401
  const kind = topology.kindByMigrationHash.get(edge.migrationHash) ?? 'forward';
293
- const classified: ClassifiedEdge = {
402
+ edges.push({
294
403
  migrationHash: edge.migrationHash,
295
404
  from: edge.from,
296
405
  to: edge.to,
297
406
  dirName: edge.dirName,
298
407
  kind,
299
- };
300
- edges.push(classified);
301
-
302
- const fromBucket = edgesByFrom.get(edge.from);
303
- if (fromBucket) fromBucket.push(classified);
304
- else edgesByFrom.set(edge.from, [classified]);
305
-
306
- const toBucket = edgesByTo.get(edge.to);
307
- if (toBucket) toBucket.push(classified);
308
- else edgesByTo.set(edge.to, [classified]);
408
+ });
309
409
  }
310
410
  }
311
411
 
412
+ const sortedEdges = sortEdgesForContractHashTrunk(edges, options.contractHash);
413
+ const { edgesByFrom, edgesByTo } = rebuildEdgeLookupMaps(sortedEdges);
414
+
312
415
  // 3. Find weakly-connected components (ordered: EMPTY first, then lex)
313
416
  const components = weaklyConnectedComponents(graph);
314
417
 
@@ -318,7 +421,12 @@ export function buildMigrationGraphRows(
318
421
  if (i > 0) nodes.push(null);
319
422
  const component = components[i];
320
423
  if (component === undefined) continue;
321
- const ordered = layerNodesByLongestForwardPath(component, topology, graph);
424
+ const ordered = layerNodesByLongestForwardPath(
425
+ component,
426
+ topology,
427
+ graph,
428
+ options.contractHash,
429
+ );
322
430
  for (const node of ordered) {
323
431
  nodes.push(node);
324
432
  }
@@ -332,5 +440,10 @@ export function buildMigrationGraphRows(
332
440
  nodes.unshift(detached);
333
441
  }
334
442
 
335
- return { nodes, edges, edgesByFrom, edgesByTo };
443
+ return {
444
+ nodes,
445
+ edges: sortedEdges,
446
+ edgesByFrom,
447
+ edgesByTo,
448
+ };
336
449
  }