Skip to content

Commit 92e42f8

Browse files
committed
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.
1 parent a1c1b82 commit 92e42f8

3 files changed

Lines changed: 182 additions & 1 deletion

File tree

gittensor/classes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ class Issue:
153153
discovery_time_decay_multiplier: float = 1.0
154154
discovery_credibility_multiplier: float = 1.0
155155
discovery_open_issue_spam_multiplier: float = 1.0
156+
discovery_label_multiplier: float = 1.0
156157

157158
@property
158159
def is_transferred(self) -> bool:

gittensor/validator/issue_discovery/scan.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141

4242
from gittensor.classes import Issue, MinerEvaluation, MinerEvaluationCache
4343
from gittensor.constants import (
44+
MAINTAINER_ASSOCIATIONS,
4445
MIN_TOKEN_SCORE_FOR_BASE_SCORE,
4546
PR_LOOKBACK_DAYS,
4647
)
@@ -51,6 +52,7 @@
5152
calculate_open_issue_spam_multiplier,
5253
check_issue_eligibility,
5354
)
55+
from gittensor.validator.oss_contributions.label_resolution import resolve_highest_label_multiplier
5456
from gittensor.validator.oss_contributions.mirror.adapters import mirror_files_to_legacy
5557
from gittensor.validator.oss_contributions.mirror.scoring import (
5658
calculate_base_score_for_pr_files,
@@ -469,6 +471,7 @@ async def _score_miner_issues(
469471
issue.discovery_open_issue_spam_multiplier = spam_mult
470472
issue.discovery_earned_score = round(
471473
issue.discovery_base_score
474+
* issue.discovery_label_multiplier
472475
* issue.discovery_time_decay_multiplier
473476
* issue.discovery_review_quality_multiplier
474477
* issue.discovery_credibility_multiplier
@@ -649,4 +652,13 @@ def _mirror_issue_for_scoring(
649652
2,
650653
)
651654

655+
trusted = repo_config.trusted_label_pipeline
656+
candidate_names = [
657+
(label.name or '').lower()
658+
for label in solving_pr.labels
659+
if label.name and (trusted or label.actor_association in MAINTAINER_ASSOCIATIONS)
660+
]
661+
_, label_multiplier = resolve_highest_label_multiplier(candidate_names, repo_config)
662+
adapted.discovery_label_multiplier = label_multiplier
663+
652664
return adapted

tests/validator/issue_discovery/test_scan.py

Lines changed: 169 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@
2525
run_issue_discovery = scan_module.run_issue_discovery
2626
_classify_issue = scan_module._classify_issue
2727
_build_solving_pr_cache = scan_module._build_solving_pr_cache
28+
_mirror_issue_for_scoring = scan_module._mirror_issue_for_scoring
2829
CachedSolvingPR = scan_module.CachedSolvingPR
2930
MirrorIssue = mirror_models.MirrorIssue
3031
MirrorIssuesResponse = mirror_models.MirrorIssuesResponse
3132
MirrorPullRequest = mirror_models.MirrorPullRequest
3233
MirrorPullRequestFilesResponse = mirror_models.MirrorPullRequestFilesResponse
34+
MirrorSolvingPR = mirror_models.MirrorSolvingPR
3335
MirrorRequestError = mirror_client_mod.MirrorRequestError
3436
MinerEvaluation = classes.MinerEvaluation
3537
MinerEvaluationCache = classes.MinerEvaluationCache
@@ -111,6 +113,7 @@ def _issue_dict(
111113
last_edited_at: Optional[str] = None,
112114
repo: str = 'entrius/gittensor-ui',
113115
created_at: str = '2026-04-01T00:00:00Z',
116+
solving_pr_labels: Optional[list] = None,
114117
) -> dict:
115118
sp = None
116119
if solved_by_pr:
@@ -124,7 +127,7 @@ def _issue_dict(
124127
'head_sha': 'h',
125128
'base_sha': 'b',
126129
'merge_base_sha': 'mb',
127-
'labels': [],
130+
'labels': solving_pr_labels or [],
128131
'review_summary': {'maintainer_changes_requested_count': 0},
129132
}
130133
return {
@@ -1349,3 +1352,168 @@ def _score_with_emission_share(emission_share: float) -> float:
13491352
return evaluation.issue_discovery_score
13501353

13511354
assert _score_with_emission_share(0.1) == pytest.approx(_score_with_emission_share(0.9))
1355+
1356+
1357+
# ============================================================================
1358+
# Repository label policy applied to solving-PR discovery scoring
1359+
# ============================================================================
1360+
1361+
1362+
class TestMirrorIssueForScoringLabelMultiplier:
1363+
"""Unit tests: _mirror_issue_for_scoring resolves discovery_label_multiplier
1364+
from solving_pr.labels using the same trust-gate logic as OSS PR scoring."""
1365+
1366+
def test_zero_default_multiplier_applied_when_no_labels(self):
1367+
issue = MirrorIssue.from_dict(_issue_dict())
1368+
repo_config = RepositoryConfig(
1369+
emission_share=0.5,
1370+
trusted_label_pipeline=True,
1371+
default_label_multiplier=0.0,
1372+
label_multipliers={'benchmark-improvement': 1.0},
1373+
)
1374+
result = _mirror_issue_for_scoring(issue, issue.solving_pr, repo_config, base_score=1.0)
1375+
assert result is not None
1376+
assert result.discovery_label_multiplier == pytest.approx(0.0)
1377+
1378+
def test_matching_label_overrides_zero_default_multiplier(self):
1379+
label = {'name': 'benchmark-improvement', 'actor_association': 'OWNER'}
1380+
issue = MirrorIssue.from_dict(_issue_dict(solving_pr_labels=[label]))
1381+
repo_config = RepositoryConfig(
1382+
emission_share=0.5,
1383+
trusted_label_pipeline=True,
1384+
default_label_multiplier=0.0,
1385+
label_multipliers={'benchmark-improvement': 1.0},
1386+
)
1387+
result = _mirror_issue_for_scoring(issue, issue.solving_pr, repo_config, base_score=1.0)
1388+
assert result is not None
1389+
assert result.discovery_label_multiplier == pytest.approx(1.0)
1390+
1391+
def test_downweight_label_sets_multiplier(self):
1392+
label = {'name': 'refactor', 'actor_association': 'OWNER'}
1393+
issue = MirrorIssue.from_dict(_issue_dict(solving_pr_labels=[label]))
1394+
repo_config = RepositoryConfig(
1395+
emission_share=0.5,
1396+
trusted_label_pipeline=True,
1397+
label_multipliers={'refactor': 0.25},
1398+
)
1399+
result = _mirror_issue_for_scoring(issue, issue.solving_pr, repo_config, base_score=1.0)
1400+
assert result is not None
1401+
assert result.discovery_label_multiplier == pytest.approx(0.25)
1402+
1403+
def test_untrusted_actor_label_falls_back_to_default_multiplier(self):
1404+
label = {'name': 'benchmark-improvement', 'actor_association': 'CONTRIBUTOR'}
1405+
issue = MirrorIssue.from_dict(_issue_dict(solving_pr_labels=[label]))
1406+
repo_config = RepositoryConfig(
1407+
emission_share=0.5,
1408+
trusted_label_pipeline=False,
1409+
default_label_multiplier=0.0,
1410+
label_multipliers={'benchmark-improvement': 1.0},
1411+
)
1412+
result = _mirror_issue_for_scoring(issue, issue.solving_pr, repo_config, base_score=1.0)
1413+
assert result is not None
1414+
assert result.discovery_label_multiplier == pytest.approx(0.0)
1415+
1416+
1417+
class TestLabelPolicyIssueDiscovery:
1418+
"""Integration tests: repository label policy flows through run_issue_discovery
1419+
to issue_discovery_score via solving_pr.labels."""
1420+
1421+
def _seven_issues(self, solving_pr_labels=None):
1422+
return [
1423+
_issue_dict(issue_number=50 + i, solved_by_pr=100 + i, solving_pr_labels=solving_pr_labels)
1424+
for i in range(7)
1425+
]
1426+
1427+
def _seed(self, uid=2, base_score=42.0):
1428+
seed = MinerEvaluation(uid=uid, hotkey='hk2', github_id='seed')
1429+
seed.merged_prs = [
1430+
_scored_mirror_pr('entrius/gittensor-ui', 100 + i, base_score=base_score)
1431+
for i in range(7)
1432+
]
1433+
return seed
1434+
1435+
def test_zero_default_multiplier_unlabeled_solving_prs_earn_zero_score(self):
1436+
client = Mock()
1437+
client.get_miner_issues.return_value = _response(self._seven_issues())
1438+
eval_ = _eval()
1439+
seed = self._seed()
1440+
repo_config = RepositoryConfig(
1441+
emission_share=0.5,
1442+
trusted_label_pipeline=True,
1443+
default_label_multiplier=0.0,
1444+
label_multipliers={'benchmark-improvement': 1.0},
1445+
)
1446+
_run(
1447+
run_issue_discovery(
1448+
{1: eval_, 2: seed},
1449+
{'entrius/gittensor-ui': repo_config},
1450+
_EMPTY_LANGS,
1451+
_EMPTY_TOKEN_CONFIG,
1452+
client=client,
1453+
)
1454+
)
1455+
assert eval_.is_issue_eligible is True
1456+
assert eval_.issue_discovery_score == pytest.approx(0.0)
1457+
assert all(i.discovery_label_multiplier == pytest.approx(0.0) for i in eval_.issue_discovery_issues)
1458+
1459+
def test_matching_label_earns_nonzero_score_with_zero_default_multiplier(self):
1460+
label = [{'name': 'benchmark-improvement', 'actor_association': 'OWNER'}]
1461+
client = Mock()
1462+
client.get_miner_issues.return_value = _response(self._seven_issues(solving_pr_labels=label))
1463+
eval_ = _eval()
1464+
seed = self._seed()
1465+
repo_config = RepositoryConfig(
1466+
emission_share=0.5,
1467+
trusted_label_pipeline=True,
1468+
default_label_multiplier=0.0,
1469+
label_multipliers={'benchmark-improvement': 1.0},
1470+
)
1471+
_run(
1472+
run_issue_discovery(
1473+
{1: eval_, 2: seed},
1474+
{'entrius/gittensor-ui': repo_config},
1475+
_EMPTY_LANGS,
1476+
_EMPTY_TOKEN_CONFIG,
1477+
client=client,
1478+
)
1479+
)
1480+
assert eval_.is_issue_eligible is True
1481+
assert eval_.issue_discovery_score > 0.0
1482+
assert all(i.discovery_label_multiplier == pytest.approx(1.0) for i in eval_.issue_discovery_issues)
1483+
1484+
def test_downweight_label_reduces_discovery_score(self):
1485+
"""A refactor=0.25 label on the solving PR sets discovery_label_multiplier=0.25
1486+
on each scored issue and reduces the aggregate discovery score vs unlabeled."""
1487+
label = [{'name': 'refactor', 'actor_association': 'OWNER'}]
1488+
repo_config = RepositoryConfig(
1489+
emission_share=0.5,
1490+
trusted_label_pipeline=True,
1491+
label_multipliers={'refactor': 0.25},
1492+
)
1493+
1494+
def _run_discovery(issues):
1495+
client = Mock()
1496+
client.get_miner_issues.return_value = _response(issues)
1497+
ev = _eval(uid=1, github_id='999')
1498+
seed = MinerEvaluation(uid=2, hotkey='hk2', github_id='seed')
1499+
seed.merged_prs = [_scored_mirror_pr('entrius/gittensor-ui', 100 + i) for i in range(7)]
1500+
_run(
1501+
run_issue_discovery(
1502+
{1: ev, 2: seed},
1503+
{'entrius/gittensor-ui': repo_config},
1504+
_EMPTY_LANGS,
1505+
_EMPTY_TOKEN_CONFIG,
1506+
client=client,
1507+
)
1508+
)
1509+
return ev
1510+
1511+
ev_unlabeled = _run_discovery(self._seven_issues())
1512+
ev_labeled = _run_discovery(self._seven_issues(solving_pr_labels=label))
1513+
1514+
assert ev_unlabeled.issue_discovery_score > 0.0
1515+
assert ev_labeled.issue_discovery_score < ev_unlabeled.issue_discovery_score
1516+
assert all(
1517+
i.discovery_label_multiplier == pytest.approx(0.25)
1518+
for i in ev_labeled.issue_discovery_issues
1519+
)

0 commit comments

Comments
 (0)