#!/usr/bin/env python3 from __future__ import annotations import argparse import json import os import re from dataclasses import dataclass from datetime import datetime, timezone from pathlib import Path from typing import Iterable from urllib.request import Request, urlopen API_BASE = "https://forge.alexanderwhitestone.com/api/v1" ORG = "Timmy_Foundation" DEFAULT_TOKEN_PATH = os.path.expanduser("~/.config/gitea/token") @dataclass(frozen=True) class PullSummary: number: int title: str state: str merged: bool head: str body: str url: str @dataclass(frozen=True) class IssueAuditRow: number: int title: str state: str classification: str pr_summary: str issue_url: str def to_dict(self) -> dict[str, object]: return { "number": self.number, "title": self.title, "state": self.state, "classification": self.classification, "pr_summary": self.pr_summary, "issue_url": self.issue_url, } def extract_issue_numbers(body: str) -> list[int]: numbers: list[int] = [] seen: set[int] = set() for match in re.finditer(r"#(\d+)(?:-(\d+))?", body or ""): start = int(match.group(1)) end = match.group(2) if end is None: if start not in seen: seen.add(start) numbers.append(start) continue stop = int(end) step = 1 if stop >= start else -1 for value in range(start, stop + step, step): if value not in seen: seen.add(value) numbers.append(value) return numbers def api_get(path: str, token: str): req = Request(API_BASE + path, headers={"Authorization": f"token {token}"}) with urlopen(req, timeout=30) as resp: return json.loads(resp.read().decode()) def collect_pull_summaries(repo: str, token: str) -> list[PullSummary]: pulls: list[PullSummary] = [] for state in ("open", "closed"): page = 1 while True: batch = api_get(f"/repos/{ORG}/{repo}/pulls?state={state}&limit=100&page={page}", token) if not batch: break for pr in batch: pulls.append( PullSummary( number=pr["number"], title=pr.get("title") or "", state=pr.get("state") or state, merged=bool(pr.get("merged")), head=(pr.get("head") or {}).get("ref") or "", body=pr.get("body") or "", url=pr.get("html_url") or pr.get("url") or "", ) ) page += 1 return pulls def match_prs(issue_num: int, pulls: Iterable[PullSummary]) -> list[PullSummary]: matches: list[PullSummary] = [] for pr in pulls: text = f"{pr.title} {pr.head} {pr.body}" if f"#{issue_num}" in text or pr.head == f"fix/{issue_num}" or f"/{issue_num}" in pr.head or f"-{issue_num}" in pr.head: matches.append(pr) return matches def classify_issue(issue: dict, related_prs: list[PullSummary]) -> IssueAuditRow: number = issue["number"] title = issue.get("title") or "" state = issue.get("state") or "unknown" issue_url = issue.get("html_url") or issue.get("url") or "" if state == "closed": classification = "already_closed" pr_summary = summarize_prs(related_prs) or "issue already closed" else: merged = [pr for pr in related_prs if pr.merged] open_prs = [pr for pr in related_prs if pr.state == "open"] closed_unmerged = [pr for pr in related_prs if pr.state != "open" and not pr.merged] if merged: classification = "closure_candidate" pr_summary = summarize_prs(merged) elif open_prs: classification = "active_pr" pr_summary = summarize_prs(open_prs) elif closed_unmerged: classification = "needs_manual_review" pr_summary = summarize_prs(closed_unmerged) else: classification = "needs_manual_review" pr_summary = "no matching PR found" return IssueAuditRow( number=number, title=title, state=state, classification=classification, pr_summary=pr_summary, issue_url=issue_url, ) def summarize_prs(prs: Iterable[PullSummary]) -> str: parts = [] for pr in prs: if pr.merged: parts.append(f"merged PR #{pr.number}") else: parts.append(f"{pr.state} PR #{pr.number}") return ", ".join(parts) def render_report(source_issue: int, source_title: str, referenced_rows: list[dict], generated_at: str) -> str: closure = [row for row in referenced_rows if row["classification"] == "closure_candidate"] active = [row for row in referenced_rows if row["classification"] == "active_pr"] manual = [row for row in referenced_rows if row["classification"] == "needs_manual_review"] closed = [row for row in referenced_rows if row["classification"] == "already_closed"] def table(rows: list[dict]) -> str: if not rows: return "| None |\n|---|\n| None |" lines = ["| Issue | State | Classification | PR Summary |", "|---|---|---|---|"] for row in rows: lines.append( f"| #{row['number']} | {row['state']} | {row['classification'].replace('_', ' ')} | {row['pr_summary']} |" ) return "\n".join(lines) return "\n".join( [ f"# Burn Lane Empty Audit — timmy-home #{source_issue}", "", f"Generated: {generated_at}", f"Source issue: `{source_title}`", "", "## Source Snapshot", "", "Issue #662 is an operational status note, not a normal feature request. Its body is a historical snapshot of one burn lane claiming the queue was exhausted and recommending bulk closure of stale-open items.", "", "## Live Summary", "", f"- Referenced issues audited: {len(referenced_rows)}", f"- Already closed: {len(closed)}", f"- Open but likely closure candidates (merged PR found): {len(closure)}", f"- Open with active PRs: {len(active)}", f"- Open / needs manual review: {len(manual)}", "", "## Issue Body Drift", "", "The body of #662 is not current truth. It mixes closed issues, open issues, ranges, and process notes into one static snapshot. This audit re-queries every referenced issue and classifies it against live forge state instead of trusting the original note.", "", table(referenced_rows), "", "## Closure Candidates", "", "These issues are still open but already have merged PR evidence in the forge and should be reviewed for bulk closure.", "", table(closure), "", "## Still Open / Needs Manual Review", "", "These issues either have no matching PR signal or still have an active PR / ambiguous state and should stay in a human review lane.", "", table(active + manual), "", "## Recommendation", "", "1. Close the `closure_candidate` issues in one deliberate ops pass after a final spot-check on main.", "2. Leave `active_pr` items open until the current PRs are merged or closed.", "3. Investigate `needs_manual_review` items individually — they may be report-only, assigned elsewhere, or still actionable.", "4. Use this audit artifact instead of the raw body text of #662 for future lane-empty claims.", ] ) def run_audit(issue_number: int, repo: str, token: str, output_path: Path) -> Path: issue = api_get(f"/repos/{ORG}/{repo}/issues/{issue_number}", token) referenced = extract_issue_numbers(issue.get("body") or "") pulls = collect_pull_summaries(repo, token) rows: list[dict] = [] for ref in referenced: try: ref_issue = api_get(f"/repos/{ORG}/{repo}/issues/{ref}", token) except Exception: rows.append( IssueAuditRow( number=ref, title="missing or inaccessible", state="unknown", classification="needs_manual_review", pr_summary="issue lookup failed", issue_url="", ).to_dict() ) continue rows.append(classify_issue(ref_issue, match_prs(ref, pulls)).to_dict()) generated_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") report = render_report(issue_number, issue.get("title") or "", rows, generated_at) output_path.parent.mkdir(parents=True, exist_ok=True) output_path.write_text(report + "\n", encoding="utf-8") return output_path def main() -> int: parser = argparse.ArgumentParser(description="Audit a 'burn lane empty' issue body against live forge state.") parser.add_argument("--issue", type=int, default=662) parser.add_argument("--repo", default="timmy-home") parser.add_argument( "--output", default="reports/production/2026-04-16-burn-lane-empty-audit.md", help="Repo-relative output path for the generated markdown report.", ) parser.add_argument("--token-file", default=DEFAULT_TOKEN_PATH) args = parser.parse_args() token = Path(args.token_file).read_text(encoding="utf-8").strip() output = run_audit(args.issue, args.repo, token, Path(args.output)) print(output) return 0 if __name__ == "__main__": raise SystemExit(main())