Skip to content

Commit 20c4598

Browse files
authored
Merge branch 'test' into fix/cli-help-bittensor-argparse-hijack
2 parents 9726b24 + 86390ad commit 20c4598

32 files changed

Lines changed: 1376 additions & 289 deletions

‎gittensor/classes.py‎

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -305,12 +305,8 @@ def from_graphql_response(cls, pr_data: dict, uid: int, hotkey: str, github_id:
305305
last_edited_at = parse_github_timestamp_to_cst(raw_edited_at) if isinstance(raw_edited_at, str) else None
306306
merged_at = parse_github_timestamp_to_cst(pr_data['mergedAt']) if is_merged else None
307307

308-
changes_requested_count = 0
309-
if is_merged:
310-
cr_reviews = pr_data.get('changesRequestedReviews', {}).get('nodes', [])
311-
changes_requested_count = sum(
312-
1 for r in cr_reviews if r.get('authorAssociation') in MAINTAINER_ASSOCIATIONS
313-
)
308+
cr_reviews = (pr_data.get('changesRequestedReviews') or {}).get('nodes') or []
309+
changes_requested_count = sum(1 for r in cr_reviews if r.get('authorAssociation') in MAINTAINER_ASSOCIATIONS)
314310

315311
current = {(n.get('name') or '').lower() for n in (pr_data.get('labels') or {}).get('nodes') or [] if n}
316312
label: Optional[str] = None
@@ -370,10 +366,15 @@ class MinerEvaluation:
370366
total_leaf_score: float = 0.0
371367
failed_reason: Optional[str] = None
372368
github_pr_fetch_failed: bool = False
369+
# Mirror-source-specific fetch flag set by mirror.combine.combine alongside
370+
# the OR into github_pr_fetch_failed. Lets the validator tell a complete
371+
# mirror outage apart from a legacy partial-pagination failure.
372+
mirror_pr_fetch_failed: bool = False
373373
evaluation_timestamp: Optional[datetime] = None
374374
merged_pull_requests: List[PullRequest] = field(default_factory=list)
375375
open_pull_requests: List[PullRequest] = field(default_factory=list)
376376
closed_pull_requests: List[PullRequest] = field(default_factory=list)
377+
stale_closed_pull_requests: List[PullRequest] = field(default_factory=list)
377378

378379
# Populated by gittensor.validator.oss_contributions.mirror.combine.combine
379380
# when the mirror scoring path runs. Empty for legacy-only evaluations.
@@ -485,6 +486,13 @@ def add_closed_pull_request(self, raw_pr: Dict):
485486
PullRequest.from_graphql_response(raw_pr, self.uid, self.hotkey, self.github_id)
486487
)
487488

489+
def add_stale_closed_pull_request(self, raw_pr: Dict):
490+
"""Track a stale CLOSED PR so storage can refresh its pull_requests row."""
491+
bt.logging.info(f'Stale CLOSED PR #{raw_pr["number"]} in {parse_repo_name(raw_pr["repository"])}')
492+
self.stale_closed_pull_requests.append(
493+
PullRequest.from_graphql_response(raw_pr, self.uid, self.hotkey, self.github_id)
494+
)
495+
488496

489497
@dataclass
490498
class ScoreBreakdown:
@@ -708,6 +716,9 @@ def _build_cache_entry(evaluation: 'MinerEvaluation') -> 'MinerEvaluation':
708716
cached.merged_pull_requests = [_pr_for_cache(pr) for pr in evaluation.merged_pull_requests]
709717
cached.open_pull_requests = [_pr_for_cache(pr) for pr in evaluation.open_pull_requests]
710718
cached.closed_pull_requests = [_pr_for_cache(pr) for pr in evaluation.closed_pull_requests]
719+
cached.mirror_merged_prs = [_scored_mirror_pr_for_cache(pr) for pr in evaluation.mirror_merged_prs]
720+
cached.mirror_open_prs = [_scored_mirror_pr_for_cache(pr) for pr in evaluation.mirror_open_prs]
721+
cached.mirror_closed_prs = [_scored_mirror_pr_for_cache(pr) for pr in evaluation.mirror_closed_prs]
711722
return cached
712723

713724
@staticmethod
@@ -735,3 +746,9 @@ def _pr_with_fresh_issues(pr: 'PullRequest') -> 'PullRequest':
735746
if pr.issues is not None:
736747
pr_copy.issues = [copy.copy(issue) for issue in pr.issues]
737748
return pr_copy
749+
750+
751+
def _scored_mirror_pr_for_cache(scored: 'ScoredMirrorPR') -> 'ScoredMirrorPR':
752+
scored_copy = copy.copy(scored)
753+
scored_copy.files = None
754+
return scored_copy

‎gittensor/cli/issue_commands/help.py‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,6 @@
1616
from rich.table import Table
1717

1818

19-
def _single_paragraph(text: str) -> str:
20-
"""Collapse multiline help text into a single paragraph."""
21-
return ' '.join(text.split())
22-
23-
2419
def _collect_help_rows(params: list[click.Parameter], ctx: click.Context) -> list[tuple[str, str]]:
2520
"""Collect Click help records for parameters."""
2621
rows: list[tuple[str, str]] = []
@@ -192,7 +187,7 @@ def get_help(self, ctx: click.Context) -> str:
192187
if help_text:
193188
console.print(
194189
Padding(
195-
f'[bright_white]{escape(_single_paragraph(help_text))}[/bright_white]',
190+
f'[bright_white]{escape(" ".join(help_text.split()))}[/bright_white]',
196191
(0, 0, 0, 1),
197192
)
198193
)

‎gittensor/cli/issue_commands/helpers.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ def fetch_open_issue_pull_requests(
281281
"""Fetch open PR submissions for a GitHub issue."""
282282
token = get_github_pat() or ''
283283
if not token and not as_json:
284-
print_warning('No GitHub token (GITTENSOR_MINER_PAT) found; using unauthenticated requests (lower rate limits)')
284+
print_warning('No GitHub token found; set GITTENSOR_MINER_PAT to fetch GitHub issue submissions')
285285

286286
try:
287287
from gittensor.utils.github_api_tools import find_prs_for_issue

‎gittensor/cli/issue_commands/vote.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ def vote_list_validators(network: str, rpc_url: str, contract: str, as_json: boo
246246
validators = client.get_validators()
247247

248248
n = len(validators)
249-
required = (n // 2) + 1
249+
required = (n // 2) + 1 if n > 0 else 0
250250

251251
if as_json:
252252
emit_json(

‎gittensor/cli/miner_commands/check.py‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@
1111
from rich.table import Table
1212

1313
from .helpers import (
14+
DEFAULT_MIN_VALIDATOR_STAKE,
15+
DEFAULT_MIN_VALIDATOR_VTRUST,
1416
NETUID_DEFAULT,
1517
_connect_bittensor,
1618
_error,
1719
_load_config_value,
1820
_pat_check_aggregate_counts,
1921
_pat_check_row_category,
2022
_print,
23+
_render_skipped_validators,
2124
_require_registered,
2225
_require_validator_axons,
2326
_resolve_endpoint,
@@ -40,8 +43,22 @@
4043
@click.option('--netuid', type=int, default=NETUID_DEFAULT, help='Subnet UID.', show_default=True)
4144
@click.option('--network', default=None, help='Network name (local, test, finney).')
4245
@click.option('--rpc-url', default=None, help='Subtensor RPC endpoint URL (overrides --network).')
46+
@click.option(
47+
'--min-vtrust',
48+
type=float,
49+
default=DEFAULT_MIN_VALIDATOR_VTRUST,
50+
show_default=True,
51+
help='Minimum validator_trust to probe.',
52+
)
53+
@click.option(
54+
'--min-stake',
55+
type=float,
56+
default=DEFAULT_MIN_VALIDATOR_STAKE,
57+
show_default=True,
58+
help='Minimum validator stake (α) to probe.',
59+
)
4360
@click.option('--json-output', 'json_mode', is_flag=True, default=False, help='Output results as JSON.')
44-
def miner_check(wallet_name, wallet_hotkey, netuid, network, rpc_url, json_mode):
61+
def miner_check(wallet_name, wallet_hotkey, netuid, network, rpc_url, min_vtrust, min_stake, json_mode):
4562
"""Check how many validators have your PAT stored.
4663
4764
Sends a lightweight probe to each validator — no PAT is transmitted.
@@ -71,8 +88,10 @@ def miner_check(wallet_name, wallet_hotkey, netuid, network, rpc_url, json_mode)
7188
# Verify miner is registered
7289
_require_registered(wallet, metagraph, netuid, json_mode)
7390

74-
# 3. Find active validator axons (vtrust > 0.1 = actively participating in consensus)
75-
validator_axons, validator_uids = _require_validator_axons(metagraph, json_mode)
91+
# 3. Find active validator axons (vtrust + serving + stake threshold)
92+
validator_axons, validator_uids, excluded = _require_validator_axons(
93+
metagraph, json_mode, min_vtrust=min_vtrust, min_stake=min_stake
94+
)
7695

7796
# 4. Send check probes
7897
synapse = PatCheckSynapse()
@@ -115,6 +134,7 @@ async def _check():
115134
'success': valid_count > 0,
116135
'total_validators': len(results),
117136
**counts,
137+
'skipped': excluded,
118138
'results': results,
119139
},
120140
indent=2,
@@ -134,3 +154,4 @@ async def _check():
134154

135155
console.print(table)
136156
console.print(f'\n[bold]{valid_count}/{len(results)} validators have a valid PAT stored.[/bold]')
157+
_render_skipped_validators(excluded, json_mode)

‎gittensor/cli/miner_commands/helpers.py‎

Lines changed: 88 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,51 @@
1313

1414
import click
1515
from rich.console import Console
16+
from rich.table import Table
1617

1718
from gittensor.constants import NETWORK_MAP
1819

1920
console = Console()
2021

2122
NETUID_DEFAULT = 74
22-
23-
24-
def _get_validator_axons(metagraph) -> tuple[list, list]:
25-
"""Return (axons, uids) for all active validators (vtrust > 0.1, serving)."""
26-
axons = []
27-
uids = []
23+
DEFAULT_MIN_VALIDATOR_VTRUST = 0.25
24+
DEFAULT_MIN_VALIDATOR_STAKE = 15_000.0
25+
26+
27+
def _get_validator_axons(
28+
metagraph,
29+
*,
30+
min_vtrust: float = DEFAULT_MIN_VALIDATOR_VTRUST,
31+
min_stake: float = DEFAULT_MIN_VALIDATOR_STAKE,
32+
) -> tuple[list, list, list[dict]]:
33+
"""Return (axons, uids, excluded) for active validators.
34+
35+
A validator is broadcast to when vtrust > min_vtrust AND axon.is_serving
36+
AND stake >= min_stake. UIDs failing only the latter two checks are
37+
surfaced in `excluded` so miners can see why a high-vtrust validator
38+
was skipped. Sub-vtrust UIDs are dropped silently — they are not
39+
validators.
40+
"""
41+
axons: list = []
42+
uids: list[int] = []
43+
excluded: list[dict] = []
2844
for uid in range(metagraph.n):
29-
if metagraph.validator_trust[uid] > 0.1 and metagraph.axons[uid].is_serving:
30-
axons.append(metagraph.axons[uid])
31-
uids.append(uid)
32-
return axons, uids
45+
vt = float(metagraph.validator_trust[uid])
46+
if vt <= min_vtrust:
47+
continue
48+
serving = bool(metagraph.axons[uid].is_serving)
49+
stake = float(metagraph.S[uid])
50+
reasons: list[str] = []
51+
if not serving:
52+
reasons.append('not serving an axon')
53+
if stake < min_stake:
54+
reasons.append(f'stake {stake:,.0f} α below {min_stake:,.0f} α threshold')
55+
if reasons:
56+
excluded.append({'uid': uid, 'vtrust': vt, 'stake': stake, 'reasons': reasons})
57+
continue
58+
axons.append(metagraph.axons[uid])
59+
uids.append(uid)
60+
return axons, uids, excluded
3361

3462

3563
def _load_config_value(key: str):
@@ -96,13 +124,40 @@ def _require_registered(wallet, metagraph, netuid: int, json_mode: bool) -> None
96124
sys.exit(1)
97125

98126

99-
def _require_validator_axons(metagraph, json_mode: bool) -> tuple[list, list]:
100-
"""Return validator (axons, uids), or exit with error if none found."""
101-
validator_axons, validator_uids = _get_validator_axons(metagraph)
127+
def _require_validator_axons(
128+
metagraph,
129+
json_mode: bool,
130+
*,
131+
min_vtrust: float = DEFAULT_MIN_VALIDATOR_VTRUST,
132+
min_stake: float = DEFAULT_MIN_VALIDATOR_STAKE,
133+
) -> tuple[list, list, list[dict]]:
134+
"""Return validator (axons, uids, excluded), or exit with error if no axons match."""
135+
validator_axons, validator_uids, excluded = _get_validator_axons(
136+
metagraph, min_vtrust=min_vtrust, min_stake=min_stake
137+
)
102138
if not validator_axons:
103139
_error('No reachable validator axons found on the network.', json_mode)
104140
sys.exit(1)
105-
return validator_axons, validator_uids
141+
return validator_axons, validator_uids, excluded
142+
143+
144+
def _render_skipped_validators(excluded: list[dict], json_mode: bool) -> None:
145+
"""Print a 'Skipped Validators' table when any high-vtrust UIDs were filtered."""
146+
if json_mode or not excluded:
147+
return
148+
table = Table(title='Skipped Validators')
149+
table.add_column('UID', style='cyan', justify='right')
150+
table.add_column('vtrust', justify='right')
151+
table.add_column('stake (α)', justify='right')
152+
table.add_column('Reason', style='dim')
153+
for e in excluded:
154+
table.add_row(
155+
str(e['uid']),
156+
f'{e["vtrust"]:.4f}',
157+
f'{e["stake"]:,.0f}',
158+
'; '.join(e['reasons']),
159+
)
160+
console.print(table)
106161

107162

108163
def _pat_check_row_category(row: dict[str, Any]) -> str:
@@ -125,3 +180,22 @@ def _pat_check_aggregate_counts(results: list[dict[str, Any]]) -> dict[str, int]
125180
'invalid_pat': counts['invalid_pat'],
126181
'no_response': counts['no_response'],
127182
}
183+
184+
185+
def _pat_post_row_category(row: dict[str, Any]) -> str:
186+
"""Classify a PAT broadcast row; must match `miner post` table rendering order."""
187+
if row.get('accepted') is True:
188+
return 'accepted'
189+
if row.get('accepted') is False:
190+
return 'rejected'
191+
return 'no_response'
192+
193+
194+
def _pat_post_aggregate_counts(results: list[dict[str, Any]]) -> dict[str, int]:
195+
"""Count PAT broadcast rows by status for JSON summaries."""
196+
counts = Counter(_pat_post_row_category(r) for r in results)
197+
return {
198+
'accepted': counts['accepted'],
199+
'rejected': counts['rejected'],
200+
'no_response': counts['no_response'],
201+
}

0 commit comments

Comments
 (0)