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