diff --git a/gittensor/validator/oss_contributions/label_resolution.py b/gittensor/validator/oss_contributions/label_resolution.py index 6bfff306..34a9dfcf 100644 --- a/gittensor/validator/oss_contributions/label_resolution.py +++ b/gittensor/validator/oss_contributions/label_resolution.py @@ -18,7 +18,15 @@ def get_default_label_multiplier(repo_config: Optional[RepositoryConfig]) -> flo def get_label_multiplier(label: str, repo_config: Optional[RepositoryConfig]) -> Optional[float]: - """Return the highest configured multiplier matching a label, or None.""" + """Return the configured multiplier matching a label, or None if unmatched. + + A configured multiplier of ``0.0`` is a veto: repos use it to mark rejection + / void labels (``eval:REJECT``, ``invalid-pr``, ``duplicate-pr``, ``slop``, + ``kata:invalid``, ...). So a label matching any ``0.0`` pattern resolves to + ``0.0`` even when it also matches a higher-valued pattern — the veto is not + overridden by a co-matching reward pattern. Among purely positive matches the + highest wins, preserving the "best classification" behavior. + """ if repo_config is None or not repo_config.label_multipliers: return None @@ -28,14 +36,25 @@ def get_label_multiplier(label: str, repo_config: Optional[RepositoryConfig]) -> for pattern, multiplier in repo_config.label_multipliers.items() if fnmatch(label_lower, pattern.lower()) ] - return max(matches) if matches else None + if not matches: + return None + if any(multiplier == 0.0 for multiplier in matches): + return 0.0 + return max(matches) def resolve_highest_label_multiplier( labels: Iterable[str], repo_config: Optional[RepositoryConfig], ) -> tuple[Optional[str], float]: - """Resolve the highest-multiplier label from unordered candidate labels.""" + """Resolve the scoring multiplier from unordered candidate labels. + + A label configured to ``0.0`` is a veto (rejection / void marker) and must + not be overridden by a co-applied higher-value label: if any candidate + resolves to ``0.0`` the result is ``0.0``. Otherwise the highest positive + multiplier wins. Ties (including ties among vetoes) break deterministically + by label name so the result is independent of mirror response ordering. + """ default_multiplier = get_default_label_multiplier(repo_config) candidates = [] for label in labels: @@ -46,6 +65,10 @@ def resolve_highest_label_multiplier( if not candidates: return None, default_multiplier + vetoes = [candidate for candidate in candidates if candidate[1] == 0.0] + if vetoes: + return min(vetoes, key=lambda candidate: candidate[0]) + label, multiplier = max(candidates, key=lambda candidate: (candidate[1], candidate[0])) return label, multiplier diff --git a/tests/validator/test_label_veto.py b/tests/validator/test_label_veto.py new file mode 100644 index 00000000..45d141e5 --- /dev/null +++ b/tests/validator/test_label_veto.py @@ -0,0 +1,118 @@ +# The MIT License (MIT) +# Copyright © 2025 Entrius + +"""Veto-label semantics for repository label multipliers. + +A label configured with multiplier ``0.0`` is a rejection / void marker +(``eval:REJECT``, ``invalid-pr``, ``duplicate-pr``, ``slop``, ``kata:invalid``, +...). It must zero the PR's label multiplier even when a higher-valued label is +applied alongside it, otherwise a maintainer can never actually revoke a PR that +already earned a reward label. +""" + +import pytest + +from gittensor.validator.oss_contributions.label_resolution import ( + get_label_multiplier, + resolve_highest_label_multiplier, +) +from gittensor.validator.utils.load_weights import RepositoryConfig + + +class TestVetoBeatsRewardLabel: + """A 0.0 label co-applied with a positive label wins (result is 0.0).""" + + def test_reject_label_vetoes_eval_reward(self): + # Mirrors the sparkinfer / SparkDistill / cuda-compute-oss registries. + config = RepositoryConfig( + emission_share=1.0, + label_multipliers={'eval:L': 2.5, 'eval:REJECT': 0.0}, + ) + label, multiplier = resolve_highest_label_multiplier(['eval:L', 'eval:REJECT'], config) + assert multiplier == pytest.approx(0.0) + assert label == 'eval:REJECT' + + def test_invalid_pr_vetoes_round_winner(self): + # Mirrors the gt-imagent registry (round-winner 10.0, invalid-pr 0.0). + config = RepositoryConfig( + emission_share=1.0, + label_multipliers={'round-winner': 10.0, 'invalid-pr': 0.0, 'duplicate-pr': 0.0}, + ) + _, multiplier = resolve_highest_label_multiplier(['round-winner', 'invalid-pr'], config) + assert multiplier == pytest.approx(0.0) + + def test_slop_vetoes_priority(self): + # Mirrors the JSONbored registries (gittensor:priority 1.5, slop 0.0). + config = RepositoryConfig( + emission_share=1.0, + label_multipliers={'gittensor:priority': 1.5, 'slop': 0.0}, + ) + _, multiplier = resolve_highest_label_multiplier(['gittensor:priority', 'slop'], config) + assert multiplier == pytest.approx(0.0) + + def test_veto_independent_of_label_order(self): + config = RepositoryConfig( + emission_share=1.0, + label_multipliers={'eval:L': 2.5, 'eval:REJECT': 0.0}, + ) + forward = resolve_highest_label_multiplier(['eval:L', 'eval:REJECT'], config) + reverse = resolve_highest_label_multiplier(['eval:REJECT', 'eval:L'], config) + assert forward == reverse + assert forward[1] == pytest.approx(0.0) + + def test_wildcard_veto_beats_wildcard_reward(self): + # Mirrors the kata registry (kata:winner:* 0.7, kata:invalid 0.0) but + # exercises the wildcard path on both sides. + config = RepositoryConfig( + emission_share=1.0, + label_multipliers={'kata:winner:*': 0.7, 'kata:defeat:*': 0.0}, + ) + _, multiplier = resolve_highest_label_multiplier(['kata:winner:s1', 'kata:defeat:s1'], config) + assert multiplier == pytest.approx(0.0) + + +class TestSingleLabelMatchingBothVetoAndReward: + """One label matching a 0.0 pattern and a positive pattern resolves to 0.0.""" + + def test_catch_all_reward_plus_specific_veto(self): + # A repo adds an `eval:*` catch-all alongside an explicit `eval:REJECT` + # veto; the single label `eval:REJECT` matches both patterns. + config = RepositoryConfig( + emission_share=1.0, + label_multipliers={'eval:*': 3.0, 'eval:REJECT': 0.0}, + ) + assert get_label_multiplier('eval:REJECT', config) == pytest.approx(0.0) + # A non-veto label under the same catch-all still scores. + assert get_label_multiplier('eval:L', config) == pytest.approx(3.0) + + +class TestNonVetoBehaviorUnchanged: + """Positive-only resolution keeps its previous 'highest wins' semantics.""" + + def test_highest_positive_still_wins(self): + config = RepositoryConfig( + emission_share=1.0, + label_multipliers={'bug': 1.2, 'enhancement': 1.3, 'feature': 1.0}, + ) + label, multiplier = resolve_highest_label_multiplier(['bug', 'enhancement'], config) + assert label == 'enhancement' + assert multiplier == pytest.approx(1.3) + + def test_low_positive_is_not_a_veto(self): + # 0.05 is a low reward, not a rejection: it must not veto a higher label. + config = RepositoryConfig( + emission_share=1.0, + label_multipliers={'mult:contribution': 0.05, 'perf:l': 2.5}, + ) + _, multiplier = resolve_highest_label_multiplier(['mult:contribution', 'perf:l'], config) + assert multiplier == pytest.approx(2.5) + + def test_default_multiplier_used_when_no_pattern_matches(self): + config = RepositoryConfig( + emission_share=1.0, + label_multipliers={'bug': 1.2}, + default_label_multiplier=0.8, + ) + label, multiplier = resolve_highest_label_multiplier(['unrelated'], config) + assert label is None + assert multiplier == pytest.approx(0.8)