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
8 changes: 5 additions & 3 deletions gittensor/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ class PullRequest:
# Token scoring breakdown (after test weight applied)
code_density: float = 0.0
token_score: float = 0.0
source_token_score: Optional[float] = None
structural_count: int = 0
structural_score: float = 0.0
leaf_count: int = 0
Expand All @@ -225,9 +226,10 @@ def set_file_changes(self, file_changes: List[FileChange]) -> None:
def is_pioneer_eligible(self) -> bool:
"""Check if this PR qualifies for pioneer consideration.

A PR is eligible if it is merged and meets the minimum token score quality gate.
A PR is eligible if it is merged and meets the minimum SOURCE token score quality gate.
"""
return self.merged_at is not None and self.token_score >= MIN_TOKEN_SCORE_FOR_BASE_SCORE
gate_score = self.source_token_score if self.source_token_score is not None else self.token_score
return self.merged_at is not None and gate_score >= MIN_TOKEN_SCORE_FOR_BASE_SCORE

def calculate_final_earned_score(self) -> float:
"""Combine base score with all multipliers. Pioneer dividend is added separately after."""
Expand Down Expand Up @@ -399,7 +401,7 @@ class MinerEvaluation:
issue_credibility: float = 0.0
is_issue_eligible: bool = False
total_solved_issues: int = 0
total_valid_solved_issues: int = 0 # solved issues where solving PR has token_score >= 5
total_valid_solved_issues: int = 0 # solved issues where solving PR has SOURCE token_score >= 5
total_closed_issues: int = 0
total_open_issues: int = 0 # mirror-tracked open issues in lookback window (set by mirror_scan)

Expand Down
6 changes: 4 additions & 2 deletions gittensor/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,17 @@
# =============================================================================
# Eligibility Gate (OSS Contributions)
# =============================================================================
MIN_VALID_MERGED_PRS = 5 # minimum "valid" merged PRs (token_score >= MIN_TOKEN_SCORE_FOR_BASE_SCORE) to receive score
# Minimum "valid" merged PRs with SOURCE token_score >= MIN_TOKEN_SCORE_FOR_BASE_SCORE
MIN_VALID_MERGED_PRS = 5
MIN_CREDIBILITY = 0.80 # minimum credibility ratio to receive score
CREDIBILITY_MULLIGAN_COUNT = 1 # number of closed PRs forgiven (erased from merged+closed counts entirely)

# =============================================================================
# Issue Discovery
# =============================================================================
# Eligibility gate (stricter than OSS contributions)
MIN_VALID_SOLVED_ISSUES = 7 # minimum solved issues where solving PR has token_score >= MIN_TOKEN_SCORE_FOR_BASE_SCORE
# Minimum solved issues where solving PR has SOURCE token_score >= MIN_TOKEN_SCORE_FOR_BASE_SCORE
MIN_VALID_SOLVED_ISSUES = 7
MIN_ISSUE_CREDIBILITY = 0.80 # minimum issue credibility ratio

# Review quality cliff model (different from OSS: has clean bonus + steeper penalty)
Expand Down
25 changes: 18 additions & 7 deletions gittensor/validator/issue_discovery/mirror_scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@
calculate_open_issue_spam_multiplier,
check_issue_eligibility,
)
from gittensor.validator.oss_contributions.credibility import (
meets_token_quality_gate,
source_quality_token_score,
)
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,
Expand Down Expand Up @@ -80,6 +84,7 @@ class CachedSolvingPR:

base_score: float
token_score: float
source_token_score: Optional[float] = None


@dataclass
Expand Down Expand Up @@ -241,14 +246,15 @@ def _build_solving_pr_cache(
cache: Dict[Tuple[str, int], CachedSolvingPR] = {}
for evaluation in miner_evaluations.values():
for scored in evaluation.mirror_merged_prs:
if scored.token_score < MIN_TOKEN_SCORE_FOR_BASE_SCORE:
if not meets_token_quality_gate(scored):
continue
key = (scored.pr.repo_full_name, scored.pr.pr_number)
if key in cache:
continue # first miner wins — values are the same PR's fields
cache[key] = CachedSolvingPR(
base_score=scored.base_score,
token_score=scored.token_score,
source_token_score=scored.source_token_score,
)
return cache

Expand Down Expand Up @@ -315,16 +321,16 @@ def _score_miner_mirror_issues(
)
if cached is None:
# Fetch failed — issue still counts for solved/credibility but not scored.
# Can't apply the valid-solved gate without a real token_score, so be
# Can't apply the valid-solved gate without real score data, so be
# conservative and don't increment valid_solved_count.
bt.logging.debug(
f' issue #{issue.issue_number} ({issue.repo_full_name}): solver score unavailable '
f'(fetch failed) — credibility only'
)
continue

# Valid-solved gate (legacy parity): solving PR must meet the token threshold.
if cached.token_score >= MIN_TOKEN_SCORE_FOR_BASE_SCORE:
# Valid-solved gate (legacy parity): solving PR must meet the SOURCE token threshold.
if meets_token_quality_gate(cached):
valid_solved_count += 1

# Same-account: discoverer == solver gets credibility only, no score
Expand All @@ -350,10 +356,11 @@ def _score_miner_mirror_issues(

# Quality gate — matches legacy issue-discovery behavior: below-threshold
# solving PRs add credibility only, no discovery score.
if cached.token_score < MIN_TOKEN_SCORE_FOR_BASE_SCORE:
if not meets_token_quality_gate(cached):
gate_score = source_quality_token_score(cached)
bt.logging.debug(
f' issue #{issue.issue_number} ({issue.repo_full_name}): solving PR '
f'#{solving_pr.pr_number} token_score {cached.token_score:.2f} < '
f'#{solving_pr.pr_number} source_token_score {gate_score:.2f} < '
f'{MIN_TOKEN_SCORE_FOR_BASE_SCORE} — credibility only'
)
continue
Expand Down Expand Up @@ -450,7 +457,11 @@ def _resolve_solving_pr_score(
issue.repo_full_name, solving_pr.pr_number, files_response.files
)
result = calculate_base_score_for_pr_files(file_changes, file_contents, programming_languages, token_config)
cached = CachedSolvingPR(base_score=result.base_score, token_score=result.token_score)
cached = CachedSolvingPR(
base_score=result.base_score,
token_score=result.token_score,
source_token_score=result.source_token_score,
)
cache[key] = cached
return cached

Expand Down
2 changes: 1 addition & 1 deletion gittensor/validator/issue_discovery/scoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def check_issue_eligibility(solved_count: int, valid_solved_count: int, closed_c
"""Check if a miner passes the issue discovery eligibility gate.

Credibility uses total solved / total attempts (with mulligan).
The gate uses ``valid_solved_count`` (solving PR meets token threshold)
The gate uses ``valid_solved_count`` (solving PR meets SOURCE token threshold)
so low-quality solves don't carry the miner past the minimum.

Returns (is_eligible, issue_credibility, reason).
Expand Down
17 changes: 14 additions & 3 deletions gittensor/validator/oss_contributions/credibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@
PrLike = Union['PullRequest', 'ScoredMirrorPR']


def source_quality_token_score(pr: object) -> float:
"""SOURCE-only score for quality gates, falling back when absent or unset."""
source_token_score = getattr(pr, 'source_token_score', None)
token_score = getattr(pr, 'token_score')
return float(token_score if source_token_score is None else source_token_score)


def meets_token_quality_gate(pr: object) -> bool:
return source_quality_token_score(pr) >= MIN_TOKEN_SCORE_FOR_BASE_SCORE


def calculate_credibility(merged_prs: Sequence[PrLike], closed_prs: Sequence[PrLike]) -> float:
"""Calculate flat credibility ratio with mulligan applied.

Expand All @@ -42,7 +53,7 @@ def check_eligibility(merged_prs: Sequence[PrLike], closed_prs: Sequence[PrLike]
"""Check if a miner passes the eligibility gate.

Gate requires:
1. At least MIN_VALID_MERGED_PRS merged PRs with token_score >= MIN_TOKEN_SCORE_FOR_BASE_SCORE
1. At least MIN_VALID_MERGED_PRS merged PRs with SOURCE token_score >= MIN_TOKEN_SCORE_FOR_BASE_SCORE
(after mulligan — if a closed PR was "valid", it no longer counts toward the minimum)
2. At least MIN_CREDIBILITY credibility (after mulligan)

Expand All @@ -52,8 +63,8 @@ def check_eligibility(merged_prs: Sequence[PrLike], closed_prs: Sequence[PrLike]
"""
credibility = calculate_credibility(merged_prs, closed_prs)

# Count valid merged PRs (token_score >= threshold)
valid_merged_count = sum(1 for pr in merged_prs if pr.token_score >= MIN_TOKEN_SCORE_FOR_BASE_SCORE)
# Count valid merged PRs (SOURCE token_score >= threshold)
valid_merged_count = sum(1 for pr in merged_prs if meets_token_quality_gate(pr))

if valid_merged_count < MIN_VALID_MERGED_PRS:
reason = f'{valid_merged_count}/{MIN_VALID_MERGED_PRS} valid merged PRs (need {MIN_VALID_MERGED_PRS})'
Expand Down
1 change: 1 addition & 0 deletions gittensor/validator/oss_contributions/mirror/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ def mirror_scored_pr_to_legacy_pull_request(
total_nodes_scored=scored.total_nodes_scored,
code_density=scored.code_density,
token_score=scored.token_score,
source_token_score=scored.source_token_score,
structural_count=scored.structural_count,
structural_score=scored.structural_score,
leaf_count=scored.leaf_count,
Expand Down
6 changes: 4 additions & 2 deletions gittensor/validator/oss_contributions/mirror/scored_pr.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class ScoredMirrorPR:
# Token scoring breakdown (populated when files are tokenized)
code_density: float = 0.0
token_score: float = 0.0
source_token_score: Optional[float] = None
structural_count: int = 0
structural_score: float = 0.0
leaf_count: int = 0
Expand Down Expand Up @@ -82,12 +83,13 @@ def merged_at(self) -> Optional[datetime]:
return self.pr.merged_at

def is_pioneer_eligible(self) -> bool:
"""Pioneer-eligible iff merged AND meets the minimum token-score gate.
"""Pioneer-eligible iff merged AND meets the minimum SOURCE token-score gate.

Mirrors `PullRequest.is_pioneer_eligible` so the legacy pioneer math
functions can be reused unchanged.
"""
return self.pr.merged_at is not None and self.token_score >= MIN_TOKEN_SCORE_FOR_BASE_SCORE
gate_score = self.source_token_score if self.source_token_score is not None else self.token_score
return self.pr.merged_at is not None and gate_score >= MIN_TOKEN_SCORE_FOR_BASE_SCORE

def calculate_final_earned_score(self) -> float:
"""Combine base score with all multipliers. Pioneer dividend is added separately after."""
Expand Down
3 changes: 3 additions & 0 deletions gittensor/validator/oss_contributions/mirror/scoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ def score_mirror_pr(

result = calculate_base_score_for_pr_files(file_changes, file_contents, programming_languages, token_config)
scored.token_score = result.token_score
scored.source_token_score = result.source_token_score
scored.structural_count = result.structural_count
scored.structural_score = result.structural_score
scored.leaf_count = result.leaf_count
Expand Down Expand Up @@ -244,6 +245,7 @@ class BaseScoreResult:

base_score: float
token_score: float
source_token_score: float
structural_count: int
structural_score: float
leaf_count: int
Expand Down Expand Up @@ -315,6 +317,7 @@ def calculate_base_score_for_pr_files(
return BaseScoreResult(
base_score=base_score,
token_score=token_score,
source_token_score=source_token_score,
structural_count=structural_count,
structural_score=structural_score,
leaf_count=leaf_count,
Expand Down
7 changes: 4 additions & 3 deletions gittensor/validator/oss_contributions/scoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ def calculate_base_score(
pr.file_changes or [], file_contents, programming_languages, token_config
)
pr.token_score = result.token_score
pr.source_token_score = result.source_token_score
pr.structural_count = result.structural_count
pr.structural_score = result.structural_score
pr.leaf_count = result.leaf_count
Expand Down Expand Up @@ -350,9 +351,9 @@ def finalize_miner_scores(miner_evaluations: Dict[int, MinerEvaluation]) -> None
bt.logging.info('No merged or closed PRs - skipping evaluation')
continue

# Check eligibility gate across both paths. check_eligibility only touches
# token_score on each PR; ScoredMirrorPR has the same field, so combining
# the lists works without adapting types.
# Check eligibility gate across both paths. check_eligibility uses the
# SOURCE-quality token score when present, falling back to token_score
# for older/test objects, so combining the lists works without adapting types.
is_eligible, credibility, reason = check_eligibility(
evaluation.merged_pull_requests + evaluation.mirror_merged_prs,
evaluation.closed_pull_requests + evaluation.mirror_closed_prs,
Expand Down
61 changes: 60 additions & 1 deletion tests/validator/issue_discovery/test_mirror_scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,16 @@ def test_below_threshold_prs_excluded_from_cache(self):
assert ('foo/poisoned', 1) not in cache
assert ('foo/healthy', 2) in cache

def test_source_quality_below_threshold_prs_excluded_from_cache(self):
e1 = MinerEvaluation(uid=1, hotkey='hk1', github_id='g1')
test_only = _scored_mirror_pr('foo/test-only', 1, token_score=50.0, base_score=0.5)
test_only.source_token_score = 0.0
e1.mirror_merged_prs = [test_only]

cache = _build_solving_pr_cache({1: e1})

assert ('foo/test-only', 1) not in cache

def test_cache_hit_reuses_base_score_no_fetch(self):
"""A solving PR already in cache must not trigger a get_pr_files call."""
client = Mock()
Expand All @@ -458,7 +468,7 @@ def test_cache_hit_reuses_base_score_no_fetch(self):
client.get_pr_files.assert_not_called()
# The cached token_score (100) flowed into issue_token_score
assert eval_.issue_token_score == 100.0
# And the issue counted toward valid_solved (token_score >= MIN threshold)
# And the issue counted toward valid_solved (SOURCE-quality score >= MIN threshold)
assert eval_.total_valid_solved_issues == 1

def test_cache_miss_fetches_and_writes_back(self):
Expand Down Expand Up @@ -551,6 +561,55 @@ def test_token_score_below_threshold_counts_credibility_only(self):
assert eval_.total_valid_solved_issues == 0 # below gate
assert eval_.issue_discovery_score == 0

def test_fetched_test_only_solving_pr_counts_credibility_only(self):
"""A large TEST-only solving PR can have aggregate token_score above the
threshold, but SOURCE quality is still below the valid-solved gate."""
test_code = '\n'.join(
f'def test_alpha_{i}():\n value = {i}\n adjusted = value + 1\n assert adjusted == {i + 1}\n'
for i in range(80)
)
lines = len(test_code.splitlines())
client = Mock()
client.get_miner_issues.return_value = _response([_issue_dict()])
client.get_pr_files.return_value = MirrorPullRequestFilesResponse.from_dict(
{
'repo_full_name': 'entrius/gittensor-ui',
'pr_number': 100,
'head_sha': 'h',
'base_sha': 'b',
'merge_base_sha': 'mb',
'scoring_data_stored': True,
'files': [
{
'filename': 'tests/test_alpha.py',
'previous_filename': None,
'status': 'added',
'additions': lines,
'deletions': 0,
'changes': lines,
'is_binary': False,
'head_content': test_code,
'base_content': None,
}
],
}
)
eval_ = _eval()

_run(
run_mirror_issue_discovery(
{1: eval_},
_mirror_repos('entrius/gittensor-ui'),
load_weights.load_programming_language_weights(),
load_weights.load_token_config(),
client=client,
)
)

assert eval_.total_solved_issues == 1
assert eval_.total_valid_solved_issues == 0
assert eval_.issue_discovery_score == 0


class TestCacheStats:
"""Verify the _CacheStats counter accurately tracks hits / misses /
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def test_empty_file_changes_returns_zero_result(self):
assert isinstance(result, BaseScoreResult)
assert result.base_score == 0.0
assert result.token_score == 0.0
assert result.source_token_score == 0.0
assert result.total_nodes_scored == 0
assert result.structural_count == 0
assert result.leaf_count == 0
Expand All @@ -44,6 +45,7 @@ def test_result_has_all_expected_fields(self):
for field_name in [
'base_score',
'token_score',
'source_token_score',
'structural_count',
'structural_score',
'leaf_count',
Expand Down
8 changes: 7 additions & 1 deletion tests/validator/oss_contributions/mirror/test_scored_pr.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Covers:
- Composition: raw response data accessed via .pr.<field>; scoring fields default neutrally
- is_pioneer_eligible respects merged + token_score gate
- is_pioneer_eligible respects merged + SOURCE token_score gate
- calculate_final_earned_score multiplies base by every multiplier
"""

Expand Down Expand Up @@ -96,6 +96,12 @@ def test_merged_at_threshold_eligible(self):
scored.token_score = 5.0 # equals MIN_TOKEN_SCORE_FOR_BASE_SCORE
assert scored.is_pioneer_eligible() is True

def test_source_quality_below_threshold_not_eligible(self):
scored = ScoredMirrorPR(pr=_make_pr())
scored.token_score = 5.0
scored.source_token_score = 0.0
assert scored.is_pioneer_eligible() is False

def test_merged_above_threshold_eligible(self):
scored = ScoredMirrorPR(pr=_make_pr())
scored.token_score = 50.0
Expand Down
5 changes: 5 additions & 0 deletions tests/validator/test_pioneer_dividend.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ def test_ineligible_when_below_token_score(self, builder):
pr = builder.create(state=PRState.MERGED, uid=1, token_score=MIN_TOKEN_SCORE_FOR_BASE_SCORE - 1)
assert not pr.is_pioneer_eligible()

def test_ineligible_when_source_quality_score_below_threshold(self, builder):
pr = builder.create(state=PRState.MERGED, uid=1, token_score=MIN_TOKEN_SCORE_FOR_BASE_SCORE)
pr.source_token_score = 0.0
assert not pr.is_pioneer_eligible()

def test_ineligible_when_open(self, builder):
pr = builder.create(state=PRState.OPEN, uid=1)
assert not pr.is_pioneer_eligible()
Expand Down
Loading
Loading