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
34 changes: 20 additions & 14 deletions gittensor/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from gittensor.validator.oss_contributions.mirror.scored_pr import ScoredMirrorPR

from gittensor.constants import (
LABEL_MULTIPLIERS,
MAINTAINER_ASSOCIATIONS,
MAX_CODE_DENSITY_MULTIPLIER,
MIN_TOKEN_SCORE_FOR_BASE_SCORE,
Expand Down Expand Up @@ -183,8 +182,13 @@ class PullRequest:
time_decay_multiplier: float = 1.0
credibility_multiplier: float = 1.0
review_quality_multiplier: float = 1.0 # Penalty for CHANGES_REQUESTED reviews from maintainers
label_multiplier: float = 1.0 # Multiplier based on PR label (exact match against known labels)
label: Optional[str] = None # Last label set on the PR
label_multiplier: float = 1.0 # Multiplier resolved from per-repo label_multipliers config
label: Optional[str] = None # Resolved scoring label (set during scoring, stored in DB)
current_labels: frozenset[str] = field(default_factory=frozenset) # All currently-applied labels (lowercased)
# Current labels ordered by last application (most recent first) from timeline scan.
# Subset of current_labels that appeared in timelineItems; labels absent from the timeline
# (truncated) are not included here and fall back to highest-multiplier selection.
label_timeline_order: tuple[str, ...] = field(default_factory=tuple)
changes_requested_count: int = 0 # Number of maintainer CHANGES_REQUESTED reviews
earned_score: float = 0.0
collateral_score: float = 0.0 # For OPEN PRs: potential_score * collateral_percent
Expand Down Expand Up @@ -310,18 +314,19 @@ def from_graphql_response(cls, pr_data: dict, uid: int, hotkey: str, github_id:
cr_reviews = (pr_data.get('changesRequestedReviews') or {}).get('nodes') or []
changes_requested_count = sum(1 for r in cr_reviews if r.get('authorAssociation') in MAINTAINER_ASSOCIATIONS)

current = {(n.get('name') or '').lower() for n in (pr_data.get('labels') or {}).get('nodes') or [] if n}
label: Optional[str] = None
scoring_labels = current & LABEL_MULTIPLIERS.keys()
if scoring_labels:
current: frozenset = frozenset(
(n.get('name') or '').lower() for n in (pr_data.get('labels') or {}).get('nodes') or [] if n
)
# Collect all currently-applied labels in reverse-timeline order (most recently applied first).
# Per-repo multiplier resolution happens later in calculate_pr_multipliers which has repo config.
timeline_ordered: list = []
if current:
seen: set = set()
for event in reversed((pr_data.get('timelineItems') or {}).get('nodes') or []):
name = ((event or {}).get('label') or {}).get('name', '').lower()
if name in scoring_labels:
label = name
break
if label is None:
# Timeline truncated — fall back to highest-multiplier currently-applied label
label = max(scoring_labels, key=lambda n: (LABEL_MULTIPLIERS[n], n))
if name and name in current and name not in seen:
seen.add(name)
timeline_ordered.append(name)

return cls(
number=pr_data['number'],
Expand All @@ -343,7 +348,8 @@ def from_graphql_response(cls, pr_data: dict, uid: int, hotkey: str, github_id:
last_edited_at=last_edited_at,
head_ref_oid=pr_data.get('headRefOid'),
base_ref_oid=pr_data.get('baseRefOid'),
label=label,
current_labels=current,
label_timeline_order=tuple(timeline_ordered),
changes_requested_count=changes_requested_count,
)

Expand Down
23 changes: 0 additions & 23 deletions gittensor/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,29 +83,6 @@
# Boosts
MAX_CODE_DENSITY_MULTIPLIER = 1.15

# Label multipliers - applied based on the last label set on the PR (requires triage+ access)
LABEL_MULTIPLIERS: dict[str, float] = {
# features
'feature': 1.50,
'feat': 1.50,
# bug fixes
'bug': 1.25,
'fix': 1.25,
'crash': 1.25,
'regression': 1.25,
'security': 1.25,
# enhancements
'enhancement': 1.10,
'improve': 1.10,
'perf': 1.10,
# refactors
'refactor': 0.5,
'cleanup': 0.5,
'polish': 0.5,
'debt': 0.5,
'chore': 0.5,
}

# 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
Expand Down
29 changes: 16 additions & 13 deletions gittensor/validator/oss_contributions/mirror/scoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
from gittensor.classes import FileChange, PrScoringResult, ScoringCategory
from gittensor.constants import (
CONTRIBUTION_SCORE_FOR_FULL_BONUS,
LABEL_MULTIPLIERS,
MAINTAINER_ASSOCIATIONS,
MAINTAINER_ISSUE_MULTIPLIER,
MAX_CONTRIBUTION_BONUS,
Expand All @@ -53,6 +52,7 @@
LanguageConfig,
RepositoryConfig,
TokenConfig,
resolve_label_multiplier,
resolve_repo_weight,
)
from gittensor.validator.utils.tree_sitter_scoring import calculate_token_score_from_file_changes
Expand Down Expand Up @@ -351,9 +351,9 @@ def _calculate_pr_multipliers(scored: ScoredMirrorPR, repo_config: RepositoryCon

scored.repo_weight_multiplier = resolve_repo_weight(repo_config)

chosen_label = _resolve_trusted_scoring_label(pr, repo_config)
chosen_label, label_multiplier = _resolve_trusted_scoring_label(pr, repo_config)
scored.label = chosen_label
scored.label_multiplier = LABEL_MULTIPLIERS.get(chosen_label, 1.0) if chosen_label else 1.0
scored.label_multiplier = label_multiplier

scored.issue_multiplier = round(_calculate_issue_multiplier(scored), 2)

Expand All @@ -372,9 +372,12 @@ def _calculate_pr_multipliers(scored: ScoredMirrorPR, repo_config: RepositoryCon
scored.review_quality_multiplier = 1.0


def _resolve_trusted_scoring_label(pr: MirrorPullRequest, repo_config: RepositoryConfig) -> Optional[str]:
def _resolve_trusted_scoring_label(pr: MirrorPullRequest, repo_config: RepositoryConfig) -> tuple[Optional[str], float]:
"""Pick the highest-multiplier currently-applied scoring label whose actor is trusted.

Returns ``(label_name, multiplier)``. Returns ``(None, default_label_multiplier)``
when no trusted scoring label is present.

By default the actor must be in ``MAINTAINER_ASSOCIATIONS``. Repos opted into
``trusted_label_pipeline`` accept any actor — including GitHub-App actors that
surface as ``actor_association=NULL`` because they lack a row in
Expand All @@ -383,17 +386,17 @@ def _resolve_trusted_scoring_label(pr: MirrorPullRequest, repo_config: Repositor
auto-labelers (release-drafter, actions/labeler) and must keep the gate.
"""
trusted = repo_config.trusted_label_pipeline
candidates = [
label
for label in pr.labels
if (label.name or '').lower() in LABEL_MULTIPLIERS
and (trusted or label.actor_association in MAINTAINER_ASSOCIATIONS)
]
candidates: list[tuple[str, float]] = []
for label in pr.labels:
if not (trusted or label.actor_association in MAINTAINER_ASSOCIATIONS):
continue
mult = resolve_label_multiplier((label.name or '').lower(), repo_config)
if mult is not None:
candidates.append(((label.name or '').lower(), mult))
if not candidates:
return None
return None, repo_config.default_label_multiplier
# Highest multiplier wins; tie-broken by label name for deterministic output
best = max(candidates, key=lambda label: (LABEL_MULTIPLIERS[label.name.lower()], label.name.lower()))
return best.name.lower()
return max(candidates, key=lambda t: (t[1], t[0]))


# ============================================================================
Expand Down
46 changes: 43 additions & 3 deletions gittensor/validator/oss_contributions/scoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from gittensor.validator.oss_contributions.mirror.scored_pr import ScoredMirrorPR
from gittensor.constants import (
EXCESSIVE_PR_PENALTY_BASE_THRESHOLD,
LABEL_MULTIPLIERS,
MAINTAINER_ASSOCIATIONS,
MAINTAINER_ISSUE_MULTIPLIER,
MAX_ISSUE_CLOSE_WINDOW_DAYS,
Expand All @@ -41,7 +40,13 @@
)
from gittensor.validator.oss_contributions.credibility import check_eligibility
from gittensor.validator.utils.datetime_utils import calculate_time_decay
from gittensor.validator.utils.load_weights import LanguageConfig, RepositoryConfig, TokenConfig, resolve_repo_weight
from gittensor.validator.utils.load_weights import (
LanguageConfig,
RepositoryConfig,
TokenConfig,
resolve_label_multiplier,
resolve_repo_weight,
)


def score_miner_prs(
Expand Down Expand Up @@ -202,6 +207,41 @@ def calculate_review_collateral_multiplier(changes_requested_count: int, pr_numb
return multiplier


def _resolve_label(
timeline_order: tuple[str, ...],
current_labels: frozenset[str],
repo_config: Optional[RepositoryConfig],
) -> tuple[Optional[str], float]:
"""Resolve the scoring label and its multiplier from per-repo config.

Tries labels in *timeline_order* (most recently applied first) and returns
the first that matches a per-repo pattern — preserving the last-applied-
scoring-label semantics of the original global-table approach.

Falls back to the highest-multiplier matching label from *current_labels*
for labels absent from the timeline (truncated history). Returns
``(None, default_label_multiplier)`` when nothing matches at all.
"""
default_mult = repo_config.default_label_multiplier if repo_config else 1.0

for lbl in timeline_order:
mult = resolve_label_multiplier(lbl, repo_config)
if mult is not None:
return lbl, mult

best: Optional[tuple[str, float]] = None
for lbl in current_labels:
mult = resolve_label_multiplier(lbl, repo_config)
if mult is not None:
if best is None or mult > best[1] or (mult == best[1] and lbl < best[0]):
best = (lbl, mult)

if best is not None:
return best

return None, default_mult


def calculate_pr_multipliers(
pr: PullRequest, miner_eval: MinerEvaluation, master_repositories: Dict[str, RepositoryConfig]
) -> None:
Expand All @@ -211,7 +251,7 @@ def calculate_pr_multipliers(

pr.repo_weight_multiplier = resolve_repo_weight(repo_config)
pr.issue_multiplier = round(calculate_issue_multiplier(pr), 2)
pr.label_multiplier = LABEL_MULTIPLIERS.get(pr.label, 1.0) if pr.label else 1.0
pr.label, pr.label_multiplier = _resolve_label(pr.label_timeline_order, pr.current_labels, repo_config)

if is_merged:
# Spam multiplier is recalculated in finalize_miner_scores with total token score
Expand Down
65 changes: 64 additions & 1 deletion gittensor/validator/utils/load_weights.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@
# Copyright © 2025 Entrius
import json
from dataclasses import dataclass, field
from fnmatch import fnmatch
from pathlib import Path
from typing import Dict, List, Optional

import bittensor as bt

from gittensor.constants import DEFAULT_REPO_WEIGHT, NON_CODE_EXTENSIONS

_LABEL_MULTIPLIER_MIN = 0.0
_LABEL_MULTIPLIER_MAX = 20.0
_LABEL_MULTIPLIERS_MAX_ENTRIES = 10


@dataclass
class LanguageConfig:
Expand Down Expand Up @@ -38,14 +43,22 @@ class RepositoryConfig:
actor — including GitHub Apps that surface as ``actor_association=NULL``.
Defaults to False; only enable on repos with an authoritative label
pipeline. See ``_resolve_trusted_scoring_label`` for the threat model.

label_multipliers: Per-repo label-to-multiplier mapping. Keys support
fnmatch wildcards (e.g. ``"type/*"``, ``"*-dev"``). Each value must
be in [0.0, 20.0]; at most 10 entries. When None no label scoring
applies and ``default_label_multiplier`` is used for all PRs.
default_label_multiplier: Multiplier applied when no label on the PR
matches any pattern in ``label_multipliers``. Must be in [0.0, 20.0].
Defaults to 1.0 (neutral).
"""

weight: float
inactive_at: Optional[str] = None
additional_acceptable_branches: Optional[List[str]] = None
mirror_enabled: bool = False
trusted_label_pipeline: bool = False
label_multipliers: Optional[Dict[str, float]] = None
default_label_multiplier: float = 1.0


def resolve_repo_weight(repo_config: Optional[RepositoryConfig]) -> float:
Expand All @@ -55,6 +68,22 @@ def resolve_repo_weight(repo_config: Optional[RepositoryConfig]) -> float:
return repo_config.weight


def resolve_label_multiplier(label: str, repo_config: Optional[RepositoryConfig]) -> Optional[float]:
"""Return the per-repo multiplier for *label* using fnmatch pattern matching.

Patterns and label are compared in lowercase. Returns ``None`` when no
pattern matches — callers should then fall back to
``repo_config.default_label_multiplier`` (or 1.0 if config is absent).
"""
if repo_config is None or not repo_config.label_multipliers:
return None
label_lower = label.lower()
for pattern, multiplier in repo_config.label_multipliers.items():
if fnmatch(label_lower, pattern.lower()):
return multiplier
return None


@dataclass
class TokenConfig:
"""Configuration for token-based scoring weights.
Expand Down Expand Up @@ -122,12 +151,46 @@ def load_master_repo_weights() -> Dict[str, RepositoryConfig]:
normalized_data: Dict[str, RepositoryConfig] = {}
for repo_name, metadata in data.items():
try:
raw_lm = metadata.get('label_multipliers')
label_multipliers: Optional[Dict[str, float]] = None
if raw_lm is not None:
if not isinstance(raw_lm, dict):
bt.logging.warning(f'{repo_name}: label_multipliers must be a dict, ignoring')
elif len(raw_lm) > _LABEL_MULTIPLIERS_MAX_ENTRIES:
bt.logging.warning(
f'{repo_name}: label_multipliers has {len(raw_lm)} entries '
f'(max {_LABEL_MULTIPLIERS_MAX_ENTRIES}), ignoring'
)
else:
validated: Dict[str, float] = {}
for k, v in raw_lm.items():
fv = float(v)
if not (_LABEL_MULTIPLIER_MIN <= fv <= _LABEL_MULTIPLIER_MAX):
bt.logging.warning(
f'{repo_name}: label_multipliers["{k}"]={fv} out of '
f'[{_LABEL_MULTIPLIER_MIN}, {_LABEL_MULTIPLIER_MAX}], skipping entry'
)
else:
validated[k] = fv
label_multipliers = validated or None

raw_default = metadata.get('default_label_multiplier', 1.0)
default_label_multiplier = float(raw_default)
if not (_LABEL_MULTIPLIER_MIN <= default_label_multiplier <= _LABEL_MULTIPLIER_MAX):
bt.logging.warning(
f'{repo_name}: default_label_multiplier={default_label_multiplier} out of '
f'[{_LABEL_MULTIPLIER_MIN}, {_LABEL_MULTIPLIER_MAX}], resetting to 1.0'
)
default_label_multiplier = 1.0

config = RepositoryConfig(
weight=float(metadata.get('weight', 0.01)),
inactive_at=metadata.get('inactive_at'),
additional_acceptable_branches=metadata.get('additional_acceptable_branches'),
mirror_enabled=bool(metadata.get('mirror_enabled', False)),
trusted_label_pipeline=bool(metadata.get('trusted_label_pipeline', False)),
label_multipliers=label_multipliers,
default_label_multiplier=default_label_multiplier,
)
normalized_data[repo_name.lower()] = config
except (ValueError, TypeError) as e:
Expand Down
Loading
Loading