@prisma-next/cli 0.12.0-dev.51 → 0.12.0-dev.53

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 (65) hide show
  1. package/dist/cli.mjs +7 -7
  2. package/dist/{client-DC-UlBLy.mjs → client-DIcitJdy.mjs} +113 -49
  3. package/dist/client-DIcitJdy.mjs.map +1 -0
  4. package/dist/commands/contract-infer.mjs +1 -1
  5. package/dist/commands/db-init.mjs +2 -2
  6. package/dist/commands/db-schema.mjs +1 -1
  7. package/dist/commands/db-sign.mjs +1 -1
  8. package/dist/commands/db-update.mjs +2 -2
  9. package/dist/commands/db-verify.mjs +1 -1
  10. package/dist/commands/migrate.d.mts +35 -1
  11. package/dist/commands/migrate.d.mts.map +1 -1
  12. package/dist/commands/migrate.mjs +287 -6
  13. package/dist/commands/migrate.mjs.map +1 -1
  14. package/dist/commands/migration-check.mjs +2 -2
  15. package/dist/commands/migration-graph.d.mts.map +1 -1
  16. package/dist/commands/migration-graph.mjs +4 -2
  17. package/dist/commands/migration-graph.mjs.map +1 -1
  18. package/dist/commands/migration-list.d.mts +1 -0
  19. package/dist/commands/migration-list.d.mts.map +1 -1
  20. package/dist/commands/migration-list.mjs +1 -1
  21. package/dist/commands/migration-log.mjs +1 -1
  22. package/dist/commands/migration-show.mjs +2 -2
  23. package/dist/commands/migration-status.d.mts.map +1 -1
  24. package/dist/commands/migration-status.mjs +2 -2
  25. package/dist/{contract-infer-DpGN9SAj.mjs → contract-infer-BAdhYGQH.mjs} +2 -2
  26. package/dist/{contract-infer-DpGN9SAj.mjs.map → contract-infer-BAdhYGQH.mjs.map} +1 -1
  27. package/dist/{db-verify-Cq16Obsw.mjs → db-verify-CiUCDXnv.mjs} +2 -2
  28. package/dist/{db-verify-Cq16Obsw.mjs.map → db-verify-CiUCDXnv.mjs.map} +1 -1
  29. package/dist/exports/control-api.mjs +1 -1
  30. package/dist/{inspect-live-schema-CRDKTNcf.mjs → inspect-live-schema-DegaqKFT.mjs} +2 -2
  31. package/dist/{inspect-live-schema-CRDKTNcf.mjs.map → inspect-live-schema-DegaqKFT.mjs.map} +1 -1
  32. package/dist/{migration-check-BxWlQBOs.mjs → migration-check-B2ccCHe7.mjs} +3 -3
  33. package/dist/{migration-check-BxWlQBOs.mjs.map → migration-check-B2ccCHe7.mjs.map} +1 -1
  34. package/dist/{migration-command-scaffold-BDd9abqW.mjs → migration-command-scaffold-D6UeN71F.mjs} +2 -2
  35. package/dist/{migration-command-scaffold-BDd9abqW.mjs.map → migration-command-scaffold-D6UeN71F.mjs.map} +1 -1
  36. package/dist/{migration-graph-space-render-CeNXh_Wy.mjs → migration-graph-space-render-B0HkTNj3.mjs} +488 -84
  37. package/dist/migration-graph-space-render-B0HkTNj3.mjs.map +1 -0
  38. package/dist/{migration-list-vJWFuXca.mjs → migration-list-mYmj2j33.mjs} +6 -4
  39. package/dist/migration-list-mYmj2j33.mjs.map +1 -0
  40. package/dist/{migration-log-6rcHQSI4.mjs → migration-log-Dzs18GU7.mjs} +3 -3
  41. package/dist/{migration-log-6rcHQSI4.mjs.map → migration-log-Dzs18GU7.mjs.map} +1 -1
  42. package/dist/{migration-path-target-UkxkgXnv.mjs → migration-path-target-DK-B7POa.mjs} +1 -1
  43. package/dist/{migration-path-target-UkxkgXnv.mjs.map → migration-path-target-DK-B7POa.mjs.map} +1 -1
  44. package/dist/{migration-status-Bjv91dE7.mjs → migration-status-BT9eCQsf.mjs} +8 -5
  45. package/dist/migration-status-BT9eCQsf.mjs.map +1 -0
  46. package/dist/{schemas-DJY2O09F.mjs → schemas-B4xeMrNt.mjs} +1 -1
  47. package/dist/{schemas-DJY2O09F.mjs.map → schemas-B4xeMrNt.mjs.map} +1 -1
  48. package/dist/types-qV41eEXH.d.mts.map +1 -1
  49. package/package.json +18 -18
  50. package/src/commands/migrate.ts +512 -2
  51. package/src/commands/migration-graph.ts +2 -0
  52. package/src/commands/migration-list.ts +3 -0
  53. package/src/commands/migration-status.ts +4 -0
  54. package/src/control-api/operations/db-run.ts +14 -3
  55. package/src/control-api/operations/db-verify.ts +15 -5
  56. package/src/control-api/operations/migrate.ts +149 -56
  57. package/src/utils/formatters/migration-graph-layout.ts +187 -42
  58. package/src/utils/formatters/migration-graph-space-render.ts +11 -1
  59. package/src/utils/formatters/migration-graph-tree-render.ts +609 -59
  60. package/src/utils/formatters/migration-list-render.ts +12 -0
  61. package/src/utils/formatters/migration-list-styler.ts +5 -7
  62. package/dist/client-DC-UlBLy.mjs.map +0 -1
  63. package/dist/migration-graph-space-render-CeNXh_Wy.mjs.map +0 -1
  64. package/dist/migration-list-vJWFuXca.mjs.map +0 -1
  65. package/dist/migration-status-Bjv91dE7.mjs.map +0 -1
@@ -12,18 +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-land-tee' }
25
- | { readonly kind: 'arc-crossing' }
26
- | { 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 }
27
33
  | {
28
34
  readonly kind: 'edge-lane';
29
35
  readonly migrationHash: string;
@@ -337,6 +343,15 @@ function refineAdjacency(
337
343
  row.convergenceProducer ?? false,
338
344
  divergenceBranchEdge,
339
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
+ }
340
355
  return {
341
356
  ...row,
342
357
  cells: buildEdgeCells(
@@ -345,6 +360,7 @@ function refineAdjacency(
345
360
  row.passThroughLanes ?? [],
346
361
  adjacency,
347
362
  row.cells.length,
363
+ existingLaneEdge,
348
364
  ),
349
365
  };
350
366
  });
@@ -377,30 +393,50 @@ function emptyCells(width: number): StructuralCell[] {
377
393
  return Array.from({ length: width }, () => ({ kind: 'empty' as const }));
378
394
  }
379
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
+
380
406
  function buildBranchConnectorCells(
381
407
  startLane: number,
382
408
  endLane: number,
383
409
  fanTargetLanes: ReadonlySet<number>,
384
410
  activeLanes: ReadonlySet<number>,
385
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>,
386
418
  ): StructuralCell[] {
387
419
  const cells = emptyCells(gridWidth);
388
420
  for (let lane = 0; lane < gridWidth; lane++) {
389
421
  if (activeLanes.has(lane) && (lane < startLane || lane > endLane)) {
390
- cells[lane] = { kind: 'vertical-pass' };
422
+ cells[lane] = { kind: 'vertical-pass', ...hashProp(laneEdgeByIndex.get(lane)) };
391
423
  continue;
392
424
  }
393
425
  if (lane === startLane) {
394
- cells[lane] = { kind: 'branch-tee' };
426
+ cells[lane] = { kind: 'branch-tee', ...hashProp(trunkEdgeHash) };
395
427
  } else if (lane === endLane) {
396
- cells[lane] = { kind: 'branch-corner' };
428
+ cells[lane] = { kind: 'branch-corner', ...hashProp(fanEdgeHashByLane.get(lane)) };
397
429
  } else if (lane > startLane && lane < endLane) {
398
430
  if (fanTargetLanes.has(lane)) {
399
- cells[lane] = { kind: 'branch-tee' };
431
+ cells[lane] = { kind: 'branch-tee', ...hashProp(fanEdgeHashByLane.get(lane)) };
400
432
  } else if (activeLanes.has(lane)) {
401
- cells[lane] = { kind: 'arc-crossing' };
433
+ cells[lane] = {
434
+ kind: 'arc-crossing',
435
+ ...hashProp(laneEdgeByIndex.get(lane)),
436
+ ...arcHashProp(fanEdgeHashByLane.get(endLane)),
437
+ };
402
438
  } else {
403
- cells[lane] = { kind: 'branch-tee' };
439
+ cells[lane] = { kind: 'branch-tee', ...hashProp(fanEdgeHashByLane.get(lane)) };
404
440
  }
405
441
  }
406
442
  }
@@ -413,24 +449,30 @@ function buildMergeConnectorCells(
413
449
  fanTargetLanes: ReadonlySet<number>,
414
450
  activeLanes: ReadonlySet<number>,
415
451
  gridWidth: number,
452
+ /** Hash of the edge occupying each active lane (fan lanes + pass-throughs). */
453
+ laneEdgeByIndex: ReadonlyMap<number, string>,
416
454
  ): StructuralCell[] {
417
455
  const cells = emptyCells(gridWidth);
418
456
  for (let lane = 0; lane < gridWidth; lane++) {
419
457
  if (activeLanes.has(lane) && (lane < startLane || lane > endLane)) {
420
- cells[lane] = { kind: 'vertical-pass' };
458
+ cells[lane] = { kind: 'vertical-pass', ...hashProp(laneEdgeByIndex.get(lane)) };
421
459
  continue;
422
460
  }
423
461
  if (lane === startLane) {
424
- cells[lane] = { kind: 'merge-tee' };
462
+ cells[lane] = { kind: 'merge-tee', ...hashProp(laneEdgeByIndex.get(lane)) };
425
463
  } else if (lane === endLane) {
426
- cells[lane] = { kind: 'merge-corner' };
464
+ cells[lane] = { kind: 'merge-corner', ...hashProp(laneEdgeByIndex.get(lane)) };
427
465
  } else if (lane > startLane && lane < endLane) {
428
466
  if (fanTargetLanes.has(lane)) {
429
- cells[lane] = { kind: 'merge-tee' };
467
+ cells[lane] = { kind: 'merge-tee', ...hashProp(laneEdgeByIndex.get(lane)) };
430
468
  } else if (activeLanes.has(lane)) {
431
- cells[lane] = { kind: 'arc-crossing' };
469
+ cells[lane] = {
470
+ kind: 'arc-crossing',
471
+ ...hashProp(laneEdgeByIndex.get(lane)),
472
+ ...arcHashProp(laneEdgeByIndex.get(endLane)),
473
+ };
432
474
  } else {
433
- cells[lane] = { kind: 'horizontal-pass' };
475
+ cells[lane] = { kind: 'horizontal-pass', ...hashProp(laneEdgeByIndex.get(startLane)) };
434
476
  }
435
477
  }
436
478
  }
@@ -442,11 +484,13 @@ function buildNodeCells(
442
484
  nodeColumn: number,
443
485
  activeLanes: readonly number[],
444
486
  gridWidth: number,
487
+ /** Hash of the edge occupying each active pass-through lane. */
488
+ laneEdgeByIndex: ReadonlyMap<number, string>,
445
489
  ): StructuralCell[] {
446
490
  const cells = emptyCells(gridWidth);
447
491
  for (const lane of activeLanes) {
448
492
  if (lane !== nodeColumn && lane < gridWidth) {
449
- cells[lane] = { kind: 'vertical-pass' };
493
+ cells[lane] = { kind: 'vertical-pass', ...hashProp(laneEdgeByIndex.get(lane)) };
450
494
  }
451
495
  }
452
496
  if (nodeColumn < gridWidth) {
@@ -461,10 +505,14 @@ function buildEdgeCells(
461
505
  passThroughLanes: readonly number[],
462
506
  adjacency: EdgeAdjacency,
463
507
  gridWidth: number,
508
+ /** Hash of the edge occupying each active pass-through lane. */
509
+ laneEdgeByIndex: ReadonlyMap<number, string>,
464
510
  ): StructuralCell[] {
465
511
  const cells = emptyCells(gridWidth);
466
512
  for (const lane of passThroughLanes) {
467
- if (lane < gridWidth) cells[lane] = { kind: 'vertical-pass' };
513
+ if (lane < gridWidth) {
514
+ cells[lane] = { kind: 'vertical-pass', ...hashProp(laneEdgeByIndex.get(lane)) };
515
+ }
468
516
  }
469
517
  if (laneIndex < gridWidth) {
470
518
  cells[laneIndex] = {
@@ -713,6 +761,8 @@ function applySkipRollbackRouting(
713
761
  .map((other) => other.backLane);
714
762
  const maxCoLandingLane = Math.max(...coLandingLanes);
715
763
 
764
+ const { migrationHash: arcHash } = edge;
765
+
716
766
  const sourceRow = result[sourceRowIndex];
717
767
  if (sourceRow !== undefined) {
718
768
  const cells = sourceRow.cells;
@@ -721,7 +771,12 @@ function applySkipRollbackRouting(
721
771
  cells[nodeCol] = { kind: 'node', contractHash, arcTee: true };
722
772
  for (let lane = nodeCol + 1; lane < backLane; lane += 1) {
723
773
  if (coSourcedLanes.includes(lane)) {
724
- 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
+ };
725
780
  continue;
726
781
  }
727
782
  const existing = cells[lane];
@@ -734,14 +789,30 @@ function applySkipRollbackRouting(
734
789
  occupied ||
735
790
  routes.some(
736
791
  (other) =>
737
- other.edge.migrationHash !== edge.migrationHash &&
792
+ other.edge.migrationHash !== arcHash &&
738
793
  other.backLane === lane &&
739
794
  routeCrossesRow(other, sourceRowIndex, result),
740
795
  );
741
- 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
+ }
742
811
  }
743
812
  cells[backLane] =
744
- 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 };
745
816
  }
746
817
 
747
818
  const edgeRow = result[edgeRowIndex];
@@ -750,10 +821,17 @@ function applySkipRollbackRouting(
750
821
  // lane may already cross this row, and rebuilding would clobber it.
751
822
  const cells = edgeRow.cells;
752
823
  ensureCellWidth(cells, backLane + 1);
753
- 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) };
754
832
  cells[backLane] = {
755
833
  kind: 'edge-lane',
756
- migrationHash: edge.migrationHash,
834
+ migrationHash: arcHash,
757
835
  edgeKind: edge.kind,
758
836
  ownsLabel: true,
759
837
  adjacency: 'node-skipping-rollback',
@@ -780,7 +858,7 @@ function applySkipRollbackRouting(
780
858
  existing?.kind !== 'arc-branch-tee' &&
781
859
  existing?.kind !== 'arc-crossing'
782
860
  ) {
783
- cells[backLane] = { kind: 'vertical-pass' };
861
+ cells[backLane] = { kind: 'vertical-pass', migrationHash: arcHash };
784
862
  }
785
863
  }
786
864
 
@@ -794,7 +872,9 @@ function applySkipRollbackRouting(
794
872
  // An inner converging arc's own landing junction: the outer arcs' bridge
795
873
  // passes through it (`┴`) while its own vertical run closes here.
796
874
  if (coLandingLanes.includes(lane)) {
797
- cells[lane] = { kind: 'arc-land-tee' };
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) };
798
878
  continue;
799
879
  }
800
880
  // A bridged lane that carries another arc OR a forward vertical still
@@ -811,16 +891,30 @@ function applySkipRollbackRouting(
811
891
  occupied ||
812
892
  routes.some(
813
893
  (other) =>
814
- other.edge.migrationHash !== edge.migrationHash &&
894
+ other.edge.migrationHash !== arcHash &&
815
895
  other.backLane === lane &&
816
896
  routeCrossesRow(other, targetRowIndex, result),
817
897
  );
818
- 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
+ }
819
911
  }
820
912
  // Inner converging arcs close as a landing tee so the outermost arc's
821
913
  // bridge reads through to the node; only the outermost arc draws `╯`.
822
914
  cells[backLane] =
823
- backLane < maxCoLandingLane ? { kind: 'arc-land-tee' } : { kind: 'arc-land-corner' };
915
+ backLane < maxCoLandingLane
916
+ ? { kind: 'arc-land-tee', migrationHash: arcHash }
917
+ : { kind: 'arc-land-corner', migrationHash: arcHash };
824
918
  for (const other of routes) {
825
919
  if (other.backLane <= backLane) continue;
826
920
  if (!routeCrossesRow(other, targetRowIndex, result)) continue;
@@ -832,7 +926,12 @@ function applySkipRollbackRouting(
832
926
  existing?.kind !== 'arc-land-bridge' &&
833
927
  existing?.kind !== 'node'
834
928
  ) {
835
- 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
+ };
836
935
  }
837
936
  }
838
937
  }
@@ -914,6 +1013,9 @@ function layoutComponent(
914
1013
  const nodeColumn = new Map<string, number>();
915
1014
  const edgeColumn = new Map<string, number>();
916
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>();
917
1019
  let gridWidth = 1;
918
1020
 
919
1021
  function ensureGridWidth(minWidth: number): void {
@@ -965,7 +1067,14 @@ function layoutComponent(
965
1067
  startLane,
966
1068
  endLane,
967
1069
  branchCount: laneIndices.length,
968
- cells: buildMergeConnectorCells(startLane, endLane, fanTargetLanes, activeLanes, gridWidth),
1070
+ cells: buildMergeConnectorCells(
1071
+ startLane,
1072
+ endLane,
1073
+ fanTargetLanes,
1074
+ activeLanes,
1075
+ gridWidth,
1076
+ laneEdgeByIndex,
1077
+ ),
969
1078
  });
970
1079
  for (const index of laneIndices) {
971
1080
  if (index !== startLane) setLane(index, null);
@@ -979,9 +1088,15 @@ function layoutComponent(
979
1088
  endLane: number,
980
1089
  branchCount: number,
981
1090
  fanTargetLanes: readonly number[],
1091
+ /** Hash of the first/representative edge for each fan lane (keyed by lane index). */
1092
+ fanEdgeHashByLane: ReadonlyMap<number, string>,
982
1093
  ): void {
983
1094
  ensureGridWidth(endLane + 1);
984
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);
985
1100
  rows.push({
986
1101
  kind: 'branch-connector',
987
1102
  contractHash,
@@ -994,6 +1109,9 @@ function layoutComponent(
994
1109
  new Set(fanTargetLanes),
995
1110
  activeLanes,
996
1111
  gridWidth,
1112
+ trunkEdgeHash,
1113
+ fanEdgeHashByLane,
1114
+ laneEdgeByIndex,
997
1115
  ),
998
1116
  });
999
1117
  }
@@ -1007,11 +1125,14 @@ function layoutComponent(
1007
1125
  edge,
1008
1126
  laneIndex: lane,
1009
1127
  passThroughLanes: passThrough,
1010
- cells: buildEdgeCells(edge, lane, passThrough, adjacency, gridWidth),
1128
+ cells: buildEdgeCells(edge, lane, passThrough, adjacency, gridWidth, laneEdgeByIndex),
1011
1129
  };
1012
1130
  rows.push(convergenceProducer ? { ...row, convergenceProducer: true } : row);
1013
1131
  edgeColumn.set(edge.migrationHash, lane);
1014
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);
1015
1136
  }
1016
1137
 
1017
1138
  function emitNodeRow(contractHash: string, column: number): void {
@@ -1020,7 +1141,7 @@ function layoutComponent(
1020
1141
  rows.push({
1021
1142
  kind: 'node',
1022
1143
  contractHash,
1023
- cells: buildNodeCells(contractHash, column, passThrough, gridWidth),
1144
+ cells: buildNodeCells(contractHash, column, passThrough, gridWidth, laneEdgeByIndex),
1024
1145
  });
1025
1146
  nodeColumn.set(contractHash, column);
1026
1147
  }
@@ -1090,7 +1211,31 @@ function layoutComponent(
1090
1211
 
1091
1212
  if (groups.length >= 2) {
1092
1213
  const endLane = Math.max(...laneForGroup);
1093
- emitBranchConnector(node, column, endLane, groups.length, laneForGroup);
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
+ }
1094
1239
  }
1095
1240
 
1096
1241
  for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
@@ -44,6 +44,14 @@ export interface RenderMigrationGraphSpaceTreeInput {
44
44
  readonly styler?: MigrationListStyler;
45
45
  readonly globalMaxEdgeTreePrefixWidth?: number;
46
46
  readonly globalMaxDirNameWidth?: number;
47
+ /**
48
+ * Whether this render is for the app space. When false, `contractHash` is
49
+ * not forwarded to `buildMigrationGraphRows` (suppressing the floating
50
+ * working-contract node) and `isAppSpace: false` is passed to
51
+ * `renderMigrationGraphTree` (suppressing the `@contract` marker).
52
+ * Defaults to `true` so single-space callers are unaffected.
53
+ */
54
+ readonly isAppSpace?: boolean;
47
55
  }
48
56
 
49
57
  export interface ComputeGlobalMaxEdgeTreePrefixWidthInput {
@@ -80,8 +88,9 @@ export function computeGlobalMaxDirNameWidth(
80
88
  }
81
89
 
82
90
  function renderMigrationGraphSpaceTreeInternal(input: RenderMigrationGraphSpaceTreeInput): string {
91
+ const appSpace = input.isAppSpace !== false;
83
92
  const rowModel = buildMigrationGraphRows(input.graph, {
84
- contractHash: input.liveContractHash,
93
+ ...(appSpace ? { contractHash: input.liveContractHash } : {}),
85
94
  });
86
95
  const layout = buildMigrationGraphLayout(rowModel);
87
96
  const listOverlay = buildEdgeAnnotationsByHashFromListEntries(input.migrations);
@@ -92,6 +101,7 @@ function renderMigrationGraphSpaceTreeInternal(input: RenderMigrationGraphSpaceT
92
101
  return renderMigrationGraphTree(layout, {
93
102
  refsByHash: input.refsByHash ?? buildRefsByHashFromListEntries(input.migrations),
94
103
  contractHash: input.liveContractHash,
104
+ isAppSpace: appSpace,
95
105
  edgeAnnotationsByHash,
96
106
  colorize: input.colorize,
97
107
  glyphMode: input.glyphMode,