From b106a0d6ce9c096efa8ca331c83f30bc59fbe799 Mon Sep 17 00:00:00 2001 From: bitloi Date: Thu, 14 May 2026 22:57:16 +0200 Subject: [PATCH] fix(issue-discovery): apply repository label policy to solving-PR scoring _mirror_issue_for_scoring resolved discovery_base_score, time_decay, and review_quality but never applied the repository label policy (default_label_multiplier / label_multipliers) from repo_config. This left the issue-discovery path inconsistent with OSS PR scoring since #1027. Fix: - Add Issue.discovery_label_multiplier (float, default 1.0) alongside the existing discovery_* multiplier fields in classes.py. - In _mirror_issue_for_scoring, resolve the label multiplier from solving_pr.labels using the same trusted_label_pipeline trust gate as _resolve_trusted_scoring_label in oss_contributions/mirror/scoring.py. - Multiply discovery_label_multiplier into discovery_earned_score in the scoring loop. repos with default_label_multiplier: 0.0 (e.g. entrius/oc-1, which is scheduled to receive issue_discovery_share > 0 imminently) will now correctly produce discovery_earned_score == 0.0 for unlabelled solving PRs. Downweight labels (refactor: 0.25 in entrius/allways, entrius/gittensor, etc.) are also now applied consistently across both scoring paths. Tests: add TestMirrorIssueForScoringLabelMultiplier (unit) and TestLabelPolicyIssueDiscovery (integration) covering zero-default/no-label, matching-label override, downweight, and untrusted-actor gate cases. --- gittensor/classes.py | 1 + gittensor/validator/issue_discovery/scan.py | 11 ++ tests/validator/issue_discovery/test_scan.py | 164 ++++++++++++++++++- 3 files changed, 175 insertions(+), 1 deletion(-) diff --git a/gittensor/classes.py b/gittensor/classes.py index 77235bf3..2b954d0e 100644 --- a/gittensor/classes.py +++ b/gittensor/classes.py @@ -152,6 +152,7 @@ class Issue: discovery_time_decay_multiplier: float = 1.0 discovery_credibility_multiplier: float = 1.0 discovery_open_issue_spam_multiplier: float = 1.0 + discovery_label_multiplier: float = 1.0 @property def is_transferred(self) -> bool: diff --git a/gittensor/validator/issue_discovery/scan.py b/gittensor/validator/issue_discovery/scan.py index c30a12e3..de234acb 100644 --- a/gittensor/validator/issue_discovery/scan.py +++ b/gittensor/validator/issue_discovery/scan.py @@ -52,6 +52,7 @@ calculate_open_issue_spam_multiplier, check_issue_eligibility, ) +from gittensor.validator.oss_contributions.label_resolution import resolve_highest_label_multiplier from gittensor.validator.oss_contributions.mirror.adapters import mirror_files_to_legacy from gittensor.validator.oss_contributions.mirror.scoring import ( calculate_base_score_for_pr_files, @@ -555,6 +556,7 @@ def _finalize_repo_issue_scores( issue.discovery_open_issue_spam_multiplier = spam_mult issue.discovery_earned_score = round( issue.discovery_base_score + * issue.discovery_label_multiplier * issue.discovery_time_decay_multiplier * issue.discovery_review_quality_multiplier * issue.discovery_credibility_multiplier @@ -753,4 +755,13 @@ def _mirror_issue_for_scoring( 2, ) + trusted = repo_config.trusted_label_pipeline + candidate_names = [ + (label.name or '').lower() + for label in solving_pr.labels + if label.name and (trusted or label.actor_association in MAINTAINER_ASSOCIATIONS) + ] + _, label_multiplier = resolve_highest_label_multiplier(candidate_names, repo_config) + adapted.discovery_label_multiplier = label_multiplier + return adapted diff --git a/tests/validator/issue_discovery/test_scan.py b/tests/validator/issue_discovery/test_scan.py index bc994c67..24454fe7 100644 --- a/tests/validator/issue_discovery/test_scan.py +++ b/tests/validator/issue_discovery/test_scan.py @@ -25,11 +25,13 @@ run_issue_discovery = scan_module.run_issue_discovery _classify_issue = scan_module._classify_issue _build_solving_pr_cache = scan_module._build_solving_pr_cache +_mirror_issue_for_scoring = scan_module._mirror_issue_for_scoring CachedSolvingPR = scan_module.CachedSolvingPR MirrorIssue = mirror_models.MirrorIssue MirrorIssuesResponse = mirror_models.MirrorIssuesResponse MirrorPullRequest = mirror_models.MirrorPullRequest MirrorPullRequestFilesResponse = mirror_models.MirrorPullRequestFilesResponse +MirrorSolvingPR = mirror_models.MirrorSolvingPR MirrorRequestError = mirror_client_mod.MirrorRequestError MinerEvaluation = classes.MinerEvaluation MinerEvaluationCache = classes.MinerEvaluationCache @@ -112,6 +114,7 @@ def _issue_dict( repo: str = 'entrius/gittensor-ui', created_at: str = '2026-04-01T00:00:00Z', author_association: str = 'CONTRIBUTOR', + solving_pr_labels: Optional[list] = None, ) -> dict: sp = None if solved_by_pr: @@ -125,7 +128,7 @@ def _issue_dict( 'head_sha': 'h', 'base_sha': 'b', 'merge_base_sha': 'mb', - 'labels': [], + 'labels': solving_pr_labels or [], 'review_summary': {'maintainer_changes_requested_count': 0}, } return { @@ -1435,3 +1438,162 @@ def test_dev_mode_bypasses_maintainer_skip(self, monkeypatch): ) assert eval_.total_solved_issues == 1 + + +# ============================================================================ +# Repository label policy applied to solving-PR discovery scoring +# ============================================================================ + + +class TestMirrorIssueForScoringLabelMultiplier: + """Unit tests: _mirror_issue_for_scoring resolves discovery_label_multiplier + from solving_pr.labels using the same trust-gate logic as OSS PR scoring.""" + + def test_zero_default_multiplier_applied_when_no_labels(self): + issue = MirrorIssue.from_dict(_issue_dict()) + repo_config = RepositoryConfig( + emission_share=0.5, + trusted_label_pipeline=True, + default_label_multiplier=0.0, + label_multipliers={'benchmark-improvement': 1.0}, + ) + result = _mirror_issue_for_scoring(issue, issue.solving_pr, repo_config, base_score=1.0) + assert result is not None + assert result.discovery_label_multiplier == pytest.approx(0.0) + + def test_matching_label_overrides_zero_default_multiplier(self): + label = {'name': 'benchmark-improvement', 'actor_association': 'OWNER'} + issue = MirrorIssue.from_dict(_issue_dict(solving_pr_labels=[label])) + repo_config = RepositoryConfig( + emission_share=0.5, + trusted_label_pipeline=True, + default_label_multiplier=0.0, + label_multipliers={'benchmark-improvement': 1.0}, + ) + result = _mirror_issue_for_scoring(issue, issue.solving_pr, repo_config, base_score=1.0) + assert result is not None + assert result.discovery_label_multiplier == pytest.approx(1.0) + + def test_downweight_label_sets_multiplier(self): + label = {'name': 'refactor', 'actor_association': 'OWNER'} + issue = MirrorIssue.from_dict(_issue_dict(solving_pr_labels=[label])) + repo_config = RepositoryConfig( + emission_share=0.5, + trusted_label_pipeline=True, + label_multipliers={'refactor': 0.25}, + ) + result = _mirror_issue_for_scoring(issue, issue.solving_pr, repo_config, base_score=1.0) + assert result is not None + assert result.discovery_label_multiplier == pytest.approx(0.25) + + def test_untrusted_actor_label_falls_back_to_default_multiplier(self): + label = {'name': 'benchmark-improvement', 'actor_association': 'CONTRIBUTOR'} + issue = MirrorIssue.from_dict(_issue_dict(solving_pr_labels=[label])) + repo_config = RepositoryConfig( + emission_share=0.5, + trusted_label_pipeline=False, + default_label_multiplier=0.0, + label_multipliers={'benchmark-improvement': 1.0}, + ) + result = _mirror_issue_for_scoring(issue, issue.solving_pr, repo_config, base_score=1.0) + assert result is not None + assert result.discovery_label_multiplier == pytest.approx(0.0) + + +class TestLabelPolicyIssueDiscovery: + """Integration tests: repository label policy flows through run_issue_discovery + to issue_discovery_score via solving_pr.labels.""" + + def _seven_issues(self, solving_pr_labels=None): + return [ + _issue_dict(issue_number=50 + i, solved_by_pr=100 + i, solving_pr_labels=solving_pr_labels) + for i in range(7) + ] + + def _seed(self, uid=2, base_score=42.0): + seed = MinerEvaluation(uid=uid, hotkey='hk2', github_id='seed') + seed.merged_prs = [_scored_mirror_pr('entrius/gittensor-ui', 100 + i, base_score=base_score) for i in range(7)] + return seed + + def test_zero_default_multiplier_unlabeled_solving_prs_earn_zero_score(self): + client = Mock() + client.get_miner_issues.return_value = _response(self._seven_issues()) + eval_ = _eval() + seed = self._seed() + repo_config = RepositoryConfig( + emission_share=0.5, + trusted_label_pipeline=True, + default_label_multiplier=0.0, + label_multipliers={'benchmark-improvement': 1.0}, + ) + _run( + run_issue_discovery( + {1: eval_, 2: seed}, + {'entrius/gittensor-ui': repo_config}, + _EMPTY_LANGS, + _EMPTY_TOKEN_CONFIG, + client=client, + ) + ) + assert eval_.is_issue_eligible is True + assert eval_.issue_discovery_score == pytest.approx(0.0) + assert all(i.discovery_label_multiplier == pytest.approx(0.0) for i in eval_.issue_discovery_issues) + + def test_matching_label_earns_nonzero_score_with_zero_default_multiplier(self): + label = [{'name': 'benchmark-improvement', 'actor_association': 'OWNER'}] + client = Mock() + client.get_miner_issues.return_value = _response(self._seven_issues(solving_pr_labels=label)) + eval_ = _eval() + seed = self._seed() + repo_config = RepositoryConfig( + emission_share=0.5, + trusted_label_pipeline=True, + default_label_multiplier=0.0, + label_multipliers={'benchmark-improvement': 1.0}, + ) + _run( + run_issue_discovery( + {1: eval_, 2: seed}, + {'entrius/gittensor-ui': repo_config}, + _EMPTY_LANGS, + _EMPTY_TOKEN_CONFIG, + client=client, + ) + ) + assert eval_.is_issue_eligible is True + assert eval_.issue_discovery_score > 0.0 + assert all(i.discovery_label_multiplier == pytest.approx(1.0) for i in eval_.issue_discovery_issues) + + def test_downweight_label_reduces_discovery_score(self): + """A refactor=0.25 label on the solving PR sets discovery_label_multiplier=0.25 + on each scored issue and reduces the aggregate discovery score vs unlabeled.""" + label = [{'name': 'refactor', 'actor_association': 'OWNER'}] + repo_config = RepositoryConfig( + emission_share=0.5, + trusted_label_pipeline=True, + label_multipliers={'refactor': 0.25}, + ) + + def _run_discovery(issues): + client = Mock() + client.get_miner_issues.return_value = _response(issues) + ev = _eval(uid=1, github_id='999') + seed = MinerEvaluation(uid=2, hotkey='hk2', github_id='seed') + seed.merged_prs = [_scored_mirror_pr('entrius/gittensor-ui', 100 + i) for i in range(7)] + _run( + run_issue_discovery( + {1: ev, 2: seed}, + {'entrius/gittensor-ui': repo_config}, + _EMPTY_LANGS, + _EMPTY_TOKEN_CONFIG, + client=client, + ) + ) + return ev + + ev_unlabeled = _run_discovery(self._seven_issues()) + ev_labeled = _run_discovery(self._seven_issues(solving_pr_labels=label)) + + assert ev_unlabeled.issue_discovery_score > 0.0 + assert ev_labeled.issue_discovery_score < ev_unlabeled.issue_discovery_score + assert all(i.discovery_label_multiplier == pytest.approx(0.25) for i in ev_labeled.issue_discovery_issues)