Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gittensor/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
18 changes: 10 additions & 8 deletions gittensor/validator/issue_discovery/mirror_scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,22 +156,18 @@ 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
continue

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)
Expand Down Expand Up @@ -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]]:
Expand Down Expand Up @@ -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.
Expand Down
45 changes: 42 additions & 3 deletions tests/validator/issue_discovery/test_mirror_scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down