diff --git a/src/leakgauge/leaderboard.py b/src/leakgauge/leaderboard.py
index 621e71a..d1d78fc 100644
--- a/src/leakgauge/leaderboard.py
+++ b/src/leakgauge/leaderboard.py
@@ -100,8 +100,26 @@ def y(rank: int) -> int:
f''
)
- parts.append(f'')
- parts.append(f'')
+ parts.append(f'')
+ parts.append(f'')
+ # Explicit rank position at each endpoint, so the reader doesn't have to
+ # infer it from vertical order.
+ parts.append(
+ f'{h_rank}'
+ )
+ parts.append(
+ f'{l_rank}'
+ )
+ # A mover gets its rank delta with an up/down glyph; a null (tau = 1)
+ # shows none. Rank 1 is best, so a smaller leakage rank is a move up.
+ if moved:
+ arrow = "▲" if l_rank < h_rank else "▼"
+ parts.append(
+ f'{arrow}{abs(l_rank - h_rank)}'
+ )
name = html.escape(m)
parts.append(
f''
diff --git a/tests/test_leaderboard.py b/tests/test_leaderboard.py
index 68b57fe..6f2ee6e 100644
--- a/tests/test_leaderboard.py
+++ b/tests/test_leaderboard.py
@@ -87,3 +87,29 @@ def test_write_site_emits_index_html(tmp_path: Path) -> None:
path = write_site(summaries, rank_reorder(summaries), tmp_path)
assert path == tmp_path / "index.html"
assert path.read_text(encoding="utf-8").startswith("")
+
+
+def test_rank_numerals_and_delta_render_for_a_reorder() -> None:
+ # a hijacks more (hijack rank 1) but b leaks more (leakage rank 1): both move.
+ summaries = [_summary("prov:a", 0.9, 0.3), _summary("prov:b", 0.6, 0.7)]
+ reorder = rank_reorder(summaries)
+ assert reorder is not None
+
+ out = render_html(summaries, reorder)
+ # Explicit rank integers at the endpoints (white numerals on the nodes).
+ assert 'fill="#ffffff">1' in out
+ assert 'fill="#ffffff">2' in out
+ # Both models change rank, so an up and a down glyph with the delta appear.
+ assert "▲1" in out # ▲1 (moved up)
+ assert "▼1" in out # ▼1 (moved down)
+
+
+def test_no_rank_delta_glyph_when_ranks_agree() -> None:
+ # a hijacks AND leaks more -> ranks agree (tau = 1), nobody moves.
+ summaries = [_summary("prov:a", 0.9, 0.8), _summary("prov:b", 0.4, 0.3)]
+ reorder = rank_reorder(summaries)
+ assert reorder is not None
+
+ out = render_html(summaries, reorder)
+ assert 'fill="#ffffff">1' in out # ranks still numbered
+ assert "▲" not in out and "▼" not in out # but no movement glyph