"""Named subsets of a dataset's cache (tuning vs held-out). Threshold selection and rule diagnosis have to happen on *some* documents, and metrics reported on those same documents are contaminated: the operating point was fitted to them. Splitting the cache keeps the two roles apart, so a held-out number can be quoted without that caveat. **Membership is by id, never by position.** A manifest file pins the exact ids belonging to a split:: eval/splits/_tuning.json {"ids": ["X00016469670", ...], ...} Everything cached for the dataset that is *not* in the manifest is held out. That definition is stable under any of the things that break a positional "first N" rule: cache files are read in filesystem-glob order, which need not match the order the dataset streamed them in; new documents can be added later; a re-predict can rewrite a file's timestamp. Only the id set matters, so the same documents land in the same split on every machine and every run. The manifest is committed even though ``eval/cache/`` is git-ignored. The cache can be regenerated from the dataset; the record of *which documents were used to tune* cannot be, and losing it would silently turn a held-out claim into an in-sample one. """ from __future__ import annotations import json from pathlib import Path from typing import Any, Literal # Committed alongside the code, unlike the cache itself. DEFAULT_SPLITS_DIR = Path("eval/splits") SplitName = Literal["all", "tuning", "heldout"] SPLIT_NAMES: tuple[str, ...] = ("all", "tuning", "heldout") class SplitError(RuntimeError): """Raised when a named split cannot be resolved for a dataset.""" def manifest_path(dataset: str, *, splits_dir: Path = DEFAULT_SPLITS_DIR) -> Path: """Return the tuning-manifest path for a dataset (not checked for existence).""" return Path(splits_dir) / f"{dataset}_tuning.json" def load_tuning_ids(dataset: str, *, splits_dir: Path = DEFAULT_SPLITS_DIR) -> frozenset[str]: """Load the set of tuning-slice ids for a dataset. Args: dataset: Dataset name. splits_dir: Directory holding split manifests. Returns: The frozen set of example ids in the tuning slice. Raises: SplitError: If the manifest is missing, unreadable, or declares no ids. """ path = manifest_path(dataset, splits_dir=splits_dir) if not path.exists(): raise SplitError( f"No tuning manifest for dataset {dataset!r} at {path}. " "A tuning/held-out split needs one; score the whole cache with " "--split all, or write the manifest first." ) try: payload: dict[str, Any] = json.loads(path.read_text(encoding="utf-8")) except json.JSONDecodeError as exc: raise SplitError(f"Tuning manifest {path} is not valid JSON: {exc}") from exc ids = payload.get("ids") if not isinstance(ids, list) or not ids: raise SplitError(f"Tuning manifest {path} declares no 'ids' list.") return frozenset(str(value) for value in ids) def select( entries: list[dict[str, Any]], split: str, *, dataset: str, splits_dir: Path = DEFAULT_SPLITS_DIR, ) -> list[dict[str, Any]]: """Filter cached entries down to a named split. Selection is set membership on the entry id, so the result depends only on which documents are cached -- not on the order they were read in. Input order is preserved among the surviving entries. Args: entries: Cached entries for the dataset. split: One of ``"all"``, ``"tuning"``, or ``"heldout"``. dataset: Dataset name, used to locate the manifest. splits_dir: Directory holding split manifests. Returns: The subset of ``entries`` belonging to ``split``. Raises: SplitError: If ``split`` is unknown, the manifest is needed but missing, or the split resolves to no documents at all. """ if split not in SPLIT_NAMES: raise SplitError(f"Unknown split {split!r}; expected one of {', '.join(SPLIT_NAMES)}.") if split == "all": return list(entries) tuning_ids = load_tuning_ids(dataset, splits_dir=splits_dir) if split == "tuning": selected = [e for e in entries if str(e.get("id")) in tuning_ids] else: selected = [e for e in entries if str(e.get("id")) not in tuning_ids] if not selected: raise SplitError( f"Split {split!r} selected 0 of {len(entries)} cached entries for " f"dataset {dataset!r}. The cache may not have been expanded beyond " "the tuning slice yet." ) return selected def describe(split: str, n_selected: int, n_total: int) -> str: """Render a one-line description of the active split for a report header. Args: split: The split name. n_selected: Documents in the split. n_total: Documents cached for the dataset overall. Returns: A human-readable summary naming the split and its size. """ if split == "all": note = "WHOLE CACHE -- mixes tuning and held-out documents" elif split == "tuning": note = "TUNING slice -- the operating point was fitted on these, not independent" else: note = "HELD OUT -- never used to select the threshold or diagnose rules" return f"{split} ({n_selected} of {n_total} cached): {note}"