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
12 changes: 1 addition & 11 deletions gittensor/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from gittensor.constants import (
EXTENSIONLESS_FILE_EXTENSIONS,
MAX_CODE_DENSITY_MULTIPLIER,
MIN_TOKEN_SCORE_FOR_BASE_SCORE,
)


Expand Down Expand Up @@ -184,8 +183,6 @@ class PullRequest:
base_score: float = 0.0
issue_multiplier: float = 1.0
open_pr_spam_multiplier: float = 1.0
pioneer_dividend: float = 0.0 # Additive bonus for pioneering a repo
pioneer_rank: int = 0 # 0 = not eligible, 1 = pioneer, 2+ = follower position
time_decay_multiplier: float = 1.0
credibility_multiplier: float = 1.0
review_quality_multiplier: float = 1.0 # Penalty for CHANGES_REQUESTED reviews from maintainers
Expand Down Expand Up @@ -222,15 +219,8 @@ def set_file_changes(self, file_changes: List[FileChange]) -> None:
"""Set the file changes for this pull request"""
self.file_changes = file_changes

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.
"""
return self.merged_at is not None and self.token_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."""
"""Combine base score with all multipliers."""
multipliers = {
'issue': self.issue_multiplier,
'label': self.label_multiplier,
Expand Down
7 changes: 7 additions & 0 deletions gittensor/cli/miner_commands/post.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,13 @@ def _validate_pat_locally(pat: str) -> str | None:
)
return None

gql_data = gql_resp.json()
if (gql_data.get('data') or {}).get('viewer') is None:
err_console.print(
'[red]PAT lacks GraphQL API access. Fine-grained PATs need "Public Repositories (read-only)" permission.[/red]'
)
return None

return login
except requests.RequestException:
return None
13 changes: 5 additions & 8 deletions gittensor/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,6 @@
# Boosts
MAX_CODE_DENSITY_MULTIPLIER = 1.15

# Pioneer dividend — rewards the first quality contributor to each repository
# Rates applied per follower position (1st follower pays most, diminishing after)
# Dividend capped at PIONEER_DIVIDEND_MAX_RATIO × pioneer's own earned_score
PIONEER_DIVIDEND_RATE_1ST = 0.30 # 1st follower: 30% of their earned_score
PIONEER_DIVIDEND_RATE_2ND = 0.20 # 2nd follower: 20% of their earned_score
PIONEER_DIVIDEND_RATE_REST = 0.10 # 3rd+ followers: 10% of their earned_score
PIONEER_DIVIDEND_MAX_RATIO = 1.0 # Cap dividend at 1× pioneer's own earned_score (max 2× total)

# Issue boosts
MAX_ISSUE_CLOSE_WINDOW_DAYS = 1

Expand All @@ -100,6 +92,11 @@
TIME_DECAY_SIGMOID_STEEPNESS_SCALAR = 0.4
TIME_DECAY_MIN_MULTIPLIER = 0.05 # 5% of score will retain through lookback window

# Per-parse CPU budget for tree-sitter. The parser polls this flag in its
# error-recovery loops; without it, adversarial inputs can spin forever in C
# while holding the GIL. 2s is well above the millisecond cost of real files.
TREE_SITTER_PARSE_TIMEOUT_MICROS = 2_000_000

# comment nodes for token scoring
COMMENT_NODE_TYPES = frozenset(
{
Expand Down
2 changes: 0 additions & 2 deletions gittensor/validator/oss_contributions/mirror/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,6 @@ def mirror_scored_pr_to_legacy_pull_request(
base_score=scored.base_score,
issue_multiplier=scored.issue_multiplier,
open_pr_spam_multiplier=scored.open_pr_spam_multiplier,
pioneer_dividend=scored.pioneer_dividend,
pioneer_rank=scored.pioneer_rank,
time_decay_multiplier=scored.time_decay_multiplier,
credibility_multiplier=scored.credibility_multiplier,
review_quality_multiplier=scored.review_quality_multiplier,
Expand Down
13 changes: 0 additions & 13 deletions gittensor/validator/oss_contributions/mirror/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

Filtering applied at load time:
- Repo not in master_repositories: dropped (mirror returns all tracked repos).
- PR created after repo became inactive: dropped.
- PR author is a maintainer (OWNER/MEMBER/COLLABORATOR): silently dropped.
- CLOSED PRs created before the lookback window: dropped — closing an old PR
shouldn't trigger a fresh credibility penalty.
Expand All @@ -27,7 +26,6 @@
from gittensor.utils.mirror.models import MirrorPullRequest
from gittensor.validator.oss_contributions.mirror.scored_pr import ScoredPR
from gittensor.validator.oss_contributions.mirror.scoring import _should_skip_merged_mirror_pr
from gittensor.validator.utils.datetime_utils import parse_github_iso_to_utc
from gittensor.validator.utils.load_weights import RepositoryConfig


Expand Down Expand Up @@ -94,17 +92,6 @@ def _maybe_add_pr(
bt.logging.debug(f'Skipping PR #{pr.pr_number} in {pr.repo_full_name} - not in master_repositories')
return

# Skip PR if it was created after the repo became inactive
if repo_config.inactive_at is not None:
inactive_dt = parse_github_iso_to_utc(repo_config.inactive_at)
if pr.created_at >= inactive_dt:
bt.logging.info(
f'Skipping PR #{pr.pr_number} in {pr.repo_full_name} - '
f'PR was created after repo became inactive '
f'(created: {pr.created_at.isoformat()}, inactive: {inactive_dt.isoformat()})'
)
return

# Silent maintainer skip — logging every maintainer-merged PR would dominate
# the skip-reason log.
if not os.environ.get('DEV_MODE') and pr.author_association in MAINTAINER_ASSOCIATIONS:
Expand Down
16 changes: 3 additions & 13 deletions gittensor/validator/oss_contributions/mirror/scored_pr.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

The scoring fields and the ``number`` / ``repository_full_name`` / ``merged_at``
aliases below are shaped to match ``PullRequest`` (the storage-layer type) so
shared scoring helpers (``calculate_final_earned_score``, ``is_pioneer_eligible``,
shared scoring helpers (``calculate_final_earned_score``,
``calculate_open_pr_collateral_score``) work on either type unchanged.
"""

Expand All @@ -16,7 +16,6 @@
from typing import List, Optional

from gittensor.classes import _apply_score_multipliers
from gittensor.constants import MIN_TOKEN_SCORE_FOR_BASE_SCORE
from gittensor.utils.mirror.models import MirrorFile, MirrorPullRequest


Expand All @@ -35,10 +34,6 @@ class ScoredPR:
label_multiplier: float = 1.0
label: Optional[str] = None

# Pioneer attribution (per-repo, populated post per-PR scoring)
pioneer_dividend: float = 0.0
pioneer_rank: int = 0 # 0 = not eligible, 1 = pioneer, 2+ = follower position

# Score outputs
base_score: float = 0.0
earned_score: float = 0.0
Expand Down Expand Up @@ -76,16 +71,11 @@ def changes_requested_count(self) -> int:

@property
def merged_at(self) -> Optional[datetime]:
"""Alias for ``self.pr.merged_at`` — matches the ``PullRequest`` field
name so the pioneer-dividend walk treats both types identically."""
"""Alias for ``self.pr.merged_at`` — matches the ``PullRequest`` field name."""
return self.pr.merged_at

def is_pioneer_eligible(self) -> bool:
"""Pioneer-eligible iff merged AND meets the minimum token-score gate."""
return self.pr.merged_at is not None and self.token_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."""
"""Combine base score with all multipliers."""
multipliers = {
'issue': self.issue_multiplier,
'label': self.label_multiplier,
Expand Down
11 changes: 8 additions & 3 deletions gittensor/validator/oss_contributions/mirror/scoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

Cross-path concerns handled by ``finalize_miner_scores`` in
``gittensor.validator.oss_contributions.scoring`` (walks ``merged_prs``):
spam_multiplier, credibility_multiplier, pioneer dividends, final earned_score
spam_multiplier, credibility_multiplier, final earned_score
composition, and base/earned/nodes aggregation.

Anti-gaming notes:
Expand Down Expand Up @@ -97,7 +97,12 @@ async def score_miner_prs(
bt.logging.info(
f'\n[{i}/{len(scored_prs)}] {label} PR #{scored.pr.pr_number} in {scored.pr.repo_full_name}'
)
await score_pr(scored, eval_, master_repositories, programming_languages, token_config, client)
try:
await score_pr(scored, eval_, master_repositories, programming_languages, token_config, client)
except Exception as e:
bt.logging.warning(
f'UID {eval_.uid}: scoring failed for PR #{scored.pr.pr_number} in {scored.pr.repo_full_name}: {e}'
)


# ============================================================================
Expand Down Expand Up @@ -161,7 +166,7 @@ async def score_pr(

if repo_config.fixed_base_score is not None:
# Only the base score is overridden. Token fields stay token-derived so
# eligibility, pioneer, and reporting gates keep their evidence signal.
# eligibility and reporting gates keep their evidence signal.
scored.base_score = repo_config.fixed_base_score

_calculate_pr_multipliers(scored, repo_config)
Expand Down
2 changes: 1 addition & 1 deletion gittensor/validator/oss_contributions/reward.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ async def get_rewards(
if penalized_uids:
self.evaluation_cache.evict_many(penalized_uids)

# Finalize scores: apply eligibility gate, credibility, pioneer dividends, collateral
# Finalize scores: apply eligibility gate, credibility, collateral
finalize_miner_scores(miner_evaluations, master_repositories)

return miner_evaluations, cached_uids, penalized_uids
85 changes: 3 additions & 82 deletions gittensor/validator/oss_contributions/scoring.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# The MIT License (MIT)
# Copyright © 2025 Entrius

from datetime import datetime
from typing import TYPE_CHECKING, Dict, Optional, Tuple
from typing import TYPE_CHECKING, Dict, Optional

import bittensor as bt

Expand All @@ -16,10 +15,6 @@
MAX_OPEN_PR_THRESHOLD,
OPEN_PR_COLLATERAL_PERCENT,
OPEN_PR_THRESHOLD_TOKEN_SCORE,
PIONEER_DIVIDEND_MAX_RATIO,
PIONEER_DIVIDEND_RATE_1ST,
PIONEER_DIVIDEND_RATE_2ND,
PIONEER_DIVIDEND_RATE_REST,
REVIEW_PENALTY_RATE,
)
from gittensor.validator.oss_contributions.credibility import check_eligibility
Expand Down Expand Up @@ -80,76 +75,6 @@ def calculate_pr_spam_penalty_multiplier(total_open_prs: int, total_token_score:
return 1.0 if total_open_prs <= threshold else 0.0


def calculate_pioneer_dividends(
miner_evaluations: Dict[int, MinerEvaluation],
) -> None:
"""Determine pioneers and set pioneer_rank + pioneer_dividend on each PR.

For each repo, the pioneer is the miner with the earliest merged PR that
passes the quality gate (is_pioneer_eligible). The pioneer's earliest PR
on that repo earns a dividend based on ALL followers' earned_scores (post-
multiplier), using per-position rates (30%/20%/10%). The dividend uses the
follower's multipliers, not the pioneer's — so it reflects follower quality.

Must be called AFTER all earned_scores have been computed.
"""
pr_index: Dict[str, Dict[int, list]] = {}
repo_contributions: Dict[str, Dict[int, Tuple[datetime, int, float]]] = {}

for uid, evaluation in miner_evaluations.items():
for pr in evaluation.merged_prs:
if not pr.is_pioneer_eligible():
continue
assert pr.merged_at is not None
repo = pr.repository_full_name
pr_index.setdefault(repo, {}).setdefault(uid, []).append(pr)

current = repo_contributions.setdefault(repo, {}).get(uid)
if current is None:
repo_contributions[repo][uid] = (pr.merged_at, pr.number, pr.earned_score)
else:
earliest_at, earliest_num, total_score = current
new_total = total_score + pr.earned_score
if pr.merged_at < earliest_at or (pr.merged_at == earliest_at and pr.number < earliest_num):
repo_contributions[repo][uid] = (pr.merged_at, pr.number, new_total)
else:
repo_contributions[repo][uid] = (earliest_at, earliest_num, new_total)

for repo, uid_entries in repo_contributions.items():
sorted_uids = sorted(uid_entries.items(), key=lambda x: (x[1][0], x[1][1]))

for rank_pos, (uid, _) in enumerate(sorted_uids):
for pr in pr_index[repo][uid]:
pr.pioneer_rank = rank_pos + 1

dividend = 0.0
for pos, (_, entry) in enumerate(sorted_uids[1:]):
follower_earned = entry[2]
if pos == 0:
dividend += follower_earned * PIONEER_DIVIDEND_RATE_1ST
elif pos == 1:
dividend += follower_earned * PIONEER_DIVIDEND_RATE_2ND
else:
dividend += follower_earned * PIONEER_DIVIDEND_RATE_REST

if dividend <= 0:
continue

pioneer_uid = sorted_uids[0][0]
pioneer_pr_number = sorted_uids[0][1][1]
pioneer_pr = next(pr for pr in pr_index[repo][pioneer_uid] if pr.number == pioneer_pr_number)
max_dividend = pioneer_pr.earned_score * PIONEER_DIVIDEND_MAX_RATIO
capped = min(dividend, max_dividend)
pioneer_pr.pioneer_dividend = round(capped, 2)
pioneer_pr.earned_score = round(pioneer_pr.earned_score + pioneer_pr.pioneer_dividend, 2)

cap_note = f' (capped from {dividend:.2f})' if capped < dividend else ''
bt.logging.info(
f'Pioneer dividend | repo={repo} pioneer=uid {pioneer_uid} '
f'followers={len(sorted_uids) - 1} dividend={capped:.2f}{cap_note}'
)


def _pr_bypasses_eligibility(
pr: 'ScoredPR',
master_repositories: Dict[str, RepositoryConfig],
Expand All @@ -162,7 +87,7 @@ def finalize_miner_scores(
miner_evaluations: Dict[int, MinerEvaluation],
master_repositories: Optional[Dict[str, RepositoryConfig]] = None,
) -> None:
"""Finalize all miner scores: compute earned_scores, then apply pioneer dividends, then collateral."""
"""Finalize all miner scores: compute earned_scores, then collateral, then aggregate."""
bt.logging.info('**Finalizing miner scores**')
master_repositories = master_repositories or {}

Expand Down Expand Up @@ -242,10 +167,7 @@ def finalize_miner_scores(
evaluation.total_leaf_count += pr.leaf_count
evaluation.total_leaf_score += pr.leaf_score

# Phase 2: Calculate pioneer dividends from follower earned_scores
calculate_pioneer_dividends(miner_evaluations)

# Phase 3: Aggregate totals (including dividends), collateral, logging
# Phase 2: Aggregate totals, apply collateral, log summary
for uid, evaluation in miner_evaluations.items():
if not evaluation:
continue
Expand All @@ -254,7 +176,6 @@ def finalize_miner_scores(
if not has_contributions:
continue

# Aggregate scores (earned_score now includes pioneer_dividend from Phase 2).
for pr in evaluation.merged_prs:
evaluation.base_total_score += pr.base_score
evaluation.total_score += pr.earned_score
Expand Down
6 changes: 2 additions & 4 deletions gittensor/validator/storage/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
number, repository_full_name, uid, hotkey, github_id, title, author_login,
merged_at, pr_created_at, pr_state,
base_score, issue_multiplier,
open_pr_spam_multiplier, pioneer_dividend, pioneer_rank, time_decay_multiplier,
open_pr_spam_multiplier, time_decay_multiplier,
credibility_multiplier, review_quality_multiplier, label_multiplier, label,
earned_score, collateral_score,
additions, deletions, commits, total_nodes_scored,
Expand All @@ -56,7 +56,7 @@
%s, %s, %s, %s, %s, %s, %s,
%s, %s, %s,
%s, %s,
%s, %s, %s, %s,
%s, %s,
%s, %s, %s, %s,
%s, %s,
%s, %s, %s, %s,
Expand All @@ -74,8 +74,6 @@
base_score = EXCLUDED.base_score,
issue_multiplier = EXCLUDED.issue_multiplier,
open_pr_spam_multiplier = EXCLUDED.open_pr_spam_multiplier,
pioneer_dividend = EXCLUDED.pioneer_dividend,
pioneer_rank = EXCLUDED.pioneer_rank,
time_decay_multiplier = EXCLUDED.time_decay_multiplier,
credibility_multiplier = EXCLUDED.credibility_multiplier,
review_quality_multiplier = EXCLUDED.review_quality_multiplier,
Expand Down
2 changes: 0 additions & 2 deletions gittensor/validator/storage/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,6 @@ def store_pull_requests_bulk(self, pull_requests: List[PullRequest], commit: boo
pr.base_score,
pr.issue_multiplier,
pr.open_pr_spam_multiplier,
pr.pioneer_dividend,
pr.pioneer_rank,
pr.time_decay_multiplier,
pr.credibility_multiplier,
pr.review_quality_multiplier,
Expand Down
3 changes: 0 additions & 3 deletions gittensor/validator/utils/load_weights.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ class RepositoryConfig:
Attributes:
emission_share: Fraction of the combined scoring pool allocated to this repo
issue_discovery_share: Fraction of the repo allocation reserved for issue discovery
inactive_at: ISO timestamp when repository became inactive (None if active)
additional_acceptable_branches: List of additional branch patterns to accept (None if only default branch)
trusted_label_pipeline: When True, scoring labels count regardless of
actor — including GitHub Apps that surface as ``actor_association=NULL``.
Expand All @@ -49,7 +48,6 @@ class RepositoryConfig:

emission_share: float
issue_discovery_share: float = DEFAULT_ISSUE_DISCOVERY_SHARE
inactive_at: Optional[str] = None
additional_acceptable_branches: Optional[List[str]] = None
trusted_label_pipeline: bool = False
label_multipliers: Optional[Dict[str, float]] = None
Expand Down Expand Up @@ -162,7 +160,6 @@ def load_master_repo_weights() -> Dict[str, RepositoryConfig]:
'issue_discovery_share',
metadata.get('issue_discovery_share', DEFAULT_ISSUE_DISCOVERY_SHARE),
),
inactive_at=metadata.get('inactive_at'),
additional_acceptable_branches=metadata.get('additional_acceptable_branches'),
trusted_label_pipeline=bool(metadata.get('trusted_label_pipeline', False)),
label_multipliers=(
Expand Down
Loading
Loading