"""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\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()