diff --git a/gittensor/classes.py b/gittensor/classes.py index fa78c0833..84739744e 100644 --- a/gittensor/classes.py +++ b/gittensor/classes.py @@ -401,7 +401,7 @@ class MinerEvaluation: total_solved_issues: int = 0 total_valid_solved_issues: int = 0 # solved issues where solving PR has token_score >= 5 total_closed_issues: int = 0 - total_open_issues: int = 0 # mirror-tracked open issues in lookback window (set by mirror_scan) + total_open_issues: int = 0 # current mirror-tracked open issues (set by mirror_scan) @property def total_prs(self) -> int: diff --git a/gittensor/validator/issue_discovery/mirror_scan.py b/gittensor/validator/issue_discovery/mirror_scan.py index f2916ecce..c26cdab1b 100644 --- a/gittensor/validator/issue_discovery/mirror_scan.py +++ b/gittensor/validator/issue_discovery/mirror_scan.py @@ -156,6 +156,7 @@ async def run_mirror_issue_discovery( try: response = client.get_miner_issues(evaluation.github_id, since=lookback_date) + open_issue_count = _fetch_current_open_issue_count(client, evaluation.github_id, enabled_names) except MirrorRequestError as e: bt.logging.warning(f'├─ UID {uid}: mirror issue fetch failed ({e}) — skipped this miner') fetch_errors += 1 @@ -163,15 +164,10 @@ async def run_mirror_issue_discovery( filtered = [i for i in response.issues if i.repo_full_name in enabled_names] if not filtered: + evaluation.total_open_issues = open_issue_count no_issues += 1 continue - # Count this miner's currently-open issues across mirror-enabled repos - # (within the lookback window). Used as the spam-multiplier signal and - # also written to evaluation.total_open_issues so the DB row reflects - # mirror-scoped state (the legacy GraphQL global open-issue count was - # never the right signal for the gate). - open_issue_count = sum(1 for i in filtered if i.state == 'OPEN') pending.append((evaluation, filtered, open_issue_count)) canonical_pr_owners = _build_canonical_pr_owners(pending) @@ -201,6 +197,12 @@ async def run_mirror_issue_discovery( ) +def _fetch_current_open_issue_count(client: MirrorClient, github_id: str, enabled_names: Set[str]) -> int: + """Return current open issues in mirror-enabled repos, independent of scoring lookback.""" + response = client.get_miner_issues(github_id) + return sum(1 for issue in response.issues if issue.repo_full_name in enabled_names and issue.state == 'OPEN') + + def _build_canonical_pr_owners( pending: List[Tuple[MinerEvaluation, List[MirrorIssue], int]], ) -> Dict[Tuple[str, int], Tuple[datetime, int, int]]: @@ -268,8 +270,8 @@ def _score_miner_mirror_issues( """Classify + score one miner's mirror issues, populate MinerEvaluation fields. ``open_issue_count`` is the miner's currently-OPEN issue count across - mirror-enabled repos within the lookback window — the source-of-truth - for the open-issue spam multiplier on the mirror path. + mirror-enabled repos, independent of the scoring lookback window — the + source-of-truth for the open-issue spam multiplier on the mirror path. ``canonical_pr_owners`` enforces the cross-miner one-issue-per-PR rule: only the marker-matching issue scores, siblings count for credibility. diff --git a/tests/validator/issue_discovery/test_mirror_scan.py b/tests/validator/issue_discovery/test_mirror_scan.py index 384793acf..8d4b7a58c 100644 --- a/tests/validator/issue_discovery/test_mirror_scan.py +++ b/tests/validator/issue_discovery/test_mirror_scan.py @@ -630,9 +630,48 @@ def test_resolve_increments_fetch_failures_on_request_error(self): class TestOpenIssueSpamSourceIsMirror: - """The open-issue spam multiplier sources its count from mirror's response, - and mirror_scan also writes that count to evaluation.total_open_issues so - the DB row reflects mirror-scoped state.""" + """The open-issue spam multiplier sources its count from mirror's current + open issues, and mirror_scan also writes that count to + evaluation.total_open_issues so the DB row reflects mirror-scoped state.""" + + def test_open_issue_count_uses_current_issues_not_scoring_lookback(self): + """Older still-open issues absent from the lookback scoring response + still trip the spam multiplier when present in the current response.""" + solved_issues = [_issue_dict(issue_number=300 + i, author_github_id=f'discoverer{i}') for i in range(8)] + older_open_issues = [ + _issue_dict( + issue_number=200 + i, + state='OPEN', + state_reason=None, + solved_by_pr=None, + created_at='2026-01-01T00:00:00Z', + ) + for i in range(6) + ] + client = Mock() + + def _per_miner(github_id, since=None): + if since is None: + return _response(older_open_issues) + return _response(solved_issues) + + client.get_miner_issues.side_effect = _per_miner + + eval_ = _eval() + eval_.mirror_merged_prs = [_scored_mirror_pr('entrius/gittensor-ui', 100, token_score=100.0)] + + _run( + run_mirror_issue_discovery( + {1: eval_}, + _mirror_repos('entrius/gittensor-ui'), + _EMPTY_LANGS, + _EMPTY_TOKEN_CONFIG, + client=client, + ) + ) + + assert eval_.total_open_issues == 6 + assert eval_.issue_discovery_score == 0 def test_all_mirror_miner_with_many_open_issues_trips_spam(self): """6 open issues in mirror response trips the spam multiplier."""