"""Build data/prs.jsonl from oumi-ai/oumi git history. Mines squash-merge commits (subject ending in "(#NNNN)"), derives a subsystem area label from the files each PR changed, and (best-effort) fetches PR bodies and merge dates via the GitHub GraphQL API using `gh` auth. Usage: git clone --filter=blob:none --single-branch https://github.com/oumi-ai/oumi /tmp/oumi-mine uv run python scripts/build_pr_dataset.py --repo /tmp/oumi-mine [--no-bodies] """ import argparse import json import re import subprocess from collections import Counter from pathlib import Path OUT_PATH = Path(__file__).resolve().parent.parent / "data" / "prs.jsonl" AREAS = [ "training", "inference", "data", "evaluation", "configs", "launcher", "cli", "docs", "infra", "other", ] # Ordered (prefix-or-regex, area) rules; first match wins. Paths not matching # any rule fall through to "other". Tests and barrel files don't vote. IGNORE = re.compile(r"^tests/|(^|/)__init__\.py$") RULES: list[tuple[re.Pattern, str]] = [ (re.compile(r"^docs/|^notebooks/|\.(md|rst|ipynb)$|^CITATION|^LICENSE"), "docs"), (re.compile(r"^\.github/|^scripts/|^pyproject\.toml$|^uv\.lock$|^Makefile$|^Dockerfile|^install\.sh$|^\.pre-commit|^\.(gitignore|dockerignore)|^\.style|^\.readthedocs"), "infra"), (re.compile(r"^configs/|^src/(oumi|lema)/core/configs/"), "configs"), (re.compile(r"^src/(oumi|lema)/(inference/|infer.*\.py$|deploy/)|^src/(oumi|lema)/core/inference/"), "inference"), (re.compile(r"^src/(oumi|lema)/core/(trainers|tuners|callbacks|collators)/|^src/(oumi|lema)/(train|tune)\.py$|^src/(oumi|lema)/performance/|^src/(oumi|lema)/core/distributed\.py$"), "training"), (re.compile(r"^src/(oumi|lema)/(evaluation/|evaluate.*\.py$|judges/|judge\.py$|analyze/)|^src/(oumi|lema)/core/(evaluation|analyze)/"), "evaluation"), (re.compile(r"^src/(oumi|lema)/(datasets/|synth\.py$)|^src/(oumi|lema)/core/(datasets|tokenizers|processors|feature_generators|synthesis|types)/|^data/"), "data"), (re.compile(r"^src/(oumi|lema)/launcher/|^src/(oumi|lema)/core/launcher/"), "launcher"), (re.compile(r"^src/(oumi|lema)/cli/"), "cli"), ] SUBJECT_RE = re.compile(r"^(?P.*)\s+\(#(?P<number>\d+)\)$") DOMINANCE = 0.70 MAX_BODY_CHARS = 4000 def map_file(path: str) -> str | None: if IGNORE.search(path): return None for pattern, area in RULES: if pattern.search(path): return area return "other" def derive_area(files: list[str]) -> tuple[str | None, dict[str, int]]: """Return (area, votes); area is None when no file dominates.""" votes = Counter(a for f in files if (a := map_file(f))) if not votes: return None, {} area, top = votes.most_common(1)[0] if top / sum(votes.values()) < DOMINANCE: return None, dict(votes) return area, dict(votes) def mine_commits(repo: str) -> list[dict]: # Single pass: NUL-prefixed header line per commit, then its changed files. log = subprocess.run( ["git", "-C", repo, "log", "--name-only", "--no-merges", "--format=%x00%s", "main"], capture_output=True, text=True, check=True, ).stdout records = [] for block in log.split("\x00"): if not block.strip(): continue subject, _, file_block = block.partition("\n") m = SUBJECT_RE.match(subject.strip()) if not m: continue files = [ln.strip() for ln in file_block.splitlines() if ln.strip()] area, votes = derive_area(files) if area is None: continue records.append({ "number": int(m["number"]), "title": m["title"].strip(), "files": files, "area": area, "area_votes": votes, }) # Dedupe (reverts etc. can repeat a PR number); keep first (newest) occurrence. seen: set[int] = set() return [r for r in records if not (r["number"] in seen or seen.add(r["number"]))] def fetch_bodies(records: list[dict], batch_size: int = 50) -> None: """Attach `body` and `merged_at` in-place via GitHub GraphQL; skip on failure.""" for i in range(0, len(records), batch_size): batch = records[i:i + batch_size] fields = "".join( f'p{r["number"]}: pullRequest(number: {r["number"]}) {{ body mergedAt }}\n' for r in batch ) query = f'query {{ repository(owner: "oumi-ai", name: "oumi") {{ {fields} }} }}' try: out = subprocess.run( ["gh", "api", "graphql", "-f", f"query={query}"], capture_output=True, text=True, check=True, timeout=120, ).stdout repo_data = json.loads(out)["data"]["repository"] except (subprocess.SubprocessError, KeyError, json.JSONDecodeError) as e: print(f" body fetch failed for batch at {i}: {e}; continuing without") continue for r in batch: pr = repo_data.get(f'p{r["number"]}') or {} r["body"] = (pr.get("body") or "")[:MAX_BODY_CHARS] r["merged_at"] = pr.get("mergedAt") print(f" bodies {i + len(batch)}/{len(records)}") def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--repo", default="/tmp/oumi-mine") parser.add_argument("--no-bodies", action="store_true") args = parser.parse_args() records = mine_commits(args.repo) print(f"mined {len(records)} PRs with unambiguous areas") print("label distribution:", Counter(r["area"] for r in records).most_common()) if not args.no_bodies: fetch_bodies(records) records.sort(key=lambda r: -r["number"]) OUT_PATH.parent.mkdir(exist_ok=True) with OUT_PATH.open("w") as f: for r in records: f.write(json.dumps(r) + "\n") print(f"wrote {len(records)} records to {OUT_PATH}") if __name__ == "__main__": main()