"""The sample schema and its validation. Standard library only, on purpose. This used to live in `kbench/tasks/signal.py`, which imports `inspect_ai` at module scope — so checking whether a JSONL record was well-formed required the whole eval framework to be installed. That is backwards: the schema is ours and the runner is swappable, so the thing that defines what a sample *is* must not depend on the thing that happens to execute it. Practically it means authoring tools, CI checks and editors can validate data without resolving a heavy dependency tree, and that swapping the execution backend later touches `tasks/`, not this file. """ from __future__ import annotations from pathlib import Path from typing import Any DATA_DIR = Path(__file__).resolve().parent.parent / "data" VALID_SPLITS = {"train", "dev", "test"} VALID_SCORERS = {"exact", "includes", "regex", "rubric"} def family_path(family: str, tier: str = "private") -> Path: return DATA_DIR / tier / f"{family}.jsonl" def validate_record(rec: dict[str, Any], where: str) -> None: """Fail loudly on a malformed sample. A silently-skipped sample shrinks the eval set without changing the score's appearance, which is the worst possible failure mode for a benchmark. """ for field in ("id", "input", "split", "scorer"): if not rec.get(field): raise ValueError(f"{where}: missing required field {field!r}") if rec["split"] not in VALID_SPLITS: raise ValueError(f"{where}: split must be one of {sorted(VALID_SPLITS)}") if rec["scorer"] not in VALID_SCORERS: raise ValueError(f"{where}: scorer must be one of {sorted(VALID_SCORERS)}") if rec["scorer"] != "rubric" and not rec.get("target"): raise ValueError(f"{where}: scorer {rec['scorer']!r} requires a target") if rec["scorer"] == "rubric" and not (rec.get("rubric") or rec.get("target")): raise ValueError(f"{where}: rubric scorer requires a rubric or a target") def read_records(path: Path) -> list[tuple[int, dict[str, Any]]]: """Parse and validate a family file. Returns (line number, record) pairs. Line numbers are carried so an error names the line a human has to open. """ import json out: list[tuple[int, dict[str, Any]]] = [] for lineno, line in enumerate(path.read_text().splitlines(), start=1): line = line.strip() if not line or line.startswith("//"): continue rec = json.loads(line) validate_record(rec, f"{path.name}:{lineno}") out.append((lineno, rec)) return out