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)