From 97380fa24c5cbde5045bbd5ed8f407906495b7f8 Mon Sep 17 00:00:00 2001 From: anderdc Date: Sun, 21 Jun 2026 15:19:44 -0500 Subject: [PATCH 01/14] feat(validator): load repo hyperparameters from the das API Flip load_master_repo_weights() to fetch the repository registry from the das-gittensor API (GET api.gittensor.io/repos), the new source of truth for repository hyperparameters (maintainer/admin edits land there). - Extract parse+validate into _parse_registry(); add _fetch_registry_from_api() (requests + retry/backoff) and _load_registry_from_file(). - API-first with a bundled master_repositories.json fallback: a transient API outage or a contract-violating push falls back to the seed instead of bricking scoring. A broken bundled seed still raises. - Tests default to the seed via an autouse fixture (offline/deterministic); added explicit API-path + fallback tests. master_repositories.json is retained only as the fallback seed. --- gittensor/constants.py | 10 ++ gittensor/validator/utils/load_weights.py | 174 +++++++++++++++------- tests/validator/conftest.py | 17 +++ tests/validator/test_load_weights.py | 59 ++++++++ 4 files changed, 208 insertions(+), 52 deletions(-) diff --git a/gittensor/constants.py b/gittensor/constants.py index e513dbec3..eff6ddbe6 100644 --- a/gittensor/constants.py +++ b/gittensor/constants.py @@ -34,6 +34,16 @@ MIRROR_HTTP_TIMEOUT_SECONDS = 30 MIRROR_MAX_ATTEMPTS = 3 +# ============================================================================= +# das-gittensor API (https://api.gittensor.io) — repository hyperparameter registry +# ============================================================================= +# GET /repos returns the full master-repository registry (full_name -> raw config), +# the authoritative source for repository hyperparameters. The bundled +# master_repositories.json is only a fallback seed when this endpoint is unreachable. +GITTENSOR_API_DEFAULT_URL = 'https://api.gittensor.io' +REPOS_API_TIMEOUT_SECONDS = 15 +REPOS_API_MAX_ATTEMPTS = 3 + # ============================================================================= # Language & File Scoring # ============================================================================= diff --git a/gittensor/validator/utils/load_weights.py b/gittensor/validator/utils/load_weights.py index 7b7719615..f5ebfe2bc 100644 --- a/gittensor/validator/utils/load_weights.py +++ b/gittensor/validator/utils/load_weights.py @@ -1,16 +1,19 @@ # The MIT License (MIT) # Copyright © 2025 Entrius import json +import time from dataclasses import dataclass, field from pathlib import Path from typing import Any, Dict, List, Optional import bittensor as bt +import requests from gittensor.constants import ( DEFAULT_ISSUE_DISCOVERY_SHARE, EMISSION_SHARE_TOLERANCE, EXCESSIVE_PR_PENALTY_BASE_THRESHOLD, + GITTENSOR_API_DEFAULT_URL, MAINTAINER_ISSUE_MULTIPLIER, MAX_OPEN_ISSUE_THRESHOLD, MAX_OPEN_PR_THRESHOLD, @@ -25,6 +28,8 @@ OPEN_PR_COLLATERAL_PERCENT, OPEN_PR_THRESHOLD_TOKEN_SCORE, PR_LOOKBACK_DAYS, + REPOS_API_MAX_ATTEMPTS, + REPOS_API_TIMEOUT_SECONDS, REVIEW_PENALTY_RATE, SRC_TOK_SATURATION_SCALE, STANDARD_ISSUE_MULTIPLIER, @@ -512,72 +517,137 @@ def _validate_scoring_configs(configs: Dict[str, RepositoryConfig]) -> None: ) -def load_master_repo_weights() -> Dict[str, RepositoryConfig]: +def _parse_registry(data: Any) -> Dict[str, RepositoryConfig]: + """Parse + validate a raw registry map (full_name -> metadata) into + RepositoryConfig objects, keyed by lowercased full_name. + + Raises RepositoryRegistryError / ValueError on malformed entries or + emission-share / eligibility / scoring contract violations. """ - Load repository emission shares from the local JSON file. - Normalizes repository names to lowercase for case-insensitive matching. + if not isinstance(data, dict): + raise RepositoryRegistryError(f'Expected dict registry, got {type(data)}') + + normalized_data: Dict[str, RepositoryConfig] = {} + for repo_name, metadata in data.items(): + try: + if not isinstance(metadata, dict): + raise TypeError(f'expected object metadata, got {type(metadata)}') + config = RepositoryConfig( + emission_share=_coerce_share(repo_name, 'emission_share', metadata['emission_share']), + issue_discovery_share=_coerce_share( + repo_name, + 'issue_discovery_share', + metadata.get('issue_discovery_share', DEFAULT_ISSUE_DISCOVERY_SHARE), + ), + additional_acceptable_branches=metadata.get('additional_acceptable_branches'), + trusted_label_pipeline=bool(metadata.get('trusted_label_pipeline', False)), + label_multipliers=( + {str(label): float(multiplier) for label, multiplier in metadata['label_multipliers'].items()} + if metadata.get('label_multipliers') is not None + else None + ), + default_label_multiplier=float(metadata.get('default_label_multiplier', 1.0)), + fixed_base_score=metadata.get('fixed_base_score'), + eligibility=_parse_eligibility(repo_name, metadata.get('eligibility')), + scoring=_parse_scoring(repo_name, metadata.get('scoring')), + maintainer_cut=_coerce_share(repo_name, 'maintainer_cut', metadata.get('maintainer_cut', 0.0)), + ) + normalized_data[repo_name.lower()] = config + except RepositoryRegistryError: + raise + except (KeyError, ValueError, TypeError) as e: + raise ValueError(f'Could not parse config for {repo_name}: {e}') from e - Returns: - Dictionary mapping normalized (lowercase) fullName (str) to RepositoryConfig object. - Returns empty dict when the file is missing or invalid JSON. Raises - RepositoryRegistryError or ValueError when registry entries violate the - emission-share contract. + _validate_emission_shares(normalized_data) + _validate_eligibility_configs(normalized_data) + _validate_scoring_configs(normalized_data) + return normalized_data + + +def _fetch_registry_from_api() -> Any: + """GET the repository registry from the das-gittensor API, with retries. + + Returns the decoded JSON (full_name -> raw config map). Raises + RepositoryRegistryError if the endpoint is unreachable or never returns + valid JSON within the retry budget. """ - weights_file = _get_weights_dir() / 'master_repositories.json' + from gittensor.utils.utils import backoff_seconds + + url = f'{GITTENSOR_API_DEFAULT_URL.rstrip("/")}/repos' + last_error: Optional[str] = None + + for attempt in range(REPOS_API_MAX_ATTEMPTS): + try: + response = requests.get(url, timeout=REPOS_API_TIMEOUT_SECONDS) + except requests.RequestException as e: + last_error = f'request exception: {e}' + else: + if 200 <= response.status_code < 300: + try: + return response.json() + except ValueError as e: + last_error = f'invalid JSON: {e}' + else: + last_error = f'status {response.status_code}' + + if attempt < REPOS_API_MAX_ATTEMPTS - 1: + backoff = backoff_seconds(attempt) + bt.logging.warning( + f'repos API GET {url} failed ({last_error}) ' + f'(attempt {attempt + 1}/{REPOS_API_MAX_ATTEMPTS}), retrying in {backoff}s...' + ) + time.sleep(backoff) - try: - with open(weights_file, 'r') as f: - data = json.load(f) + raise RepositoryRegistryError(f'repos API GET {url} failed after {REPOS_API_MAX_ATTEMPTS} attempts: {last_error}') - if not isinstance(data, dict): - raise RepositoryRegistryError(f'Expected dict from {weights_file}, got {type(data)}') - # Parse JSON data into RepositoryConfig objects - normalized_data: Dict[str, RepositoryConfig] = {} - for repo_name, metadata in data.items(): - try: - if not isinstance(metadata, dict): - raise TypeError(f'expected object metadata, got {type(metadata)}') - config = RepositoryConfig( - emission_share=_coerce_share(repo_name, 'emission_share', metadata['emission_share']), - issue_discovery_share=_coerce_share( - repo_name, - 'issue_discovery_share', - metadata.get('issue_discovery_share', DEFAULT_ISSUE_DISCOVERY_SHARE), - ), - additional_acceptable_branches=metadata.get('additional_acceptable_branches'), - trusted_label_pipeline=bool(metadata.get('trusted_label_pipeline', False)), - label_multipliers=( - {str(label): float(multiplier) for label, multiplier in metadata['label_multipliers'].items()} - if metadata.get('label_multipliers') is not None - else None - ), - default_label_multiplier=float(metadata.get('default_label_multiplier', 1.0)), - fixed_base_score=metadata.get('fixed_base_score'), - eligibility=_parse_eligibility(repo_name, metadata.get('eligibility')), - scoring=_parse_scoring(repo_name, metadata.get('scoring')), - maintainer_cut=_coerce_share(repo_name, 'maintainer_cut', metadata.get('maintainer_cut', 0.0)), - ) - normalized_data[repo_name.lower()] = config - except RepositoryRegistryError: - raise - except (KeyError, ValueError, TypeError) as e: - raise ValueError(f'Could not parse config for {repo_name}: {e}') from e +def _load_registry_from_file() -> Any: + """Read the bundled master_repositories.json fallback seed.""" + weights_file = _get_weights_dir() / 'master_repositories.json' + with open(weights_file, 'r') as f: + return json.load(f) - _validate_emission_shares(normalized_data) - _validate_eligibility_configs(normalized_data) - _validate_scoring_configs(normalized_data) - bt.logging.debug(f'Successfully loaded {len(normalized_data)} repository entries from {weights_file}') - return normalized_data +def load_master_repo_weights() -> Dict[str, RepositoryConfig]: + """ + Load the repository hyperparameter registry, normalizing repo names to + lowercase for case-insensitive matching. + + Source of truth is the das-gittensor API (GET /repos). If the endpoint is + unreachable or serves data that fails the registry contract, falls back to + the bundled master_repositories.json seed so a transient API outage or a + bad push can't brick scoring. + + Returns: + Dictionary mapping normalized (lowercase) fullName -> RepositoryConfig. + Returns empty dict when both the API and the bundled seed are + unavailable. Raises RepositoryRegistryError / ValueError when the + bundled seed itself violates the registry contract. + """ + try: + data = _fetch_registry_from_api() + normalized = _parse_registry(data) + bt.logging.debug(f'Loaded {len(normalized)} repository entries from the repos API') + return normalized + except Exception as api_error: + bt.logging.warning( + f'Repository registry API unavailable or invalid ({api_error}); ' + f'falling back to bundled master_repositories.json' + ) + try: + data = _load_registry_from_file() except FileNotFoundError: - bt.logging.error(f'Weights file not found: {weights_file}') + bt.logging.error('Repos API unavailable and no bundled master_repositories.json fallback found') return {} except json.JSONDecodeError as e: - bt.logging.error(f'Failed to parse JSON from {weights_file}: {e}') + bt.logging.error(f'Bundled master_repositories.json is invalid JSON: {e}') return {} + normalized = _parse_registry(data) + bt.logging.debug(f'Loaded {len(normalized)} repository entries from the bundled seed (fallback)') + return normalized + def load_programming_language_weights() -> Dict[str, LanguageConfig]: """ diff --git a/tests/validator/conftest.py b/tests/validator/conftest.py index 654446d67..4d7669a79 100644 --- a/tests/validator/conftest.py +++ b/tests/validator/conftest.py @@ -9,9 +9,26 @@ from datetime import datetime, timezone from typing import Optional +import pytest + from gittensor.classes import PRState, PullRequest +@pytest.fixture(autouse=True) +def _force_registry_file_fallback(monkeypatch): + """Default validator tests to the bundled master_repositories.json by making + the repos API fetch fail, so tests stay offline and deterministic. Tests that + exercise the API path re-patch ``_fetch_registry_from_api`` themselves; their + setattr runs after this fixture and wins. + """ + from gittensor.validator.utils import load_weights as lw + + def _api_disabled(): + raise lw.RepositoryRegistryError('repos API disabled in tests') + + monkeypatch.setattr(lw, '_fetch_registry_from_api', _api_disabled) + + @dataclass class PRBuilder: """Builder for creating mock PullRequests with sensible defaults. diff --git a/tests/validator/test_load_weights.py b/tests/validator/test_load_weights.py index 32733fd4c..918dbac8b 100644 --- a/tests/validator/test_load_weights.py +++ b/tests/validator/test_load_weights.py @@ -658,5 +658,64 @@ def test_live_master_repo_emission_shares_are_valid(self): assert 0.0 <= config.issue_discovery_share <= 1.0, f'{repo_name} issue_discovery_share out of range' +class TestRegistryApiLoading: + """Tests for the API-first loader with bundled-seed fallback.""" + + def test_loads_from_api_when_available(self, monkeypatch): + from gittensor.validator.utils import load_weights as lw + + payload = {'Owner/Repo': {'emission_share': 0.1, 'label_multipliers': {'feature': 2.0}}} + monkeypatch.setattr(lw, '_fetch_registry_from_api', lambda: payload) + + repos = lw.load_master_repo_weights() + + assert 'owner/repo' in repos # normalized to lowercase + assert repos['owner/repo'].emission_share == 0.1 + assert repos['owner/repo'].label_multipliers == {'feature': 2.0} + + def test_falls_back_to_file_when_api_unavailable(self, tmp_path, monkeypatch): + # autouse fixture already makes the API fetch fail; supply a seed file. + from gittensor.validator.utils import load_weights as lw + + (tmp_path / 'master_repositories.json').write_text(json.dumps({'a/b': {'emission_share': 0.2}})) + monkeypatch.setattr(lw, '_get_weights_dir', lambda: tmp_path) + + repos = lw.load_master_repo_weights() + + assert repos['a/b'].emission_share == 0.2 + + def test_api_invalid_content_falls_back_to_seed(self, tmp_path, monkeypatch): + from gittensor.validator.utils import load_weights as lw + + # API serves data violating the emission contract -> fall back to seed. + monkeypatch.setattr(lw, '_fetch_registry_from_api', lambda: {'a/b': {'emission_share': 5.0}}) + (tmp_path / 'master_repositories.json').write_text(json.dumps({'a/b': {'emission_share': 0.3}})) + monkeypatch.setattr(lw, '_get_weights_dir', lambda: tmp_path) + + repos = lw.load_master_repo_weights() + + assert repos['a/b'].emission_share == 0.3 + + def test_returns_empty_when_api_down_and_no_seed(self, tmp_path, monkeypatch): + # autouse fixture disables the API; point the seed lookup at an empty dir. + from gittensor.validator.utils import load_weights as lw + + monkeypatch.setattr(lw, '_get_weights_dir', lambda: tmp_path) + + assert lw.load_master_repo_weights() == {} + + def test_invalid_seed_still_raises(self, tmp_path, monkeypatch): + # A broken bundled seed is a real bug and must surface, not be swallowed. + from gittensor.validator.utils import load_weights as lw + + (tmp_path / 'master_repositories.json').write_text( + json.dumps({'foo/a': {'emission_share': 0.6}, 'foo/b': {'emission_share': 0.5}}) + ) + monkeypatch.setattr(lw, '_get_weights_dir', lambda: tmp_path) + + with pytest.raises(RepositoryRegistryError, match='total emission_share must be <= 1.0'): + lw.load_master_repo_weights() + + if __name__ == '__main__': pytest.main([__file__, '-v']) From d117162f72573d12892c67e09a3dc89aa4b734f4 Mon Sep 17 00:00:00 2001 From: Ander <61125407+anderdc@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:40:24 -0500 Subject: [PATCH 02/14] chore(weights): rebalance shares, add sparkinfer, delist inactive repos (#1523) Co-authored-by: anderdc --- .../weights/master_repositories.json | 80 ++++++------------- 1 file changed, 26 insertions(+), 54 deletions(-) diff --git a/gittensor/validator/weights/master_repositories.json b/gittensor/validator/weights/master_repositories.json index 14a19544d..d0e0bfe4a 100644 --- a/gittensor/validator/weights/master_repositories.json +++ b/gittensor/validator/weights/master_repositories.json @@ -7,7 +7,7 @@ "min_valid_merged_prs": 0, "min_valid_solved_issues": 0 }, - "emission_share": 0.005, + "emission_share": 0.004, "fixed_base_score": 1.0, "issue_discovery_share": 0.0, "label_multipliers": { @@ -20,16 +20,16 @@ "trusted_label_pipeline": true }, "cogniax/tao-pulse-app": { - "emission_share": 0.008, + "emission_share": 0.0, "issue_discovery_share": 0.5, "maintainer_cut": 0.4 }, "DPBG/Engram.AI": { - "emission_share": 0.005, + "emission_share": 0.0, "issue_discovery_share": 0.0 }, "e35ventura/taopedia": { - "emission_share": 0.025, + "emission_share": 0.05, "issue_discovery_share": 0.0, "maintainer_cut": 0.0, "additional_acceptable_branches": ["test"], @@ -161,9 +161,22 @@ }, "maintainer_cut": 0.3 }, - "infiniflow/ragflow": { - "emission_share": 0.055, - "issue_discovery_share": 0.0 + "gittensor-ai-lab/sparkinfer": { + "default_label_multiplier": 0.0, + "eligibility": { + "min_credibility": 0.0, + "min_issue_credibility": 0.0, + "min_valid_merged_prs": 0, + "min_valid_solved_issues": 0 + }, + "emission_share": 0.1, + "fixed_base_score": 1.0, + "issue_discovery_share": 0.0, + "label_multipliers": { + "benchmark-improvement": 1.0 + }, + "maintainer_cut": 0.0, + "trusted_label_pipeline": true }, "JSONbored/awesome-claude": { "default_label_multiplier": 0.0, @@ -196,7 +209,7 @@ "JSONbored/gittensory": { "default_label_multiplier": 0.0, "trusted_label_pipeline": true, - "emission_share": 0.013, + "emission_share": 0.1, "issue_discovery_share": 0.0, "label_multipliers": { "gittensor:bug": 0.5, @@ -224,7 +237,7 @@ "JSONbored/metagraphed": { "default_label_multiplier": 0.0, "trusted_label_pipeline": true, - "emission_share": 0.0075, + "emission_share": 0.25, "issue_discovery_share": 0.0, "label_multipliers": { "gittensor:bug": 0.5, @@ -250,7 +263,7 @@ } }, "MkDev11/gittensor-hub": { - "emission_share": 0.008, + "emission_share": 0.005, "issue_discovery_share": 0.0, "label_multipliers": { "feature": 2, @@ -272,7 +285,7 @@ "maintainer_cut": 0.3 }, "vouchdev/vouch": { - "emission_share": 0.01, + "emission_share": 0.0, "issue_discovery_share": 0.0, "label_multipliers": { "feature": 1.5, @@ -297,7 +310,7 @@ "maintainer_cut": 0.5 }, "phase-rs/phase": { - "emission_share": 0.018, + "emission_share": 0.02, "issue_discovery_share": 0.0, "label_multipliers": { "bug": 1.2, @@ -317,12 +330,8 @@ "excessive_pr_penalty_base_threshold": 10 } }, - "seroperson/jvm-live-reload": { - "emission_share": 0.005, - "issue_discovery_share": 0.0 - }, "touchpilot/touchpilot": { - "emission_share": 0.01, + "emission_share": 0.0, "issue_discovery_share": 0.0, "label_multipliers": { "type: bug": 1.1, @@ -332,42 +341,5 @@ "type: test": 1.2 }, "maintainer_cut": 0.3 - }, - "we-promise/sure": { - "emission_share": 0.03, - "issue_discovery_share": 0.0, - "label_multipliers": { - "performance": 1.5, - "mobile/Flutter": 1.2 - }, - "maintainer_cut": 0.3 - }, - "e35dev/live-bittensor-emissions-leaderboard": { - "emission_share": 0.005, - "issue_discovery_share": 0, - "additional_acceptable_branches": [ - "test" - ], - "trusted_label_pipeline": true, - "default_label_multiplier": 0, - "label_multipliers": { - "feature": 3, - "ui-ux": 2, - "bug": 0.625, - "security": 0.625, - "other": 0.1 - }, - "eligibility": { - "min_credibility": 0.6 - }, - "scoring": { - "pr_lookback_days": 7, - "time_decay": { - "grace_period_hours": 6, - "sigmoid_midpoint_days": 2, - "sigmoid_steepness": 1, - "min_multiplier": 0.02 - } - } } } From 9a1cf42062b480362fcb6808a98ab61792e38f0e Mon Sep 17 00:00:00 2001 From: Ander <61125407+anderdc@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:52:22 -0500 Subject: [PATCH 03/14] Register e35dev/podcast-design-canvas (#1527) Co-authored-by: anderdc --- .../weights/master_repositories.json | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/gittensor/validator/weights/master_repositories.json b/gittensor/validator/weights/master_repositories.json index d0e0bfe4a..eb5a7d304 100644 --- a/gittensor/validator/weights/master_repositories.json +++ b/gittensor/validator/weights/master_repositories.json @@ -341,5 +341,37 @@ "type: test": 1.2 }, "maintainer_cut": 0.3 + }, + "e35dev/podcast-design-canvas": { + "emission_share": 0.301, + "issue_discovery_share": 0, + "maintainer_cut": 0, + "trusted_label_pipeline": true, + "default_label_multiplier": 0, + "label_multipliers": { + "episode-ingest": 3, + "preset-styles": 2.5, + "canvas-editor": 2.5, + "audio-captions": 2, + "contextual-visuals": 2, + "template-system": 1.75, + "export-publish": 1.75, + "product-polish": 1.5, + "bugfix": 1, + "infrastructure": 0.5 + }, + "eligibility": { + "min_credibility": 0.6, + "max_open_pr_threshold": 4 + }, + "scoring": { + "pr_lookback_days": 7, + "time_decay": { + "grace_period_hours": 6, + "sigmoid_midpoint_days": 2, + "sigmoid_steepness": 1, + "min_multiplier": 0.02 + } + } } } From 9a698a5bb1f5ca84e2c6865ef0c302687a5ca940 Mon Sep 17 00:00:00 2001 From: NVIDIAN Date: Wed, 24 Jun 2026 12:33:58 -0500 Subject: [PATCH 04/14] =?UTF-8?q?chore(weights):=20genie-claw=20label=20co?= =?UTF-8?q?nfig=20=E2=80=94=20multipliers=20+=20trusted=5Flabel=5Fpipeline?= =?UTF-8?q?,=20drop=20community-contribution=20(#1512)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gittensor/validator/weights/master_repositories.json | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/gittensor/validator/weights/master_repositories.json b/gittensor/validator/weights/master_repositories.json index eb5a7d304..0abc31d28 100644 --- a/gittensor/validator/weights/master_repositories.json +++ b/gittensor/validator/weights/master_repositories.json @@ -149,17 +149,19 @@ "trusted_label_pipeline": true }, "Geniepod/genie-claw": { - "default_label_multiplier": 0.2, + "default_label_multiplier": 1.0, "eligibility": { "max_open_pr_threshold": 2 }, "emission_share": 0.025, "issue_discovery_share": 0.0, "label_multipliers": { - "community-contribution": 0.2, - "optimization": 0.8 + "enhancement": 1.25, + "optimization": 2.5, + "performance": 1.5 }, - "maintainer_cut": 0.3 + "maintainer_cut": 0.3, + "trusted_label_pipeline": true }, "gittensor-ai-lab/sparkinfer": { "default_label_multiplier": 0.0, From 8bcb5ff7fc8b67d1afd21a7d609e33ed0c1139ba Mon Sep 17 00:00:00 2001 From: NVIDIAN Date: Wed, 24 Jun 2026 12:41:38 -0500 Subject: [PATCH 05/14] sparkinfer: map label_multipliers to the repo's eval:* labels; set maintainer_cut 0.30 (#1533) Co-authored-by: Ander <61125407+anderdc@users.noreply.github.com> --- gittensor/validator/weights/master_repositories.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/gittensor/validator/weights/master_repositories.json b/gittensor/validator/weights/master_repositories.json index 0abc31d28..daed0f2d8 100644 --- a/gittensor/validator/weights/master_repositories.json +++ b/gittensor/validator/weights/master_repositories.json @@ -175,9 +175,16 @@ "fixed_base_score": 1.0, "issue_discovery_share": 0.0, "label_multipliers": { - "benchmark-improvement": 1.0 + "eval:XL": 4.0, + "eval:L": 2.5, + "eval:M": 1.5, + "eval:S": 1.0, + "eval:XS": 0.5, + "eval:BASELINE": 1.0, + "eval:none": 0.0, + "eval:REJECT": 0.0 }, - "maintainer_cut": 0.0, + "maintainer_cut": 0.3, "trusted_label_pipeline": true }, "JSONbored/awesome-claude": { From fea15db5c7c83fc68d6f0139085bb3c0e37fc17a Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Wed, 24 Jun 2026 10:43:23 -0700 Subject: [PATCH 06/14] Tune JSONbored repo weights: maintainer cut, credibility, slop label (#1530) Co-authored-by: Ander <61125407+anderdc@users.noreply.github.com> --- .../validator/weights/master_repositories.json | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/gittensor/validator/weights/master_repositories.json b/gittensor/validator/weights/master_repositories.json index daed0f2d8..daad7358d 100644 --- a/gittensor/validator/weights/master_repositories.json +++ b/gittensor/validator/weights/master_repositories.json @@ -195,7 +195,8 @@ "label_multipliers": { "gittensor:bug": 0.5, "gittensor:feature": 1.25, - "gittensor:priority": 1.75 + "gittensor:priority": 1.75, + "slop": 0.0 }, "eligibility": { "min_credibility": 0.5 @@ -223,7 +224,8 @@ "label_multipliers": { "gittensor:bug": 0.5, "gittensor:feature": 1.25, - "gittensor:priority": 1.75 + "gittensor:priority": 1.75, + "slop": 0.0 }, "eligibility": { "min_credibility": 0.6 @@ -251,12 +253,13 @@ "label_multipliers": { "gittensor:bug": 0.5, "gittensor:feature": 1.5, - "gittensor:priority": 2.5 + "gittensor:priority": 2.5, + "slop": 0.0 }, "eligibility": { - "min_credibility": 0.5 + "min_credibility": 0.6 }, - "maintainer_cut": 0.25, + "maintainer_cut": 0.5, "scoring": { "pr_lookback_days": 7, "open_pr_collateral_percent": 0.2, From f8e45bc93654b578ebf4e0184a31987cca458766 Mon Sep 17 00:00:00 2001 From: ebios-star Date: Wed, 24 Jun 2026 14:15:25 -0400 Subject: [PATCH 07/14] refactor(validator): reuse uid_index map for treasury/recycle lookups in blend_emission_pools (#1534) Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Ander <61125407+anderdc@users.noreply.github.com> --- gittensor/validator/emission_allocation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gittensor/validator/emission_allocation.py b/gittensor/validator/emission_allocation.py index aadff2ebe..6bf532bf9 100644 --- a/gittensor/validator/emission_allocation.py +++ b/gittensor/validator/emission_allocation.py @@ -58,7 +58,7 @@ def blend_emission_pools( # Issue treasury (10% flat to UID 111) if ISSUES_TREASURY_UID > 0 and ISSUES_TREASURY_UID in miner_uids: - treasury_idx = sorted_uids.index(ISSUES_TREASURY_UID) + treasury_idx = uid_index[ISSUES_TREASURY_UID] rewards[treasury_idx] += ISSUES_TREASURY_EMISSION_SHARE bt.logging.info( f'Treasury allocation: UID {ISSUES_TREASURY_UID} receives ' @@ -67,7 +67,7 @@ def blend_emission_pools( # Recycle receives registry slack and empty repo slices. if RECYCLE_UID in miner_uids: - recycle_idx = sorted_uids.index(RECYCLE_UID) + recycle_idx = uid_index[RECYCLE_UID] rewards[recycle_idx] += recycle_share if recycle_share > EMISSION_SHARE_TOLERANCE: bt.logging.info(f'Recycling {recycle_share * 100:.0f}% unclaimed emissions from repo allocation') From ca225074561f37ae103bfc9b9ebd282047997041 Mon Sep 17 00:00:00 2001 From: ventura-oss Date: Thu, 25 Jun 2026 09:13:43 -0500 Subject: [PATCH 08/14] Replace e35dev/podcast-design-canvas with podcast-design-canvas-2 (#1542) Co-authored-by: e35ventura --- gittensor/validator/weights/master_repositories.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gittensor/validator/weights/master_repositories.json b/gittensor/validator/weights/master_repositories.json index daad7358d..ff1d74b77 100644 --- a/gittensor/validator/weights/master_repositories.json +++ b/gittensor/validator/weights/master_repositories.json @@ -354,7 +354,7 @@ }, "maintainer_cut": 0.3 }, - "e35dev/podcast-design-canvas": { + "e35dev/podcast-design-canvas-2": { "emission_share": 0.301, "issue_discovery_share": 0, "maintainer_cut": 0, From 213104132b813277cc4617bef4bb2a8419b52b2b Mon Sep 17 00:00:00 2001 From: Jake Armstrong <65635253+jakearmstrong59@users.noreply.github.com> Date: Thu, 25 Jun 2026 04:28:14 -1000 Subject: [PATCH 09/14] fix(validator): SCALE-encode str args in _encode_args (#1374) (#1376) Co-authored-by: Ander <61125407+anderdc@users.noreply.github.com> --- .../issue_competitions/contract_client.py | 21 +++ .../test_contract_client_transactions.py | 126 ++++++++++++++++++ 2 files changed, 147 insertions(+) diff --git a/gittensor/validator/issue_competitions/contract_client.py b/gittensor/validator/issue_competitions/contract_client.py index 72a755b73..a67fa04aa 100644 --- a/gittensor/validator/issue_competitions/contract_client.py +++ b/gittensor/validator/issue_competitions/contract_client.py @@ -49,6 +49,22 @@ def load_contract_metadata() -> Tuple[Dict[str, bytes], Dict[str, List]]: CONTRACT_SELECTORS, CONTRACT_ARG_TYPES = load_contract_metadata() +def _scale_compact_length(n: int) -> bytes: + """SCALE-encode a non-negative integer as a compact length prefix. + + Used to prefix variable-length SCALE payloads (Vec, String). + """ + if n < 0: + raise ValueError(f'Length must be non-negative: {n}') + if n < 1 << 6: + return bytes([n << 2]) + if n < 1 << 14: + return ((n << 2) | 1).to_bytes(2, 'little') + if n < 1 << 30: + return ((n << 2) | 2).to_bytes(4, 'little') + raise ValueError(f'Length too large for compact encoding: {n}') + + class IssueStatus(Enum): """Status of an issue in its lifecycle""" @@ -568,6 +584,11 @@ def _encode_args(self, method_name: str, args: dict) -> bytes: encoded += struct.pack('> 64) + elif type_def == 'str': + if not isinstance(value, str): + raise ValueError(f'Expected str for {arg_name}, got {type(value).__name__}') + data = value.encode('utf-8') + encoded += _scale_compact_length(len(data)) + data elif type_def == 'AccountId': if isinstance(value, str): encoded += bytes.fromhex(self.subtensor.substrate.ss58_decode(value)) diff --git a/tests/validator/test_contract_client_transactions.py b/tests/validator/test_contract_client_transactions.py index 1e23da894..b71f680d8 100644 --- a/tests/validator/test_contract_client_transactions.py +++ b/tests/validator/test_contract_client_transactions.py @@ -3,6 +3,7 @@ """Tests for IssueCompetitionContractClient transaction methods.""" import hashlib +import struct from types import SimpleNamespace from unittest.mock import MagicMock, patch @@ -11,6 +12,7 @@ from gittensor.validator.issue_competitions.contract_client import ( DEFAULT_GAS_LIMIT, IssueCompetitionContractClient, + _scale_compact_length, ) # (method, call_kwargs, expected_contract_method, expected_args, uses_hotkey, explicit_gas) @@ -140,3 +142,127 @@ def test_get_treasury_stake_returns_zero_for_empty_alpha_result(client): return_value=_packed_treasury_storage(), ): assert client.get_treasury_stake() == 0 + + +class TestScaleCompactLength: + """Boundary coverage for the SCALE compact-length encoder.""" + + @pytest.mark.parametrize( + 'n, expected', + [ + (0, b'\x00'), + (1, b'\x04'), + (63, bytes([63 << 2])), + ], + ) + def test_mode_0_single_byte(self, n, expected): + assert _scale_compact_length(n) == expected + + @pytest.mark.parametrize('n', [64, 100, 16383]) + def test_mode_1_two_bytes(self, n): + encoded = _scale_compact_length(n) + assert len(encoded) == 2 + assert encoded == ((n << 2) | 1).to_bytes(2, 'little') + + @pytest.mark.parametrize('n', [16384, 100_000, (1 << 30) - 1]) + def test_mode_2_four_bytes(self, n): + encoded = _scale_compact_length(n) + assert len(encoded) == 4 + assert encoded == ((n << 2) | 2).to_bytes(4, 'little') + + def test_rejects_negative(self): + with pytest.raises(ValueError, match='non-negative'): + _scale_compact_length(-1) + + def test_rejects_oversize(self): + with pytest.raises(ValueError, match='too large'): + _scale_compact_length(1 << 30) + + +class TestEncodeArgsStr: + """SCALE encoding of `str` arguments via _encode_args (regression for #1374).""" + + def test_register_issue_short_url_encodes(self, client): + url = 'https://github.com/owner/repo/issues/1' + repo = 'owner/repo' + url_bytes = url.encode('utf-8') + repo_bytes = repo.encode('utf-8') + assert len(url_bytes) < 64 + + encoded = client._encode_args( + 'register_issue', + { + 'github_url': url, + 'repository_full_name': repo, + 'issue_number': 1, + 'target_bounty': 10_000_000_000, + }, + ) + + offset = 0 + assert encoded[offset] == len(url_bytes) << 2 + offset += 1 + assert encoded[offset : offset + len(url_bytes)] == url_bytes + offset += len(url_bytes) + + assert encoded[offset] == len(repo_bytes) << 2 + offset += 1 + assert encoded[offset : offset + len(repo_bytes)] == repo_bytes + offset += len(repo_bytes) + + assert struct.unpack_from(' Date: Thu, 25 Jun 2026 09:43:25 -0500 Subject: [PATCH 10/14] weights: rebalance allways/gittensor into podcast-design-canvas-2 (#1545) --- .../weights/master_repositories.json | 27 ++++++------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/gittensor/validator/weights/master_repositories.json b/gittensor/validator/weights/master_repositories.json index ff1d74b77..4dc2be4a9 100644 --- a/gittensor/validator/weights/master_repositories.json +++ b/gittensor/validator/weights/master_repositories.json @@ -86,7 +86,7 @@ } }, "entrius/allways": { - "emission_share": 0.04, + "emission_share": 0.0, "issue_discovery_share": 1.0, "label_multipliers": { "bug": 1.25, @@ -108,25 +108,14 @@ "trusted_label_pipeline": true }, "entrius/gittensor": { - "emission_share": 0.07, - "issue_discovery_share": 0.0, - "label_multipliers": { - "bug": 1.1, - "enhancement": 1.25, - "feature": 1.5, - "refactor": 0.25 - }, - "maintainer_cut": 0.0, - "trusted_label_pipeline": true - }, - "entrius/gittensor-ui": { - "emission_share": 0.0, + "emission_share": 0.06, "issue_discovery_share": 0.0, "label_multipliers": { - "bug": 1.1, - "enhancement": 1.0, - "feature": 1.25, - "refactor": 0.5 + "bug": 1.2, + "enhancement": 1.3, + "feature": 1.0, + "cli": 0.2, + "refactor": 0.1 }, "maintainer_cut": 0.0, "trusted_label_pipeline": true @@ -355,7 +344,7 @@ "maintainer_cut": 0.3 }, "e35dev/podcast-design-canvas-2": { - "emission_share": 0.301, + "emission_share": 0.351, "issue_discovery_share": 0, "maintainer_cut": 0, "trusted_label_pipeline": true, From 868dc488efcf4822f6f1eaf3c518fd94905e314f Mon Sep 17 00:00:00 2001 From: ventura-oss Date: Thu, 25 Jun 2026 10:38:46 -0500 Subject: [PATCH 11/14] Tune Podcast Design Canvas eligibility (#1546) Co-authored-by: e35ventura --- gittensor/validator/weights/master_repositories.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gittensor/validator/weights/master_repositories.json b/gittensor/validator/weights/master_repositories.json index 4dc2be4a9..a5d8f32c2 100644 --- a/gittensor/validator/weights/master_repositories.json +++ b/gittensor/validator/weights/master_repositories.json @@ -362,8 +362,9 @@ "infrastructure": 0.5 }, "eligibility": { + "min_valid_merged_prs": 1, "min_credibility": 0.6, - "max_open_pr_threshold": 4 + "max_open_pr_threshold": 1 }, "scoring": { "pr_lookback_days": 7, From 461a93c8904d9642e1b83960a6c45b31c68d1b1b Mon Sep 17 00:00:00 2001 From: ebios-star Date: Thu, 25 Jun 2026 13:55:41 -0400 Subject: [PATCH 12/14] refactor: dedupe RepoEvaluation get-or-create with a MinerEvaluation helper (#1541) Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Ander <61125407+anderdc@users.noreply.github.com> --- gittensor/classes.py | 25 ++++++++++++++------- gittensor/validator/issue_discovery/scan.py | 17 ++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/gittensor/classes.py b/gittensor/classes.py index 4856d4744..7916b8cfc 100644 --- a/gittensor/classes.py +++ b/gittensor/classes.py @@ -291,6 +291,21 @@ class MinerEvaluation: # The top-level scalars above are round-level rollups of this map. repo_evaluations: Dict[str, RepoEvaluation] = field(default_factory=dict) + def get_or_create_repo_evaluation( + self, repo_name: str, repository_full_name: Optional[str] = None + ) -> RepoEvaluation: + """Return the repo evaluation stored under ``repo_name``, creating one if absent. + + ``repo_name`` is the map key (a lowercased repository_full_name). When a + new entry is created, ``repository_full_name`` seeds it, defaulting to + ``repo_name`` when not supplied. + """ + repo_eval = self.repo_evaluations.get(repo_name) + if repo_eval is None: + repo_eval = RepoEvaluation(repository_full_name=repository_full_name or repo_name) + self.repo_evaluations[repo_name] = repo_eval + return repo_eval + @property def total_prs(self) -> int: return self.total_merged_prs + self.total_closed_prs + self.total_open_prs @@ -578,10 +593,7 @@ def store(self, evaluation: 'MinerEvaluation') -> None: value = getattr(existing.evaluation, name) setattr(cached_eval, name, _copy_issue_discovery_value(name, value)) for repo_name, prior_repo in existing.evaluation.repo_evaluations.items(): - target = cached_eval.repo_evaluations.get(repo_name) - if target is None: - target = RepoEvaluation(repository_full_name=prior_repo.repository_full_name) - cached_eval.repo_evaluations[repo_name] = target + target = cached_eval.get_or_create_repo_evaluation(repo_name, prior_repo.repository_full_name) target.copy_issue_discovery_from(prior_repo) self._cache[evaluation.uid] = CachedEvaluation( @@ -621,10 +633,7 @@ def update_issue_discovery(self, evaluation: 'MinerEvaluation') -> None: setattr(existing.evaluation, name, _copy_issue_discovery_value(name, value)) for repo_name, repo_eval in evaluation.repo_evaluations.items(): - target = existing.evaluation.repo_evaluations.get(repo_name) - if target is None: - target = RepoEvaluation(repository_full_name=repo_eval.repository_full_name) - existing.evaluation.repo_evaluations[repo_name] = target + target = existing.evaluation.get_or_create_repo_evaluation(repo_name, repo_eval.repository_full_name) target.copy_issue_discovery_from(repo_eval) bt.logging.debug(f'Refreshed cached issue discovery for UID {evaluation.uid}') diff --git a/gittensor/validator/issue_discovery/scan.py b/gittensor/validator/issue_discovery/scan.py index 2365b57a4..4f6310663 100644 --- a/gittensor/validator/issue_discovery/scan.py +++ b/gittensor/validator/issue_discovery/scan.py @@ -40,7 +40,7 @@ import bittensor as bt -from gittensor.classes import Issue, MinerEvaluation, MinerEvaluationCache, RepoEvaluation +from gittensor.classes import Issue, MinerEvaluation, MinerEvaluationCache from gittensor.constants import ( MAINTAINER_ASSOCIATIONS, ) @@ -278,10 +278,7 @@ def _apply_open_issue_counts(evaluation: MinerEvaluation, open_counts: Dict[str, """Record per-repo open-issue counts (and the round-level total) for a miner with no in-window issues to score.""" for repo_name, count in open_counts.items(): - repo_eval = evaluation.repo_evaluations.get(repo_name) - if repo_eval is None: - repo_eval = RepoEvaluation(repository_full_name=repo_name) - evaluation.repo_evaluations[repo_name] = repo_eval + repo_eval = evaluation.get_or_create_repo_evaluation(repo_name) repo_eval.total_open_issues = count evaluation.total_open_issues = sum(open_counts.values()) @@ -297,10 +294,7 @@ def _copy_issue_discovery_fields(target: MinerEvaluation, source: MinerEvaluatio target.total_open_issues = source.total_open_issues target.issue_discovery_issues = list(source.issue_discovery_issues) for repo_name, source_repo in source.repo_evaluations.items(): - target_repo = target.repo_evaluations.get(repo_name) - if target_repo is None: - target_repo = RepoEvaluation(repository_full_name=source_repo.repository_full_name) - target.repo_evaluations[repo_name] = target_repo + target_repo = target.get_or_create_repo_evaluation(repo_name, source_repo.repository_full_name) target_repo.copy_issue_discovery_from(source_repo) @@ -560,10 +554,7 @@ def _finalize_repo_issue_scores( acc = repo_acc.get(repo_name) or _RepoIssueAcc() open_count = open_counts.get(repo_name, 0) - repo_eval = evaluation.repo_evaluations.get(repo_name) - if repo_eval is None: - repo_eval = RepoEvaluation(repository_full_name=repo_name) - evaluation.repo_evaluations[repo_name] = repo_eval + repo_eval = evaluation.get_or_create_repo_evaluation(repo_name) repo_eval.total_solved_issues = acc.solved repo_eval.total_valid_solved_issues = acc.valid_solved From 738985a1c77ec5e71f07e876cf416a45cf31016d Mon Sep 17 00:00:00 2001 From: Landyn Date: Thu, 25 Jun 2026 19:29:59 -0500 Subject: [PATCH 13/14] feat(validator): replace bundled-JSON fallback with on-disk last-good cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Layers the disk-cache resilience on top of anderdc's API-fetch loader and removes the bundled registry entirely (das DB is now the sole source of truth). - load_master_repo_weights(): on a successful GET /repos, write the registry to an on-disk last-good cache; on API failure read that cache; with no cache, return an empty registry (logged loudly) — per-field knob defaults still apply. - _write_registry_to_cache() writes atomically (tmp + replace) and is best-effort (a cache-write failure never breaks the scoring cycle); _load_registry_from_cache() + _get_repos_cache_path() (env GITTENSOR_REPOS_CACHE_PATH, ~/.gittensor default). - constants: add REPOS_CACHE_PATH; refresh stale master_repositories.json comments. - DELETE gittensor/validator/weights/master_repositories.json and the _load_registry_from_file seed path (languages/token JSON untouched). - tests: conftest autouse fixture now points the cache at an empty per-test tmp (default load => empty); injection tests warm the cache instead of the bundled file; obsolete live-registry-content tests removed; new disk-cache tests added (cache-write on success, fallback-to-cache, empty-when-no-cache, invalid-cache). MERGE GATE: do not ship before the gt-utils seed migration has populated the prod DB so GET /repos is non-empty (critical sequencing risk in the spec). --- gittensor/constants.py | 14 +- gittensor/validator/utils/load_weights.py | 62 ++- .../weights/master_repositories.json | 379 ------------------ tests/validator/conftest.py | 15 +- tests/validator/test_load_weights.py | 259 +++--------- 5 files changed, 114 insertions(+), 615 deletions(-) delete mode 100644 gittensor/validator/weights/master_repositories.json diff --git a/gittensor/constants.py b/gittensor/constants.py index eff6ddbe6..3e315ae79 100644 --- a/gittensor/constants.py +++ b/gittensor/constants.py @@ -1,4 +1,5 @@ # Entrius 2025 +import os import re from typing import Dict @@ -38,11 +39,16 @@ # das-gittensor API (https://api.gittensor.io) — repository hyperparameter registry # ============================================================================= # GET /repos returns the full master-repository registry (full_name -> raw config), -# the authoritative source for repository hyperparameters. The bundled -# master_repositories.json is only a fallback seed when this endpoint is unreachable. +# the sole source of truth for repository hyperparameters. On every successful +# fetch the validator writes the registry to an on-disk last-good cache; when the +# API is unreachable it falls back to that cache (and to built-in knob defaults if +# no cache exists). There is no bundled master_repositories.json. GITTENSOR_API_DEFAULT_URL = 'https://api.gittensor.io' REPOS_API_TIMEOUT_SECONDS = 15 REPOS_API_MAX_ATTEMPTS = 3 +# On-disk last-good cache for the repos registry. Env-configurable; defaults under +# the user's home so it survives validator restarts. '~' is expanded at use. +REPOS_CACHE_PATH = os.getenv('GITTENSOR_REPOS_CACHE_PATH', '~/.gittensor/cache/repos_registry.json') # ============================================================================= # Language & File Scoring @@ -133,14 +139,14 @@ # ============================================================================= # Eligibility Gate (OSS Contributions) # ============================================================================= -# Per-repo defaults — each repo may override these in master_repositories.json. +# Per-repo defaults — each repo may override these via its registry config (GET /repos). MIN_VALID_MERGED_PRS = 3 # minimum merged PRs (per repo) to receive score MIN_CREDIBILITY = 0.80 # minimum credibility ratio to receive score # ============================================================================= # Issue Discovery # ============================================================================= -# Eligibility gate — per-repo defaults, overridable in master_repositories.json. +# Eligibility gate — per-repo defaults, overridable via the registry config (GET /repos). MIN_VALID_SOLVED_ISSUES = 3 # minimum solved issues where solving PR has token_score >= MIN_TOKEN_SCORE_FOR_VALID_ISSUE MIN_ISSUE_CREDIBILITY = 0.80 # minimum issue credibility ratio MIN_TOKEN_SCORE_FOR_VALID_ISSUE = 5 # solving-PR token_score for a solved issue to count as "valid" diff --git a/gittensor/validator/utils/load_weights.py b/gittensor/validator/utils/load_weights.py index f5ebfe2bc..ad460f761 100644 --- a/gittensor/validator/utils/load_weights.py +++ b/gittensor/validator/utils/load_weights.py @@ -30,6 +30,7 @@ PR_LOOKBACK_DAYS, REPOS_API_MAX_ATTEMPTS, REPOS_API_TIMEOUT_SECONDS, + REPOS_CACHE_PATH, REVIEW_PENALTY_RATE, SRC_TOK_SATURATION_SCALE, STANDARD_ISSUE_MULTIPLIER, @@ -601,10 +602,36 @@ def _fetch_registry_from_api() -> Any: raise RepositoryRegistryError(f'repos API GET {url} failed after {REPOS_API_MAX_ATTEMPTS} attempts: {last_error}') -def _load_registry_from_file() -> Any: - """Read the bundled master_repositories.json fallback seed.""" - weights_file = _get_weights_dir() / 'master_repositories.json' - with open(weights_file, 'r') as f: +def _get_repos_cache_path() -> Path: + """Filesystem path of the on-disk last-good registry cache (env-configurable).""" + return Path(REPOS_CACHE_PATH).expanduser() + + +def _write_registry_to_cache(data: Any) -> None: + """Atomically persist the last-good registry response to the disk cache. + + Best-effort: a cache-write failure must never break the scoring cycle, so any + error is logged and swallowed. Written only after the data has parsed cleanly, + so the cache always holds a valid registry. + """ + cache_path = _get_repos_cache_path() + try: + cache_path.parent.mkdir(parents=True, exist_ok=True) + tmp_path = cache_path.with_suffix(cache_path.suffix + '.tmp') + with open(tmp_path, 'w') as f: + json.dump(data, f) + tmp_path.replace(cache_path) + except OSError as e: + bt.logging.warning(f'Failed to write repos registry cache to {cache_path}: {e}') + + +def _load_registry_from_cache() -> Any: + """Read the on-disk last-good registry cache. + + Raises FileNotFoundError when no cache exists yet, json.JSONDecodeError when + the cache file is corrupt. + """ + with open(_get_repos_cache_path(), 'r') as f: return json.load(f) @@ -613,39 +640,44 @@ def load_master_repo_weights() -> Dict[str, RepositoryConfig]: Load the repository hyperparameter registry, normalizing repo names to lowercase for case-insensitive matching. - Source of truth is the das-gittensor API (GET /repos). If the endpoint is + Source of truth is the das-gittensor API (GET /repos). On a successful fetch + the registry is written to an on-disk last-good cache. If the endpoint is unreachable or serves data that fails the registry contract, falls back to - the bundled master_repositories.json seed so a transient API outage or a - bad push can't brick scoring. + that cache so a transient API outage or a bad push can't brick scoring. Returns: Dictionary mapping normalized (lowercase) fullName -> RepositoryConfig. - Returns empty dict when both the API and the bundled seed are - unavailable. Raises RepositoryRegistryError / ValueError when the - bundled seed itself violates the registry contract. + Returns an empty dict when the API is down and no cache exists (the + registry is empty that cycle; per-field knob defaults still apply once + repos are known). Raises RepositoryRegistryError / ValueError when the + cached registry itself violates the registry contract. """ try: data = _fetch_registry_from_api() normalized = _parse_registry(data) + _write_registry_to_cache(data) bt.logging.debug(f'Loaded {len(normalized)} repository entries from the repos API') return normalized except Exception as api_error: bt.logging.warning( f'Repository registry API unavailable or invalid ({api_error}); ' - f'falling back to bundled master_repositories.json' + f'falling back to the on-disk last-good cache' ) try: - data = _load_registry_from_file() + data = _load_registry_from_cache() except FileNotFoundError: - bt.logging.error('Repos API unavailable and no bundled master_repositories.json fallback found') + bt.logging.error( + 'Repos API unavailable and no on-disk registry cache present; ' + 'returning empty registry (no repos scored this cycle)' + ) return {} except json.JSONDecodeError as e: - bt.logging.error(f'Bundled master_repositories.json is invalid JSON: {e}') + bt.logging.error(f'On-disk registry cache is invalid JSON: {e}; returning empty registry') return {} normalized = _parse_registry(data) - bt.logging.debug(f'Loaded {len(normalized)} repository entries from the bundled seed (fallback)') + bt.logging.debug(f'Loaded {len(normalized)} repository entries from the on-disk cache (fallback)') return normalized diff --git a/gittensor/validator/weights/master_repositories.json b/gittensor/validator/weights/master_repositories.json deleted file mode 100644 index a5d8f32c2..000000000 --- a/gittensor/validator/weights/master_repositories.json +++ /dev/null @@ -1,379 +0,0 @@ -{ - "anderdc/social-media-manager": { - "default_label_multiplier": 0.0, - "eligibility": { - "min_credibility": 0.0, - "min_issue_credibility": 0.0, - "min_valid_merged_prs": 0, - "min_valid_solved_issues": 0 - }, - "emission_share": 0.004, - "fixed_base_score": 1.0, - "issue_discovery_share": 0.0, - "label_multipliers": { - "crown": 1.0 - }, - "maintainer_cut": 0.0, - "scoring": { - "pr_lookback_days": 7 - }, - "trusted_label_pipeline": true - }, - "cogniax/tao-pulse-app": { - "emission_share": 0.0, - "issue_discovery_share": 0.5, - "maintainer_cut": 0.4 - }, - "DPBG/Engram.AI": { - "emission_share": 0.0, - "issue_discovery_share": 0.0 - }, - "e35ventura/taopedia": { - "emission_share": 0.05, - "issue_discovery_share": 0.0, - "maintainer_cut": 0.0, - "additional_acceptable_branches": ["test"], - "trusted_label_pipeline": true, - "default_label_multiplier": 0.0, - "label_multipliers": { - "ui-ux": 5.0, - "feature": 2.0, - "bug": 0.35, - "security": 0.35, - "other": 0.05 - }, - "eligibility": { - "min_credibility": 0.6, - "max_open_pr_threshold": 4 - }, - "scoring": { - "pr_lookback_days": 7, - "maintainer_issue_multiplier": 5.0, - "time_decay": { - "grace_period_hours": 6, - "sigmoid_midpoint_days": 2, - "sigmoid_steepness": 1.0, - "min_multiplier": 0.02 - } - } - }, - "e35ventura/taopedia-articles": { - "emission_share": 0.025, - "issue_discovery_share": 0.0, - "maintainer_cut": 0.0, - "additional_acceptable_branches": ["test"], - "trusted_label_pipeline": true, - "default_label_multiplier": 0.0, - "label_multipliers": { - "article": 1.0, - "correction": 1.25, - "image": 0.75, - "category": 0.5, - "other": 0.1 - }, - "eligibility": { - "min_credibility": 0.5, - "min_token_score_for_valid_issue": 0.0 - }, - "scoring": { - "pr_lookback_days": 7, - "time_decay": { - "grace_period_hours": 3, - "sigmoid_midpoint_days": 1, - "sigmoid_steepness": 1.25, - "min_multiplier": 0.01 - } - } - }, - "entrius/allways": { - "emission_share": 0.0, - "issue_discovery_share": 1.0, - "label_multipliers": { - "bug": 1.25, - "enhancement": 1.0, - "refactor": 0.25 - }, - "maintainer_cut": 0.0, - "trusted_label_pipeline": true - }, - "entrius/das-github-mirror": { - "emission_share": 0.005, - "issue_discovery_share": 0.5, - "label_multipliers": { - "bug": 1.25, - "enhancement": 1.1, - "refactor": 0.1 - }, - "maintainer_cut": 0.0, - "trusted_label_pipeline": true - }, - "entrius/gittensor": { - "emission_share": 0.06, - "issue_discovery_share": 0.0, - "label_multipliers": { - "bug": 1.2, - "enhancement": 1.3, - "feature": 1.0, - "cli": 0.2, - "refactor": 0.1 - }, - "maintainer_cut": 0.0, - "trusted_label_pipeline": true - }, - "entrius/oc-1": { - "default_label_multiplier": 0.0, - "eligibility": { - "min_credibility": 0.0, - "min_issue_credibility": 0.0, - "min_valid_merged_prs": 0, - "min_valid_solved_issues": 0 - }, - "emission_share": 0.0, - "fixed_base_score": 1.0, - "issue_discovery_share": 0.0, - "label_multipliers": { - "benchmark-improvement": 1.0 - }, - "maintainer_cut": 0.0, - "trusted_label_pipeline": true - }, - "Geniepod/genie-claw": { - "default_label_multiplier": 1.0, - "eligibility": { - "max_open_pr_threshold": 2 - }, - "emission_share": 0.025, - "issue_discovery_share": 0.0, - "label_multipliers": { - "enhancement": 1.25, - "optimization": 2.5, - "performance": 1.5 - }, - "maintainer_cut": 0.3, - "trusted_label_pipeline": true - }, - "gittensor-ai-lab/sparkinfer": { - "default_label_multiplier": 0.0, - "eligibility": { - "min_credibility": 0.0, - "min_issue_credibility": 0.0, - "min_valid_merged_prs": 0, - "min_valid_solved_issues": 0 - }, - "emission_share": 0.1, - "fixed_base_score": 1.0, - "issue_discovery_share": 0.0, - "label_multipliers": { - "eval:XL": 4.0, - "eval:L": 2.5, - "eval:M": 1.5, - "eval:S": 1.0, - "eval:XS": 0.5, - "eval:BASELINE": 1.0, - "eval:none": 0.0, - "eval:REJECT": 0.0 - }, - "maintainer_cut": 0.3, - "trusted_label_pipeline": true - }, - "JSONbored/awesome-claude": { - "default_label_multiplier": 0.0, - "trusted_label_pipeline": true, - "emission_share": 0.005, - "issue_discovery_share": 0.0, - "label_multipliers": { - "gittensor:bug": 0.5, - "gittensor:feature": 1.25, - "gittensor:priority": 1.75, - "slop": 0.0 - }, - "eligibility": { - "min_credibility": 0.5 - }, - "maintainer_cut": 0.5, - "scoring": { - "pr_lookback_days": 7, - "open_pr_collateral_percent": 0.2, - "review_penalty_rate": 0.2, - "standard_issue_multiplier": 1.1, - "maintainer_issue_multiplier": 1.5, - "time_decay": { - "grace_period_hours": 24, - "sigmoid_midpoint_days": 3, - "sigmoid_steepness": 1.0, - "min_multiplier": 0.05 - } - } - }, - "JSONbored/gittensory": { - "default_label_multiplier": 0.0, - "trusted_label_pipeline": true, - "emission_share": 0.1, - "issue_discovery_share": 0.0, - "label_multipliers": { - "gittensor:bug": 0.5, - "gittensor:feature": 1.25, - "gittensor:priority": 1.75, - "slop": 0.0 - }, - "eligibility": { - "min_credibility": 0.6 - }, - "maintainer_cut": 0.5, - "scoring": { - "pr_lookback_days": 7, - "open_pr_collateral_percent": 0.2, - "review_penalty_rate": 0.2, - "standard_issue_multiplier": 1.1, - "maintainer_issue_multiplier": 1.5, - "time_decay": { - "grace_period_hours": 24, - "sigmoid_midpoint_days": 3, - "sigmoid_steepness": 1.0, - "min_multiplier": 0.05 - } - } - }, - "JSONbored/metagraphed": { - "default_label_multiplier": 0.0, - "trusted_label_pipeline": true, - "emission_share": 0.25, - "issue_discovery_share": 0.0, - "label_multipliers": { - "gittensor:bug": 0.5, - "gittensor:feature": 1.5, - "gittensor:priority": 2.5, - "slop": 0.0 - }, - "eligibility": { - "min_credibility": 0.6 - }, - "maintainer_cut": 0.5, - "scoring": { - "pr_lookback_days": 7, - "open_pr_collateral_percent": 0.2, - "review_penalty_rate": 0.2, - "standard_issue_multiplier": 1.1, - "maintainer_issue_multiplier": 1.5, - "time_decay": { - "grace_period_hours": 24, - "sigmoid_midpoint_days": 3, - "sigmoid_steepness": 1.0, - "min_multiplier": 0.05 - } - } - }, - "MkDev11/gittensor-hub": { - "emission_share": 0.005, - "issue_discovery_share": 0.0, - "label_multipliers": { - "feature": 2, - "whale": 20, - "shark": 5, - "bug": 0.3, - "refactor": 0.3 - }, - "scoring": { - "open_pr_collateral_percent": 0.2, - "review_penalty_rate": 0.2, - "maintainer_issue_multiplier": 2, - "time_decay": { - "grace_period_hours": 24, - "sigmoid_midpoint_days": 14, - "min_multiplier": 0.1 - } - }, - "maintainer_cut": 0.3 - }, - "vouchdev/vouch": { - "emission_share": 0.0, - "issue_discovery_share": 0.0, - "label_multipliers": { - "feature": 1.5, - "bug": 1.1 - }, - "scoring": { - "pr_lookback_days": 30, - "open_pr_collateral_percent": 0.1, - "review_penalty_rate": 0.2, - "maintainer_issue_multiplier": 1.5, - "standard_issue_multiplier": 1, - "time_decay": { - "grace_period_hours": 24, - "sigmoid_midpoint_days": 14, - "min_multiplier": 0.1 - } - }, - "eligibility": { - "excessive_pr_penalty_base_threshold": 3 - }, - "additional_acceptable_branches": ["test"], - "maintainer_cut": 0.5 - }, - "phase-rs/phase": { - "emission_share": 0.02, - "issue_discovery_share": 0.0, - "label_multipliers": { - "bug": 1.2, - "enhancement": 1.3, - "feature": 1.0, - "refactor": 0.25, - "test": 1.0, - "quality": 3 - }, - "scoring": { - "standard_issue_multiplier": 1, - "review_penalty_rate": 0.01, - "pr_lookback_days": 15 - }, - "maintainer_cut": 0.4, - "eligibility": { - "excessive_pr_penalty_base_threshold": 10 - } - }, - "touchpilot/touchpilot": { - "emission_share": 0.0, - "issue_discovery_share": 0.0, - "label_multipliers": { - "type: bug": 1.1, - "type: enhancement": 1.25, - "type: feature": 1.5, - "type: refactor": 0.25, - "type: test": 1.2 - }, - "maintainer_cut": 0.3 - }, - "e35dev/podcast-design-canvas-2": { - "emission_share": 0.351, - "issue_discovery_share": 0, - "maintainer_cut": 0, - "trusted_label_pipeline": true, - "default_label_multiplier": 0, - "label_multipliers": { - "episode-ingest": 3, - "preset-styles": 2.5, - "canvas-editor": 2.5, - "audio-captions": 2, - "contextual-visuals": 2, - "template-system": 1.75, - "export-publish": 1.75, - "product-polish": 1.5, - "bugfix": 1, - "infrastructure": 0.5 - }, - "eligibility": { - "min_valid_merged_prs": 1, - "min_credibility": 0.6, - "max_open_pr_threshold": 1 - }, - "scoring": { - "pr_lookback_days": 7, - "time_decay": { - "grace_period_hours": 6, - "sigmoid_midpoint_days": 2, - "sigmoid_steepness": 1, - "min_multiplier": 0.02 - } - } - } -} diff --git a/tests/validator/conftest.py b/tests/validator/conftest.py index 4d7669a79..ecb630e4e 100644 --- a/tests/validator/conftest.py +++ b/tests/validator/conftest.py @@ -15,11 +15,15 @@ @pytest.fixture(autouse=True) -def _force_registry_file_fallback(monkeypatch): - """Default validator tests to the bundled master_repositories.json by making - the repos API fetch fail, so tests stay offline and deterministic. Tests that - exercise the API path re-patch ``_fetch_registry_from_api`` themselves; their - setattr runs after this fixture and wins. +def _isolate_repo_registry(monkeypatch, tmp_path): + """Keep validator tests offline and deterministic: + + - the repos API fetch always fails, so loads exercise the fallback path + (tests covering the API re-patch ``_fetch_registry_from_api`` themselves; + their setattr runs after this fixture and wins); + - the on-disk last-good cache points at an empty per-test tmp path, so the + default load yields an empty registry. Tests that want a warm-cache + fallback write their registry JSON to ``lw._get_repos_cache_path()``. """ from gittensor.validator.utils import load_weights as lw @@ -27,6 +31,7 @@ def _api_disabled(): raise lw.RepositoryRegistryError('repos API disabled in tests') monkeypatch.setattr(lw, '_fetch_registry_from_api', _api_disabled) + monkeypatch.setattr(lw, '_get_repos_cache_path', lambda: tmp_path / 'repos_cache.json') @dataclass diff --git a/tests/validator/test_load_weights.py b/tests/validator/test_load_weights.py index 918dbac8b..67b5ed70e 100644 --- a/tests/validator/test_load_weights.py +++ b/tests/validator/test_load_weights.py @@ -27,13 +27,6 @@ ) -def _live_master_repo_metadata(): - from gittensor.validator.utils import load_weights as lw - - with open(lw._get_weights_dir() / 'master_repositories.json', 'r') as f: - return sorted(json.load(f).items()) - - class TestLoadTokenWeights: """Tests for loading token_weights.json via load_token_config().""" @@ -103,18 +96,13 @@ def test_tree_sitter_languages_have_language_field(self): class TestLoadMasterRepositories: - """Tests for loading master_repositories.json via load_master_repo_weights().""" + """Tests for the repository registry loader load_master_repo_weights().""" def test_load_master_repo_weights_returns_dict(self): """load_master_repo_weights() should return a dictionary.""" repos = load_master_repo_weights() assert isinstance(repos, dict) - def test_master_repositories_not_empty(self): - """Should load at least the entrius core repos.""" - repos = load_master_repo_weights() - assert len(repos) > 0, 'Should have at least one repository' - def test_repo_configs_are_repository_config_objects(self): """Each entry should be a RepositoryConfig object.""" repos = load_master_repo_weights() @@ -127,25 +115,6 @@ def test_repo_names_are_lowercase(self): for repo_name in repos.keys(): assert repo_name == repo_name.lower(), f'{repo_name} should be lowercase' - 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_have_trusted_label_pipeline(self): - """All entrius/* entries opt into trusted_label_pipeline (issue #911).""" - repos = load_master_repo_weights() - entrius_repos = {name: cfg for name, cfg 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 have trusted_label_pipeline=true so the agentic-maintainer ' - f'labeling worker is honored at scoring time' - ) - class TestRepositoryConfigTrustedLabelPipeline: """Dataclass + JSON-parsing tests for trusted_label_pipeline (issue #911).""" @@ -167,7 +136,7 @@ def test_loader_parses_trusted_label_pipeline_true(self, tmp_path, monkeypatch): from gittensor.validator.utils import load_weights as lw fake_weights_dir = tmp_path - (fake_weights_dir / 'master_repositories.json').write_text( + (fake_weights_dir / 'repos_cache.json').write_text( json.dumps( { 'foo/trusted': {'emission_share': 0.5, 'trusted_label_pipeline': True}, @@ -176,8 +145,6 @@ def test_loader_parses_trusted_label_pipeline_true(self, tmp_path, monkeypatch): } ) ) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: fake_weights_dir) - repos = lw.load_master_repo_weights() assert repos['foo/trusted'].trusted_label_pipeline is True @@ -198,7 +165,7 @@ def test_loader_parses_label_multiplier_config(self, tmp_path, monkeypatch): from gittensor.validator.utils import load_weights as lw fake_weights_dir = tmp_path - (fake_weights_dir / 'master_repositories.json').write_text( + (fake_weights_dir / 'repos_cache.json').write_text( json.dumps( { 'foo/labeled': { @@ -210,8 +177,6 @@ def test_loader_parses_label_multiplier_config(self, tmp_path, monkeypatch): } ) ) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: fake_weights_dir) - repos = lw.load_master_repo_weights() assert repos['foo/labeled'].label_multipliers == {'kind/*': 1.5, 'type:bug': 1.25} @@ -220,32 +185,6 @@ def test_loader_parses_label_multiplier_config(self, tmp_path, monkeypatch): assert repos['foo/defaults'].label_multipliers is None assert repos['foo/defaults'].default_label_multiplier == pytest.approx(1.0) - @pytest.mark.parametrize('repo_name,metadata', _live_master_repo_metadata()) - def test_live_label_multiplier_maps_are_bounded(self, repo_name, metadata): - label_multipliers = metadata.get('label_multipliers') - if label_multipliers is None: - return - - assert isinstance(label_multipliers, dict), f'{repo_name} label_multipliers must be a dict' - assert len(label_multipliers) <= 10, f'{repo_name} label_multipliers has too many entries' - - @pytest.mark.parametrize('repo_name,metadata', _live_master_repo_metadata()) - def test_live_label_multiplier_values_are_in_range(self, repo_name, metadata): - for pattern, multiplier in (metadata.get('label_multipliers') or {}).items(): - assert isinstance(pattern, str), f'{repo_name} label_multipliers keys must be strings' - assert 0.0 <= float(multiplier) <= 20.0, ( - f'{repo_name} label_multipliers[{pattern!r}] must be within [0.0, 20.0]' - ) - - @pytest.mark.parametrize('repo_name,metadata', _live_master_repo_metadata()) - def test_live_default_label_multiplier_values_are_in_range(self, repo_name, metadata): - if 'default_label_multiplier' not in metadata: - return - - assert 0.0 <= float(metadata['default_label_multiplier']) <= 20.0, ( - f'{repo_name} default_label_multiplier must be within [0.0, 20.0]' - ) - class TestRepositoryConfigMirrorScoringFields: """Dataclass + JSON-parsing tests for mirror scoring + per-repo eligibility fields.""" @@ -259,7 +198,7 @@ def test_mirror_scoring_field_defaults(self): def test_loader_parses_fixed_base_score(self, tmp_path, monkeypatch): from gittensor.validator.utils import load_weights as lw - (tmp_path / 'master_repositories.json').write_text( + (tmp_path / 'repos_cache.json').write_text( json.dumps( { 'foo/fixed': {'emission_share': 0.5, 'fixed_base_score': 12.5}, @@ -267,8 +206,6 @@ def test_loader_parses_fixed_base_score(self, tmp_path, monkeypatch): } ) ) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: tmp_path) - repos = lw.load_master_repo_weights() assert repos['foo/fixed'].fixed_base_score == pytest.approx(12.5) @@ -277,7 +214,7 @@ def test_loader_parses_fixed_base_score(self, tmp_path, monkeypatch): def test_loader_parses_eligibility_overrides(self, tmp_path, monkeypatch): from gittensor.validator.utils import load_weights as lw - (tmp_path / 'master_repositories.json').write_text( + (tmp_path / 'repos_cache.json').write_text( json.dumps( { 'foo/custom': { @@ -288,8 +225,6 @@ def test_loader_parses_eligibility_overrides(self, tmp_path, monkeypatch): } ) ) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: tmp_path) - repos = lw.load_master_repo_weights() assert repos['foo/custom'].eligibility.min_valid_merged_prs == 1 @@ -301,78 +236,21 @@ def test_loader_parses_eligibility_overrides(self, tmp_path, monkeypatch): def test_loader_rejects_unknown_eligibility_key(self, tmp_path, monkeypatch): from gittensor.validator.utils import load_weights as lw - (tmp_path / 'master_repositories.json').write_text( + (tmp_path / 'repos_cache.json').write_text( json.dumps({'foo/bad': {'emission_share': 0.5, 'eligibility': {'min_valid_prs': 1}}}) ) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: tmp_path) - with pytest.raises(RepositoryRegistryError): lw.load_master_repo_weights() def test_loader_rejects_out_of_range_credibility(self, tmp_path, monkeypatch): from gittensor.validator.utils import load_weights as lw - (tmp_path / 'master_repositories.json').write_text( + (tmp_path / 'repos_cache.json').write_text( json.dumps({'foo/bad': {'emission_share': 0.5, 'eligibility': {'min_credibility': 1.5}}}) ) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: tmp_path) - with pytest.raises(RepositoryRegistryError): lw.load_master_repo_weights() - def test_live_mirror_scoring_fields_have_valid_shape(self): - """Loader passes scoring + eligibility fields through unchanged — CI fails - when a bad value is committed to master_repositories.json.""" - repos = load_master_repo_weights() - for repo_name, config in repos.items(): - if config.fixed_base_score is not None: - assert isinstance(config.fixed_base_score, (int, float)) and not isinstance( - config.fixed_base_score, bool - ), f'{repo_name} fixed_base_score must be numeric, got {type(config.fixed_base_score)}' - assert 0.0 <= float(config.fixed_base_score) <= 100.0, ( - f'{repo_name} fixed_base_score must be within [0.0, 100.0]' - ) - resolved = resolve_eligibility(config.eligibility) - assert 0.0 <= resolved.min_credibility <= 1.0, f'{repo_name} min_credibility out of range' - assert 0.0 <= resolved.min_issue_credibility <= 1.0, f'{repo_name} min_issue_credibility out of range' - assert resolved.min_valid_merged_prs >= 0, f'{repo_name} min_valid_merged_prs negative' - resolved_scoring = resolve_scoring(config.scoring) - assert 1 <= resolved_scoring.pr_lookback_days <= 90, f'{repo_name} pr_lookback_days out of range' - assert 0.0 <= resolved_scoring.open_pr_collateral_percent <= 1.0, ( - f'{repo_name} open_pr_collateral_percent out of range' - ) - assert 0.0 < resolved_scoring.review_penalty_rate <= 1.0, f'{repo_name} review_penalty_rate out of range' - assert 1.0 <= resolved_scoring.standard_issue_multiplier <= 5.0, ( - f'{repo_name} standard_issue_multiplier out of range' - ) - assert 1.0 <= resolved_scoring.maintainer_issue_multiplier <= 5.0, ( - f'{repo_name} maintainer_issue_multiplier out of range' - ) - assert 10.0 <= resolved_scoring.src_tok_saturation_scale <= 500.0, ( - f'{repo_name} src_tok_saturation_scale out of range' - ) - assert 0 <= resolved_scoring.time_decay.grace_period_hours <= 168, ( - f'{repo_name} time_decay.grace_period_hours out of range' - ) - assert 1.0 <= resolved_scoring.time_decay.sigmoid_midpoint_days <= 90.0, ( - f'{repo_name} time_decay.sigmoid_midpoint_days out of range' - ) - assert 0.01 <= resolved_scoring.time_decay.sigmoid_steepness <= 5.0, ( - f'{repo_name} time_decay.sigmoid_steepness out of range' - ) - assert 0.0 <= resolved_scoring.time_decay.min_multiplier <= 1.0, ( - f'{repo_name} time_decay.min_multiplier out of range' - ) - - def test_oc_1_runs_ungated(self): - """The oc-1 benchmark repo opts out of the gate via zeroed thresholds.""" - repos = load_master_repo_weights() - resolved = resolve_eligibility(repos['entrius/oc-1'].eligibility) - assert resolved.min_valid_merged_prs == 0 - assert resolved.min_credibility == 0.0 - assert resolved.min_valid_solved_issues == 0 - - class TestRepositoryConfigScoringBlock: """Dataclass + JSON-parsing tests for the per-repo scoring block.""" @@ -383,7 +261,7 @@ def test_scoring_field_defaults(self): def test_loader_parses_scoring_overrides(self, tmp_path, monkeypatch): from gittensor.validator.utils import load_weights as lw - (tmp_path / 'master_repositories.json').write_text( + (tmp_path / 'repos_cache.json').write_text( json.dumps( { 'foo/custom': { @@ -398,8 +276,6 @@ def test_loader_parses_scoring_overrides(self, tmp_path, monkeypatch): } ) ) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: tmp_path) - repos = lw.load_master_repo_weights() assert repos['foo/custom'].scoring.pr_lookback_days == 45 @@ -410,55 +286,45 @@ def test_loader_parses_scoring_overrides(self, tmp_path, monkeypatch): def test_loader_rejects_unknown_scoring_key(self, tmp_path, monkeypatch): from gittensor.validator.utils import load_weights as lw - (tmp_path / 'master_repositories.json').write_text( + (tmp_path / 'repos_cache.json').write_text( json.dumps({'foo/bad': {'emission_share': 0.5, 'scoring': {'bogus': 1}}}) ) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: tmp_path) - with pytest.raises(RepositoryRegistryError): lw.load_master_repo_weights() def test_loader_rejects_out_of_range_collateral(self, tmp_path, monkeypatch): from gittensor.validator.utils import load_weights as lw - (tmp_path / 'master_repositories.json').write_text( + (tmp_path / 'repos_cache.json').write_text( json.dumps({'foo/bad': {'emission_share': 0.5, 'scoring': {'open_pr_collateral_percent': 1.5}}}) ) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: tmp_path) - with pytest.raises(RepositoryRegistryError): lw.load_master_repo_weights() def test_loader_rejects_zero_review_penalty_rate(self, tmp_path, monkeypatch): from gittensor.validator.utils import load_weights as lw - (tmp_path / 'master_repositories.json').write_text( + (tmp_path / 'repos_cache.json').write_text( json.dumps({'foo/bad': {'emission_share': 0.5, 'scoring': {'review_penalty_rate': 0.0}}}) ) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: tmp_path) - with pytest.raises(RepositoryRegistryError): lw.load_master_repo_weights() def test_loader_rejects_out_of_range_issue_multiplier(self, tmp_path, monkeypatch): from gittensor.validator.utils import load_weights as lw - (tmp_path / 'master_repositories.json').write_text( + (tmp_path / 'repos_cache.json').write_text( json.dumps({'foo/bad': {'emission_share': 0.5, 'scoring': {'standard_issue_multiplier': 0.5}}}) ) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: tmp_path) - with pytest.raises(RepositoryRegistryError): lw.load_master_repo_weights() def test_loader_parses_time_decay_overrides(self, tmp_path, monkeypatch): from gittensor.validator.utils import load_weights as lw - (tmp_path / 'master_repositories.json').write_text( + (tmp_path / 'repos_cache.json').write_text( json.dumps({'foo/custom': {'emission_share': 0.5, 'scoring': {'time_decay': {'grace_period_hours': 24}}}}) ) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: tmp_path) - repos = lw.load_master_repo_weights() assert repos['foo/custom'].scoring.time_decay.grace_period_hours == 24 @@ -466,22 +332,18 @@ def test_loader_parses_time_decay_overrides(self, tmp_path, monkeypatch): def test_loader_rejects_unknown_time_decay_key(self, tmp_path, monkeypatch): from gittensor.validator.utils import load_weights as lw - (tmp_path / 'master_repositories.json').write_text( + (tmp_path / 'repos_cache.json').write_text( json.dumps({'foo/bad': {'emission_share': 0.5, 'scoring': {'time_decay': {'bogus': 1}}}}) ) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: tmp_path) - with pytest.raises(RepositoryRegistryError): lw.load_master_repo_weights() def test_loader_rejects_out_of_range_lookback(self, tmp_path, monkeypatch): from gittensor.validator.utils import load_weights as lw - (tmp_path / 'master_repositories.json').write_text( + (tmp_path / 'repos_cache.json').write_text( json.dumps({'foo/bad': {'emission_share': 0.5, 'scoring': {'pr_lookback_days': 200}}}) ) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: tmp_path) - with pytest.raises(RepositoryRegistryError): lw.load_master_repo_weights() @@ -489,18 +351,16 @@ def test_loader_rejects_out_of_range_lookback(self, tmp_path, monkeypatch): def test_loader_rejects_out_of_range_saturation_scale(self, tmp_path, monkeypatch, scale): from gittensor.validator.utils import load_weights as lw - (tmp_path / 'master_repositories.json').write_text( + (tmp_path / 'repos_cache.json').write_text( json.dumps({'foo/bad': {'emission_share': 0.5, 'scoring': {'src_tok_saturation_scale': scale}}}) ) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: tmp_path) - with pytest.raises(RepositoryRegistryError): lw.load_master_repo_weights() def test_loader_accepts_saturation_scale_at_bounds(self, tmp_path, monkeypatch): from gittensor.validator.utils import load_weights as lw - (tmp_path / 'master_repositories.json').write_text( + (tmp_path / 'repos_cache.json').write_text( json.dumps( { 'foo/low': {'emission_share': 0.4, 'scoring': {'src_tok_saturation_scale': 10.0}}, @@ -508,8 +368,6 @@ def test_loader_accepts_saturation_scale_at_bounds(self, tmp_path, monkeypatch): } ) ) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: tmp_path) - repos = lw.load_master_repo_weights() assert resolve_scoring(repos['foo/low'].scoring).src_tok_saturation_scale == pytest.approx(10.0) assert resolve_scoring(repos['foo/high'].scoring).src_tok_saturation_scale == pytest.approx(500.0) @@ -526,7 +384,7 @@ def test_loader_parses_maintainer_cut(self, tmp_path, monkeypatch): from gittensor.validator.utils import load_weights as lw fake_weights_dir = tmp_path - (fake_weights_dir / 'master_repositories.json').write_text( + (fake_weights_dir / 'repos_cache.json').write_text( json.dumps( { 'foo/with-cut': {'emission_share': 0.5, 'maintainer_cut': 0.3}, @@ -534,20 +392,11 @@ def test_loader_parses_maintainer_cut(self, tmp_path, monkeypatch): } ) ) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: fake_weights_dir) - repos = lw.load_master_repo_weights() assert repos['foo/with-cut'].maintainer_cut == pytest.approx(0.3) assert repos['foo/defaults'].maintainer_cut == pytest.approx(0.0) - @pytest.mark.parametrize('repo_name,metadata', _live_master_repo_metadata()) - def test_live_maintainer_cut_is_in_range(self, repo_name, metadata): - if 'maintainer_cut' not in metadata: - return - - assert 0.0 <= float(metadata['maintainer_cut']) <= 1.0, f'{repo_name} maintainer_cut must be within [0.0, 1.0]' - class TestRepositoryEmissionShare: """Tests for bounded repo emission_share loading.""" @@ -568,7 +417,7 @@ def test_loader_parses_issue_discovery_share(self, tmp_path, monkeypatch): from gittensor.validator.utils import load_weights as lw fake_weights_dir = tmp_path - (fake_weights_dir / 'master_repositories.json').write_text( + (fake_weights_dir / 'repos_cache.json').write_text( json.dumps( { 'foo/pr-only': {'emission_share': 0.4, 'issue_discovery_share': 0.0}, @@ -576,8 +425,6 @@ def test_loader_parses_issue_discovery_share(self, tmp_path, monkeypatch): } ) ) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: fake_weights_dir) - repos = lw.load_master_repo_weights() assert repos['foo/pr-only'].issue_discovery_share == pytest.approx(0.0) @@ -587,11 +434,9 @@ def test_loader_accepts_sum_less_than_one(self, tmp_path, monkeypatch): from gittensor.validator.utils import load_weights as lw fake_weights_dir = tmp_path - (fake_weights_dir / 'master_repositories.json').write_text( + (fake_weights_dir / 'repos_cache.json').write_text( json.dumps({'foo/a': {'emission_share': 0.2}, 'foo/b': {'emission_share': 0.3}}) ) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: fake_weights_dir) - repos = lw.load_master_repo_weights() assert set(repos) == {'foo/a', 'foo/b'} @@ -612,9 +457,7 @@ def test_loader_rejects_out_of_range_values(self, tmp_path, monkeypatch, metadat 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': metadata})) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: fake_weights_dir) - + (fake_weights_dir / 'repos_cache.json').write_text(json.dumps({'foo/bad': metadata})) with pytest.raises(RepositoryRegistryError): lw.load_master_repo_weights() @@ -630,9 +473,7 @@ def test_loader_rejects_boolean_share_values(self, tmp_path, monkeypatch, metada 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': metadata})) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: fake_weights_dir) - + (fake_weights_dir / 'repos_cache.json').write_text(json.dumps({'foo/bad': metadata})) with pytest.raises(RepositoryRegistryError, match='must be a float'): lw.load_master_repo_weights() @@ -640,26 +481,15 @@ def test_loader_rejects_sum_greater_than_one(self, tmp_path, monkeypatch): from gittensor.validator.utils import load_weights as lw fake_weights_dir = tmp_path - (fake_weights_dir / 'master_repositories.json').write_text( + (fake_weights_dir / 'repos_cache.json').write_text( json.dumps({'foo/a': {'emission_share': 0.6}, 'foo/b': {'emission_share': 0.5}}) ) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: fake_weights_dir) - with pytest.raises(RepositoryRegistryError, match='total emission_share must be <= 1.0'): lw.load_master_repo_weights() - def test_live_master_repo_emission_shares_are_valid(self): - repos = load_master_repo_weights() - total = sum(config.emission_share for config in repos.values()) - - assert 0.0 <= total <= 1.0 - for repo_name, config in repos.items(): - assert 0.0 <= config.emission_share <= 1.0, f'{repo_name} emission_share out of range' - assert 0.0 <= config.issue_discovery_share <= 1.0, f'{repo_name} issue_discovery_share out of range' - class TestRegistryApiLoading: - """Tests for the API-first loader with bundled-seed fallback.""" + """Tests for the API-first loader with on-disk last-good cache fallback.""" def test_loads_from_api_when_available(self, monkeypatch): from gittensor.validator.utils import load_weights as lw @@ -673,46 +503,51 @@ def test_loads_from_api_when_available(self, monkeypatch): assert repos['owner/repo'].emission_share == 0.1 assert repos['owner/repo'].label_multipliers == {'feature': 2.0} - def test_falls_back_to_file_when_api_unavailable(self, tmp_path, monkeypatch): - # autouse fixture already makes the API fetch fail; supply a seed file. + def test_successful_fetch_writes_disk_cache(self, monkeypatch): + # A successful fetch persists the last-good registry for outage resilience. from gittensor.validator.utils import load_weights as lw - (tmp_path / 'master_repositories.json').write_text(json.dumps({'a/b': {'emission_share': 0.2}})) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: tmp_path) + payload = {'owner/repo': {'emission_share': 0.1}} + monkeypatch.setattr(lw, '_fetch_registry_from_api', lambda: payload) + + lw.load_master_repo_weights() + + cache_path = lw._get_repos_cache_path() + assert cache_path.exists(), 'successful API fetch should write the last-good cache' + assert json.loads(cache_path.read_text()) == payload + + def test_falls_back_to_disk_cache_when_api_unavailable(self, tmp_path): + # autouse fixture fails the API and points the cache at tmp_path; warm it. + from gittensor.validator.utils import load_weights as lw + lw._get_repos_cache_path().write_text(json.dumps({'a/b': {'emission_share': 0.2}})) repos = lw.load_master_repo_weights() assert repos['a/b'].emission_share == 0.2 - def test_api_invalid_content_falls_back_to_seed(self, tmp_path, monkeypatch): + def test_api_invalid_content_falls_back_to_cache(self, monkeypatch): from gittensor.validator.utils import load_weights as lw - # API serves data violating the emission contract -> fall back to seed. + # API serves data violating the emission contract -> fall back to the cache. monkeypatch.setattr(lw, '_fetch_registry_from_api', lambda: {'a/b': {'emission_share': 5.0}}) - (tmp_path / 'master_repositories.json').write_text(json.dumps({'a/b': {'emission_share': 0.3}})) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: tmp_path) - + lw._get_repos_cache_path().write_text(json.dumps({'a/b': {'emission_share': 0.3}})) repos = lw.load_master_repo_weights() assert repos['a/b'].emission_share == 0.3 - def test_returns_empty_when_api_down_and_no_seed(self, tmp_path, monkeypatch): - # autouse fixture disables the API; point the seed lookup at an empty dir. + def test_returns_empty_when_api_down_and_no_cache(self): + # autouse fixture disables the API and points the cache at an empty tmp path. from gittensor.validator.utils import load_weights as lw - monkeypatch.setattr(lw, '_get_weights_dir', lambda: tmp_path) - assert lw.load_master_repo_weights() == {} - def test_invalid_seed_still_raises(self, tmp_path, monkeypatch): - # A broken bundled seed is a real bug and must surface, not be swallowed. + def test_invalid_cache_still_raises(self): + # A cache that violates the registry contract is a real problem and must surface. from gittensor.validator.utils import load_weights as lw - (tmp_path / 'master_repositories.json').write_text( + lw._get_repos_cache_path().write_text( json.dumps({'foo/a': {'emission_share': 0.6}, 'foo/b': {'emission_share': 0.5}}) ) - monkeypatch.setattr(lw, '_get_weights_dir', lambda: tmp_path) - with pytest.raises(RepositoryRegistryError, match='total emission_share must be <= 1.0'): lw.load_master_repo_weights() From 361a98c03cd62d76a0bca6d6f1bb62bc44b663b3 Mon Sep 17 00:00:00 2001 From: LandynDev <60993791+LandynDev@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:00:45 +0000 Subject: [PATCH 14/14] style: auto-format with pre-commit --- gittensor/validator/utils/load_weights.py | 3 +-- tests/validator/test_load_weights.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/gittensor/validator/utils/load_weights.py b/gittensor/validator/utils/load_weights.py index ad460f761..4bf5a8df9 100644 --- a/gittensor/validator/utils/load_weights.py +++ b/gittensor/validator/utils/load_weights.py @@ -660,8 +660,7 @@ def load_master_repo_weights() -> Dict[str, RepositoryConfig]: return normalized except Exception as api_error: bt.logging.warning( - f'Repository registry API unavailable or invalid ({api_error}); ' - f'falling back to the on-disk last-good cache' + f'Repository registry API unavailable or invalid ({api_error}); falling back to the on-disk last-good cache' ) try: diff --git a/tests/validator/test_load_weights.py b/tests/validator/test_load_weights.py index 67b5ed70e..05e36f004 100644 --- a/tests/validator/test_load_weights.py +++ b/tests/validator/test_load_weights.py @@ -22,7 +22,6 @@ load_master_repo_weights, load_programming_language_weights, load_token_config, - resolve_eligibility, resolve_scoring, ) @@ -251,6 +250,7 @@ def test_loader_rejects_out_of_range_credibility(self, tmp_path, monkeypatch): with pytest.raises(RepositoryRegistryError): lw.load_master_repo_weights() + class TestRepositoryConfigScoringBlock: """Dataclass + JSON-parsing tests for the per-repo scoring block."""