diff --git a/gittensor/validator/oss_contributions/mirror/scoring.py b/gittensor/validator/oss_contributions/mirror/scoring.py index 487a2383b..e2e2decdf 100644 --- a/gittensor/validator/oss_contributions/mirror/scoring.py +++ b/gittensor/validator/oss_contributions/mirror/scoring.py @@ -16,8 +16,8 @@ - ``edited_after_merge`` is NOT a PR-level gate — it gates only the issue bonus multiplier in ``_is_valid_linked_issue``, matching legacy ``is_valid_issue``. -- Mirror's ``actor_association`` per label lets ``_resolve_maintainer_set_label`` - require maintainer-applied labels; legacy can't do this and accepts any-applier. +- Mirror's ``actor_association`` per label lets ``_resolve_trusted_scoring_label`` + require maintainer-applied labels unless a repo opts into its trusted label pipeline. """ import os @@ -351,7 +351,7 @@ def _calculate_pr_multipliers(scored: ScoredMirrorPR, repo_config: RepositoryCon scored.repo_weight_multiplier = resolve_repo_weight(repo_config) - chosen_label = _resolve_maintainer_set_label(pr) + chosen_label = _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 @@ -372,21 +372,22 @@ def _calculate_pr_multipliers(scored: ScoredMirrorPR, repo_config: RepositoryCon scored.review_quality_multiplier = 1.0 -def _resolve_maintainer_set_label(pr: MirrorPullRequest) -> Optional[str]: - """Pick the highest-multiplier currently-applied label that was set by a maintainer. +def _resolve_trusted_scoring_label(pr: MirrorPullRequest, repo_config: RepositoryConfig) -> Optional[str]: + """Pick the highest-multiplier currently-applied scoring label from a trusted actor. - Mirror gives us actor attribution per label, so we can directly require the - label to have been applied by an OWNER/MEMBER/COLLABORATOR. Labels with null - actor_association (backfilled events) are ignored to be conservative. + By default, mirror labels must come from OWNER/MEMBER/COLLABORATOR actors. + Repos with trusted_label_pipeline=True may use bot/App labelers whose mirror + events have null actor_association, so any scoring label on those repos counts. """ + trust_any_actor = repo_config.trusted_label_pipeline candidates = [ label for label in pr.labels - if label.actor_association in MAINTAINER_ASSOCIATIONS and (label.name or '').lower() in LABEL_MULTIPLIERS + if (label.name or '').lower() in LABEL_MULTIPLIERS + and (trust_any_actor or label.actor_association in MAINTAINER_ASSOCIATIONS) ] if not candidates: return None - # 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() diff --git a/gittensor/validator/utils/load_weights.py b/gittensor/validator/utils/load_weights.py index b78379f98..4b9a44a5e 100644 --- a/gittensor/validator/utils/load_weights.py +++ b/gittensor/validator/utils/load_weights.py @@ -34,6 +34,9 @@ class RepositoryConfig: mirror_enabled: When True, fetch this repo's data from the das-github-mirror service instead of via per-miner PATs. Defaults to False so existing entries keep their current PAT-based behavior. + trusted_label_pipeline: When True, mirror scoring trusts scoring labels + regardless of actor association. Defaults to False so community repos + keep the maintainer-association gate. """ @@ -41,6 +44,27 @@ class RepositoryConfig: inactive_at: Optional[str] = None additional_acceptable_branches: Optional[List[str]] = None mirror_enabled: bool = False + trusted_label_pipeline: bool = False + + @classmethod + def from_metadata(cls, metadata: dict) -> 'RepositoryConfig': + """Parse a master_repositories.json entry into a typed config.""" + if not isinstance(metadata, dict): + raise TypeError(f'repository metadata must be dict, got {type(metadata)}') + return cls( + weight=float(metadata.get('weight', DEFAULT_REPO_WEIGHT)), + inactive_at=metadata.get('inactive_at'), + additional_acceptable_branches=metadata.get('additional_acceptable_branches'), + mirror_enabled=_metadata_bool(metadata, 'mirror_enabled'), + trusted_label_pipeline=_metadata_bool(metadata, 'trusted_label_pipeline'), + ) + + +def _metadata_bool(metadata: dict, key: str) -> bool: + value = metadata.get(key, False) + if isinstance(value, bool): + return value + raise TypeError(f'{key} must be bool, got {type(value)}') def resolve_repo_weight(repo_config: Optional[RepositoryConfig]) -> float: @@ -117,17 +141,18 @@ def load_master_repo_weights() -> Dict[str, RepositoryConfig]: normalized_data: Dict[str, RepositoryConfig] = {} for repo_name, metadata in data.items(): try: - 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)), - ) + config = RepositoryConfig.from_metadata(metadata) normalized_data[repo_name.lower()] = config except (ValueError, TypeError) as e: bt.logging.warning(f'Could not parse config for {repo_name}: {e}, using defaults') # Create config with defaults if parsing fails - normalized_data[repo_name.lower()] = RepositoryConfig(weight=float(metadata.get('weight', 0.01))) + weight = DEFAULT_REPO_WEIGHT + if isinstance(metadata, dict): + try: + weight = float(metadata.get('weight', DEFAULT_REPO_WEIGHT)) + except (ValueError, TypeError): + pass + normalized_data[repo_name.lower()] = RepositoryConfig(weight=weight) bt.logging.debug(f'Successfully loaded {len(normalized_data)} repository entries from {weights_file}') return normalized_data diff --git a/gittensor/validator/weights/master_repositories.json b/gittensor/validator/weights/master_repositories.json index 584395592..1c8cb5479 100644 --- a/gittensor/validator/weights/master_repositories.json +++ b/gittensor/validator/weights/master_repositories.json @@ -205,19 +205,28 @@ }, "entrius/allways": { "weight": 1.0, - "mirror_enabled": true + "mirror_enabled": true, + "trusted_label_pipeline": true }, "entrius/allways-ui": { "weight": 0.5, - "mirror_enabled": true + "mirror_enabled": true, + "trusted_label_pipeline": true + }, + "entrius/das-github-mirror": { + "weight": 0.2, + "mirror_enabled": true, + "trusted_label_pipeline": true }, "entrius/gittensor": { "weight": 1.0, - "mirror_enabled": true + "mirror_enabled": true, + "trusted_label_pipeline": true }, "entrius/gittensor-ui": { "weight": 0.5, - "mirror_enabled": true + "mirror_enabled": true, + "trusted_label_pipeline": true }, "espressif/arduino-esp32": { "weight": 0.1444 diff --git a/tests/validator/oss_contributions/mirror/test_scoring.py b/tests/validator/oss_contributions/mirror/test_scoring.py index 0a379e6cb..6b5145831 100644 --- a/tests/validator/oss_contributions/mirror/test_scoring.py +++ b/tests/validator/oss_contributions/mirror/test_scoring.py @@ -34,7 +34,7 @@ _should_skip_merged_mirror_pr = scoring_module._should_skip_merged_mirror_pr _convert_mirror_files = adapters_module.mirror_files_to_legacy _calculate_pr_multipliers = scoring_module._calculate_pr_multipliers -_resolve_maintainer_set_label = scoring_module._resolve_maintainer_set_label +_resolve_trusted_scoring_label = scoring_module._resolve_trusted_scoring_label _calculate_issue_multiplier = scoring_module._calculate_issue_multiplier _is_valid_linked_issue = scoring_module._is_valid_linked_issue score_mirror_pr = scoring_module.score_mirror_pr @@ -100,11 +100,16 @@ def _pr( ) -def _config(weight: float = 0.5, additional_branches: list | None = None) -> RepositoryConfig: +def _config( + weight: float = 0.5, + additional_branches: list | None = None, + trusted_label_pipeline: bool = False, +) -> RepositoryConfig: return RepositoryConfig( weight=weight, mirror_enabled=True, additional_acceptable_branches=additional_branches, + trusted_label_pipeline=trusted_label_pipeline, ) @@ -408,7 +413,7 @@ def test_renamed_file_carries_previous_filename(self): class TestLabelResolution: def test_no_labels_returns_none(self): scored = ScoredMirrorPR(pr=_pr(labels=[])) - assert _resolve_maintainer_set_label(scored.pr) is None + assert _resolve_trusted_scoring_label(scored.pr, _config()) is None def test_non_scoring_labels_ignored(self): # 'enhancement' is in LABEL_MULTIPLIERS, 'random' typically isn't @@ -416,27 +421,33 @@ def test_non_scoring_labels_ignored(self): {'name': 'random', 'actor_github_id': '1', 'actor_association': 'OWNER'}, ] scored = ScoredMirrorPR(pr=_pr(labels=labels)) - assert _resolve_maintainer_set_label(scored.pr) is None - - def test_non_maintainer_label_ignored(self): - from gittensor.constants import LABEL_MULTIPLIERS - - scoring_label = next(iter(LABEL_MULTIPLIERS.keys())) - labels = [ - {'name': scoring_label, 'actor_github_id': '1', 'actor_association': 'CONTRIBUTOR'}, - ] + assert _resolve_trusted_scoring_label(scored.pr, _config()) is None + + @pytest.mark.parametrize( + 'actor_association,resolves', + [ + ('OWNER', True), + ('MEMBER', True), + ('COLLABORATOR', True), + ('CONTRIBUTOR', False), + ('NONE', False), + (None, False), + ], + ) + def test_untrusted_repo_requires_maintainer_actor(self, actor_association, resolves): + labels = [{'name': 'feature', 'actor_github_id': '1', 'actor_association': actor_association}] scored = ScoredMirrorPR(pr=_pr(labels=labels)) - assert _resolve_maintainer_set_label(scored.pr) is None + expected = 'feature' if resolves else None + assert _resolve_trusted_scoring_label(scored.pr, _config(trusted_label_pipeline=False)) == expected - def test_null_actor_association_ignored(self): - from gittensor.constants import LABEL_MULTIPLIERS - - scoring_label = next(iter(LABEL_MULTIPLIERS.keys())) - labels = [ - {'name': scoring_label, 'actor_github_id': None, 'actor_association': None}, - ] + @pytest.mark.parametrize( + 'actor_association', + ['OWNER', 'MEMBER', 'COLLABORATOR', 'CONTRIBUTOR', 'NONE', None], + ) + def test_trusted_repo_accepts_any_scoring_label_actor(self, actor_association): + labels = [{'name': 'feature', 'actor_github_id': '1', 'actor_association': actor_association}] scored = ScoredMirrorPR(pr=_pr(labels=labels)) - assert _resolve_maintainer_set_label(scored.pr) is None + assert _resolve_trusted_scoring_label(scored.pr, _config(trusted_label_pipeline=True)) == 'feature' def test_maintainer_set_scoring_label_returned(self): from gittensor.constants import LABEL_MULTIPLIERS @@ -446,7 +457,7 @@ def test_maintainer_set_scoring_label_returned(self): {'name': scoring_label, 'actor_github_id': '1', 'actor_association': 'COLLABORATOR'}, ] scored = ScoredMirrorPR(pr=_pr(labels=labels)) - assert _resolve_maintainer_set_label(scored.pr) == scoring_label.lower() + assert _resolve_trusted_scoring_label(scored.pr, _config()) == scoring_label.lower() def test_highest_multiplier_wins(self): from gittensor.constants import LABEL_MULTIPLIERS @@ -462,7 +473,7 @@ def test_highest_multiplier_wins(self): {'name': b, 'actor_github_id': '1', 'actor_association': 'OWNER'}, ] scored = ScoredMirrorPR(pr=_pr(labels=labels)) - chosen = _resolve_maintainer_set_label(scored.pr) + chosen = _resolve_trusted_scoring_label(scored.pr, _config()) # Whichever label has the higher LABEL_MULTIPLIERS value should win expected = max([a, b], key=lambda n: (LABEL_MULTIPLIERS[n], n)).lower() assert chosen == expected @@ -688,3 +699,31 @@ def test_open_pr_only_neutral_multipliers(self): assert scored.time_decay_multiplier == 1.0 assert scored.credibility_multiplier == 1.0 assert scored.review_quality_multiplier == 1.0 + + def test_bot_applied_refactor_label_scores_on_trusted_repo(self): + labels = [{'name': 'refactor', 'actor_github_id': '1', 'actor_association': None}] + scored = ScoredMirrorPR(pr=_pr(labels=labels)) + + _calculate_pr_multipliers(scored, _config(trusted_label_pipeline=True)) + + assert scored.label == 'refactor' + assert scored.label_multiplier == 0.5 + + def test_bot_applied_label_stays_neutral_on_untrusted_repo(self): + labels = [{'name': 'feature', 'actor_github_id': '1', 'actor_association': None}] + scored = ScoredMirrorPR(pr=_pr(labels=labels)) + + _calculate_pr_multipliers(scored, _config(trusted_label_pipeline=False)) + + assert scored.label is None + assert scored.label_multiplier == 1.0 + + @pytest.mark.parametrize('trusted_label_pipeline', [False, True]) + def test_maintainer_applied_label_scores_on_all_repos(self, trusted_label_pipeline): + labels = [{'name': 'feature', 'actor_github_id': '1', 'actor_association': 'OWNER'}] + scored = ScoredMirrorPR(pr=_pr(labels=labels)) + + _calculate_pr_multipliers(scored, _config(trusted_label_pipeline=trusted_label_pipeline)) + + assert scored.label == 'feature' + assert scored.label_multiplier == 1.5 diff --git a/tests/validator/test_load_weights.py b/tests/validator/test_load_weights.py index 459dbb81d..ac20260f4 100644 --- a/tests/validator/test_load_weights.py +++ b/tests/validator/test_load_weights.py @@ -122,9 +122,24 @@ def test_mirror_enabled_field_present_on_live_configs(self): f'{repo_name} mirror_enabled should be bool, got {type(config.mirror_enabled)}' ) + def test_trusted_label_pipeline_field_present_on_live_configs(self): + """Live master_repositories.json entries load with a bool trusted_label_pipeline.""" + repos = load_master_repo_weights() + for repo_name, config in repos.items(): + assert isinstance(config.trusted_label_pipeline, bool), ( + f'{repo_name} trusted_label_pipeline should be bool, got {type(config.trusted_label_pipeline)}' + ) + + def test_entrius_repos_enable_trusted_label_pipeline(self): + repos = load_master_repo_weights() + entrius_repos = {name: config for name, config in repos.items() if name.startswith('entrius/')} + assert entrius_repos, 'expected entrius/* entries in master_repositories.json' + for repo_name, config in entrius_repos.items(): + assert config.trusted_label_pipeline is True, f'{repo_name} must trust its label pipeline' + class TestRepositoryConfigMirrorFlag: - """Dataclass-level tests for the mirror_enabled field + its JSON parsing.""" + """Dataclass-level tests for mirror/trusted-label flags + JSON parsing.""" def test_mirror_enabled_default_false(self): """RepositoryConfig constructor defaults mirror_enabled to False.""" @@ -136,6 +151,14 @@ def test_mirror_enabled_explicit_true(self): config = RepositoryConfig(weight=0.5, mirror_enabled=True) assert config.mirror_enabled is True + def test_trusted_label_pipeline_default_false(self): + config = RepositoryConfig(weight=0.5) + assert config.trusted_label_pipeline is False + + def test_trusted_label_pipeline_explicit_true(self): + config = RepositoryConfig(weight=0.5, trusted_label_pipeline=True) + assert config.trusted_label_pipeline is True + def test_loader_parses_mirror_enabled_true(self, tmp_path, monkeypatch): """load_master_repo_weights() parses mirror_enabled:true from JSON.""" import json @@ -160,6 +183,45 @@ def test_loader_parses_mirror_enabled_true(self, tmp_path, monkeypatch): assert repos['foo/legacy-repo'].mirror_enabled is False assert repos['foo/explicit-off'].mirror_enabled is False + def test_loader_parses_trusted_label_pipeline_true(self, tmp_path, monkeypatch): + import json + + from gittensor.validator.utils import load_weights as lw + + fake_weights_dir = tmp_path + (fake_weights_dir / 'master_repositories.json').write_text( + json.dumps( + { + 'foo/trusted-repo': {'weight': 0.5, 'trusted_label_pipeline': True}, + 'foo/untrusted-repo': {'weight': 0.3}, + 'foo/explicit-off': {'weight': 0.2, 'trusted_label_pipeline': False}, + } + ) + ) + monkeypatch.setattr(lw, '_get_weights_dir', lambda: fake_weights_dir) + + repos = lw.load_master_repo_weights() + + assert repos['foo/trusted-repo'].trusted_label_pipeline is True + assert repos['foo/untrusted-repo'].trusted_label_pipeline is False + assert repos['foo/explicit-off'].trusted_label_pipeline is False + + def test_string_bool_does_not_enable_trusted_label_pipeline(self, tmp_path, monkeypatch): + """Pin _metadata_bool strictness: a JSON typo like "false" must not enable trust.""" + import json + + from gittensor.validator.utils import load_weights as lw + + fake_weights_dir = tmp_path + (fake_weights_dir / 'master_repositories.json').write_text( + json.dumps({'foo/bad-bool': {'weight': 0.5, 'trusted_label_pipeline': 'false'}}) + ) + monkeypatch.setattr(lw, '_get_weights_dir', lambda: fake_weights_dir) + + repos = lw.load_master_repo_weights() + + assert repos['foo/bad-bool'].trusted_label_pipeline is False + class TestBannedOrganizations: """Tests ensuring banned organizations are not active in the repository list.