"""Objective diff/message pairing with deterministic, split-local distractors.""" import difflib import json import random from collections import defaultdict from .schema import InvalidRecord, canonical, digest, normalized, question PERMISSIVE = {"mit", "apache-2.0", "bsd-2-clause", "bsd-3-clause", "isc", "cc0-1.0", "unlicense"} def identity(row): return digest([row["commit"], row["old_file"], row["new_file"]]) def partition(group, seed): draw = int(digest([seed, group])[:12], 16) / 16**12 return ( "test" if draw < 0.05 else "calibration" if draw < 0.10 else "validation" if draw < 0.15 else "train" ) def aliases(row): return sorted({r.strip().casefold() for r in row["repos"].split(",") if r.strip()}) def eligible(row): subject = row.get("subject", "").strip() return ( row.get("license", "").casefold() in PERMISSIVE and 25 <= len(subject) <= 180 and len(subject.split()) >= 5 and aliases(row) ) class CommitPool: def __init__(self, path, seed): self.parent, self.pools, self.seed = {}, defaultdict(dict), seed self.references = {} # Fork aliases of any commit must share one split, including transitive aliases. with path.open() as stream: for line in stream: row = json.loads(line) names = aliases(row) if not names: continue for name in names[1:]: a, b = self.find(names[0]), self.find(name) if a != b: small, large = sorted((a, b)) self.parent[large] = small with path.open() as stream: for line in stream: row = json.loads(line) if eligible(row): split, subject = self.split(row), row["subject"].strip() self.pools[split].setdefault( subject, { "source_id": identity(row), "commit": row["commit"], "license": row["license"], "repository_aliases": aliases(row), }, ) self.references = {k: dict(v) for k, v in self.pools.items()} self.pools = {k: sorted(v) for k, v in self.pools.items()} def find(self, name): self.parent.setdefault(name, name) if self.parent[name] != name: self.parent[name] = self.find(self.parent[name]) return self.parent[name] def group(self, row): return self.find(aliases(row)[0]) def split(self, row): return partition(self.group(row), self.seed) def adapt(self, row, source): if not eligible(row): raise InvalidRecord("commitpack_terms_or_message_quality") diff = "".join( difflib.unified_diff( row["old_contents"].splitlines(keepends=True), row["new_contents"].splitlines(keepends=True), fromfile=row["old_file"], tofile=row["new_file"], n=3, ) ) if not diff: raise InvalidRecord("empty_diff") actual = row["subject"].strip() rng = random.Random(identity(row) + str(self.seed)) pool = self.pools[self.split(row)] candidates = [actual] for _ in range(100): candidate = pool[rng.randrange(len(pool))] if candidate in candidates: continue if ( difflib.SequenceMatcher(None, normalized(candidate), normalized(actual)).ratio() > 0.75 ): continue candidates.append(candidate) if len(candidates) == 4: break if len(candidates) != 4: raise InvalidRecord("insufficient_commit_distractors") rng.shuffle(candidates) q, keys = question( "choice", "Which supplied commit message belongs to this code diff? Select the message that best describes the change.", {str(i): v for i, v in enumerate(candidates)}, ) target = [float(v == actual) for v in candidates] meta = { "repo": source["repo"], "revision": source["revision"], "config": source["config"], "source_id": identity(row), "question_id": "message_match", "upstream_split": "train", "upstream_source": "commitpack/python/message_match", "group_key": "commitpack-project:" + self.group(row), "license": row["license"], "license_use": "commercial", "label_origin": "objective_commit_pair", "commit": row["commit"], "old_file": row["old_file"], "new_file": row["new_file"], "repository_aliases": aliases(row), "derived_split": self.split(row), "distractor_policy": "three messages from the same derived split; no near-identical messages", "option_provenance": { str(i): ( { "source_id": identity(row), "commit": row["commit"], "license": row["license"], "repository_aliases": aliases(row), } if message == actual else self.references[self.split(row)][message] ) for i, message in enumerate(candidates) }, } yield canonical("Python code diff:\n" + diff, q, keys, target, meta)