diff --git a/gittensor/validator/issue_discovery/mirror_scan.py b/gittensor/validator/issue_discovery/mirror_scan.py index 8fdb7eb80..04fd6c8c7 100644 --- a/gittensor/validator/issue_discovery/mirror_scan.py +++ b/gittensor/validator/issue_discovery/mirror_scan.py @@ -13,6 +13,7 @@ - solved_by_pr must be populated - solving_pr.state == 'MERGED' - not solving_pr.edited_after_merge +- issue.last_edited_at <= solving_pr.merged_at (anti-spec-rewrite) - issue.state_reason == 'COMPLETED' (not NOT_PLANNED, not null) - not issue.is_transferred - issue.author_github_id != solving_pr.author_github_id (anti-self-issue) @@ -465,6 +466,13 @@ def _classify_issue(issue: MirrorIssue) -> str: ) return 'not-solved-closed' + if issue.last_edited_at is not None and sp.merged_at is not None and issue.last_edited_at > sp.merged_at: + bt.logging.debug( + f' issue #{issue.issue_number} ({issue.repo_full_name}): closed-not-solved ' + f'(issue body/title edited after solving PR #{sp.pr_number} merge — anti-spec-rewrite gate)' + ) + return 'not-solved-closed' + if not issue.author_github_id: bt.logging.debug(f' issue #{issue.issue_number} ({issue.repo_full_name}): ignore (missing author_github_id)') return 'ignore' diff --git a/tests/validator/issue_discovery/test_mirror_scan.py b/tests/validator/issue_discovery/test_mirror_scan.py index feba79d94..5d4cee08f 100644 --- a/tests/validator/issue_discovery/test_mirror_scan.py +++ b/tests/validator/issue_discovery/test_mirror_scan.py @@ -109,6 +109,7 @@ def _issue_dict( solving_pr_state: str = 'MERGED', solving_pr_author: str = '218712309', solving_pr_edited_after_merge: bool = False, + last_edited_at: Optional[str] = None, repo: str = 'entrius/gittensor-ui', ) -> dict: sp = None @@ -138,7 +139,7 @@ def _issue_dict( 'created_at': '2026-04-01T00:00:00Z', 'closed_at': '2026-04-18T10:00:00Z' if state == 'CLOSED' else None, 'updated_at': '2026-04-18T10:00:00Z', - 'last_edited_at': None, + 'last_edited_at': last_edited_at, 'is_transferred': is_transferred, 'solved_by_pr': solved_by_pr, 'labels': [], @@ -207,6 +208,23 @@ def test_solving_pr_edited_after_merge_counts_as_closed(self): issue = MirrorIssue.from_dict(_issue_dict(solving_pr_edited_after_merge=True)) assert _classify_issue(issue) == 'not-solved-closed' + def test_issue_edited_after_solving_pr_merge_counts_as_closed(self): + # Anti-spec-rewrite: miner can't author a vague issue, then rewrite the + # body after a third party's PR merges to retroactively claim discovery + # credit for a fix they didn't anticipate. + issue = MirrorIssue.from_dict(_issue_dict(last_edited_at='2026-04-18T10:00:01Z')) + assert _classify_issue(issue) == 'not-solved-closed' + + def test_issue_edited_before_solving_pr_merge_is_solved(self): + # Pre-merge edits are legitimate (sharpening the spec while the PR is + # being written) and must NOT trip the gate. + issue = MirrorIssue.from_dict(_issue_dict(last_edited_at='2026-04-17T10:00:00Z')) + assert _classify_issue(issue) == 'solved' + + def test_issue_never_edited_is_solved(self): + issue = MirrorIssue.from_dict(_issue_dict(last_edited_at=None)) + assert _classify_issue(issue) == 'solved' + def test_missing_author_ignored(self): issue = MirrorIssue.from_dict(_issue_dict(author_github_id=None)) assert _classify_issue(issue) == 'ignore'