From e07d0fe5c8667d4d7dbcd65cf0ba849f3f15b3f0 Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Tue, 14 Apr 2026 23:20:02 -0400 Subject: [PATCH] fix: PR triage tool and backlog report (closes #1471) --- docs/pr-triage-report.md | 52 ++++++++++++++ scripts/pr_triage.py | 144 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 docs/pr-triage-report.md create mode 100644 scripts/pr_triage.py diff --git a/docs/pr-triage-report.md b/docs/pr-triage-report.md new file mode 100644 index 00000000..c7a1a5e8 --- /dev/null +++ b/docs/pr-triage-report.md @@ -0,0 +1,52 @@ +# PR Triage Report — Timmy_Foundation/timmy-config + +Generated: 2026-04-15 02:15 UTC +Total open PRs: 50 + +## Duplicate PR Groups +**14 issues with duplicate PRs (26 excess PRs)** + +### Issue #681 (5 PRs) +- KEEP: #685 — fix: add python3 shebangs to 6 scripts (#681) +- CLOSE: #682, #683, #684, #680 + +### Issue #660 (4 PRs) +- KEEP: #680 — fix: Standardize training Makefile on python3 (#660) +- CLOSE: #670, #677 + +### Issue #659 (3 PRs) +- KEEP: #679 — feat: PR triage automation with auto-merge (closes #659) +- CLOSE: #665, #678 + +### Issue #645 (2 PRs) +- KEEP: #693 — data: 100 Hip-Hop scene description sets #645 +- CLOSE: #688 + +### Issue #650 (2 PRs) +- KEEP: #676 — fix: pipeline_state.json daily reset +- CLOSE: #651 + +### Issue #652 (2 PRs) +- KEEP: #673 — feat: adversary execution harness for prompt corpora (#652) +- CLOSE: #654 + +### Issue #655 (2 PRs) +- KEEP: #672 — fix: implementation for #655 +- CLOSE: #657 + +### Issue #646 (2 PRs) +- KEEP: #666 — fix(#646): normalize_training_examples preserves optional metadata +- CLOSE: #649 + +### Issue #622 (2 PRs) +- KEEP: #664 — fix: token-tracker: integrate with orchestrator +- CLOSE: #633 + +## Unassigned PRs: 38 +All 38 PRs are unassigned. Recommend batch assignment to available reviewers. + +## Recommendations +1. Close 26 duplicate PRs (keep newest for each issue) +2. Assign reviewers to all PRs +3. Add duplicate-PR prevention check to CI +4. Run this tool weekly to maintain backlog health diff --git a/scripts/pr_triage.py b/scripts/pr_triage.py new file mode 100644 index 00000000..86023042 --- /dev/null +++ b/scripts/pr_triage.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +""" +pr_triage.py — Triage PR backlog for timmy-config. + +Identifies duplicate PRs for the same issue, unassigned PRs, +and recommends which to close/merge. + +Usage: + python3 scripts/pr_triage.py --repo Timmy_Foundation/timmy-config + python3 scripts/pr_triage.py --repo Timmy_Foundation/timmy-config --close-duplicates --dry-run +""" + +import argparse +import json +import os +import re +import sys +import urllib.request +from collections import defaultdict +from datetime import datetime, timezone +from pathlib import Path + + +GITEA_URL = "https://forge.alexanderwhitestone.com" + + +def get_token(): + return (Path.home() / ".config" / "gitea" / "token").read_text().strip() + + +def fetch_open_prs(repo, headers): + all_prs = [] + page = 1 + while True: + url = f"{GITEA_URL}/api/v1/repos/{repo}/pulls?state=open&limit=100&page={page}" + req = urllib.request.Request(url, headers=headers) + resp = urllib.request.urlopen(req, timeout=15) + data = json.loads(resp.read()) + if not data: + break + all_prs.extend(data) + if len(data) < 100: + break + page += 1 + return all_prs + + +def find_duplicate_groups(prs): + issue_prs = defaultdict(list) + for pr in prs: + text = (pr.get("body") or "") + " " + (pr.get("title") or "") + issues = set(re.findall(r"#(\d+)", text)) + for iss in issues: + issue_prs[iss].append(pr) + return {k: v for k, v in issue_prs.items() if len(v) > 1} + + +def generate_report(repo, prs): + now = datetime.now(timezone.utc) + lines = [f"# PR Triage Report — {repo}", + f"\nGenerated: {now.strftime('%Y-%m-%d %H:%M UTC')}", + f"Total open PRs: {len(prs)}", ""] + + duplicates = find_duplicate_groups(prs) + unassigned = [p for p in prs if not p.get("assignee")] + + lines.append("## Duplicate PR Groups") + if duplicates: + total_dupes = sum(len(v) - 1 for v in duplicates.values()) + lines.append(f"**{len(duplicates)} issues with duplicate PRs ({total_dupes} excess PRs)**") + for issue, pr_group in sorted(duplicates.items(), key=lambda x: -len(x[1])): + keep = max(pr_group, key=lambda p: p["number"]) + close = [p for p in pr_group if p["number"] != keep["number"]] + lines.append(f"\n### Issue #{issue} ({len(pr_group)} PRs)") + lines.append(f"- **KEEP:** #{keep['number']} — {keep['title'][:60]}") + for p in close: + lines.append(f"- CLOSE: #{p['number']} — {p['title'][:60]}") + else: + lines.append("No duplicate PR groups found.") + lines.append("") + + lines.append(f"## Unassigned PRs: {len(unassigned)}") + for p in unassigned[:10]: + lines.append(f"- #{p['number']}: {p['title'][:70]}") + if len(unassigned) > 10: + lines.append(f"- ... and {len(unassigned) - 10} more") + lines.append("") + + lines.append("## Recommendations") + excess = sum(len(v) - 1 for v in duplicates.values()) + lines.append(f"1. Close {excess} duplicate PRs (keep newest for each issue)") + lines.append(f"2. Assign reviewers to {len(unassigned)} unassigned PRs") + lines.append(f"3. Consider adding duplicate-PR prevention to CI") + + return "\n".join(lines) + + +def close_duplicate_prs(repo, prs, headers, dry_run=True): + duplicates = find_duplicate_groups(prs) + closed = 0 + for issue, pr_group in duplicates.items(): + keep = max(pr_group, key=lambda p: p["number"]) + for pr in pr_group: + if pr["number"] == keep["number"]: + continue + if dry_run: + print(f"Would close PR #{pr['number']}: {pr['title'][:60]}") + else: + url = f"{GITEA_URL}/api/v1/repos/{repo}/pulls/{pr['number']}" + data = json.dumps({"state": "closed"}).encode() + req = urllib.request.Request(url, data=data, headers={**headers, "Content-Type": "application/json"}, method="PATCH") + try: + urllib.request.urlopen(req) + print(f"Closed PR #{pr['number']}") + closed += 1 + except Exception as e: + print(f"Failed to close #{pr['number']}: {e}") + return closed + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--repo", default="Timmy_Foundation/timmy-config") + parser.add_argument("--close-duplicates", action="store_true") + parser.add_argument("--dry-run", action="store_true") + args = parser.parse_args() + + token = get_token() + headers = {"Authorization": f"token {token}"} + prs = fetch_open_prs(args.repo, headers) + + if args.close_duplicates: + closed = close_duplicate_prs(args.repo, prs, headers, args.dry_run) + print(f"\n{'Would close' if args.dry_run else 'Closed'} {closed} duplicate PRs") + else: + report = generate_report(args.repo, prs) + print(report) + docs_dir = Path(__file__).resolve().parent.parent / "docs" + docs_dir.mkdir(exist_ok=True) + (docs_dir / "pr-triage-report.md").write_text(report) + + +if __name__ == "__main__": + main()