core/evals/rate_oracle/runner.py
Shay 4c271f5e9f feat(rate): R3 RateProblem model + rate gold + setup oracle (R3b)
generate/rate_comprehension/model.py: RateProblem (quantity = rate × time, exactly one unknown == query; rate_unit fixes all three units by construction; structural guard refuses zero/two unknowns). evals/rate_oracle/: span-free signature, a 12-fixture gold (6 solved / 2 solver_refuses / 2... reader_refuses), closed expect+reason taxonomy, gold-validation runner + CLI. No reader yet — proves the ruler (12/12 valid).

Gold shape gives the 6/0/6 target: 6 single-rate solved (forward + both inverses, non-temporal 'per box' included), 2 non-integer-inverse solver refusals, 4 reader refusals (unit mismatch minutes-vs-hour, missing time, combined rates, clock-time temporal). Off-serving. 9 oracle + 10 unit tests; per-branch meaningful-fail.
2026-06-07 23:01:25 -07:00

132 lines
5 KiB
Python

"""R3 rate setup-oracle runner — the ruler before reader capability (R3b).
Validates that the independent rate gold is internally coherent and deserializes into the typed
``RateProblem`` IR: every fixture matches its closed ``expect`` taxonomy, well-formed setups
construct (exactly one unknown == the query), and — for ``solved`` fixtures — the multiple-choice
key agrees with the gold value. No reader yet (lands R3d); this proves the ruler. The R3 twin of
``evals.constraint_oracle.runner``.
Exit 0 iff ``invalid == 0``.
``expect`` taxonomy (closed):
- ``solved`` — full single-rate setup; ``gold`` is the int answer; ``options[answer] == gold``.
- ``solver_refuses`` — full setup, but a non-exact inverse; ``solver_reason`` says why; no gold.
- ``reader_refuses`` — prose the reader must refuse (missing piece / unit mismatch / combined /
temporal); ``reader_reason`` says why; no setup, no gold.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from evals.rate_oracle.signature import rate_setup_signature
from generate.rate_comprehension.model import RateProblem
from generate.rate_comprehension.units import RateUnit, UnitError
_RATE_GOLD_PATH = Path(__file__).resolve().parent / "rate_gold.jsonl"
EXPECTATIONS = frozenset({"solved", "solver_refuses", "reader_refuses"})
SOLVER_REASONS = frozenset({"non_integer_solution"})
#: Closed reader-refusal set the gold uses; extended (with a fixture) as the reader grows.
READER_REASONS = frozenset(
{
"rate_unit_mismatch",
"missing_rate",
"missing_time",
"missing_quantity",
"two_unknowns",
"combined_rates",
"temporal_state",
}
)
def _load_rate_gold() -> list[dict[str, Any]]:
return [
json.loads(line)
for line in _RATE_GOLD_PATH.read_text(encoding="utf-8").splitlines()
if line.strip()
]
def gold_to_problem(fx: dict[str, Any]) -> RateProblem:
"""Deserialize a fixture's setup fields into the typed RateProblem IR."""
ru = fx["rate_unit"]
return RateProblem(
rate_unit=RateUnit(ru["numerator"], ru["denominator"]),
rate=fx.get("rate"),
time=fx.get("time"),
quantity=fx.get("quantity"),
query=fx["query"],
)
def validate_fixture(fx: dict[str, Any]) -> tuple[str, str | None]:
"""Validate one gold fixture's coherence. Returns ``(outcome, reason)``."""
expect = fx.get("expect")
if expect not in EXPECTATIONS:
return "invalid", f"unknown_expect:{expect!r}"
if expect == "reader_refuses":
if fx.get("reader_reason") not in READER_REASONS:
return "invalid", f"unknown_reader_reason:{fx.get('reader_reason')!r}"
if fx.get("gold") is not None:
return "invalid", "reader_refuses_has_gold"
return "valid", None
# solved | solver_refuses require a well-formed single-rate setup.
try:
problem = gold_to_problem(fx)
except (KeyError, TypeError, ValueError, UnitError) as exc:
return "invalid", f"malformed_setup:{exc}"
if rate_setup_signature(problem) != rate_setup_signature(problem):
return "invalid", "nondeterministic_signature" # pragma: no cover - determinism guard
if expect == "solver_refuses":
if fx.get("solver_reason") not in SOLVER_REASONS:
return "invalid", f"unknown_solver_reason:{fx.get('solver_reason')!r}"
if fx.get("gold") is not None:
return "invalid", "solver_refuses_has_gold"
return "valid", None
# solved: integer gold + coherent multiple-choice key.
gold = fx.get("gold")
if not isinstance(gold, int) or isinstance(gold, bool):
return "invalid", "solved_needs_int_gold"
options, answer = fx.get("options"), fx.get("answer")
if not isinstance(options, dict) or answer not in options:
return "invalid", "missing_or_unlabeled_answer"
if options[answer] != gold:
return "invalid", "answer_key_incoherent"
return "valid", None
def run() -> dict[str, Any]:
"""Validate every R3 rate gold fixture. Exit-0 criterion: ``invalid == 0``."""
fixtures = _load_rate_gold()
valid = invalid = 0
by_expect: dict[str, int] = {}
details: list[dict[str, Any]] = []
for fx in fixtures:
outcome, reason = validate_fixture(fx)
expect = fx.get("expect", "?")
by_expect[expect] = by_expect.get(expect, 0) + 1
if outcome == "valid":
valid += 1
details.append({"id": fx.get("id"), "outcome": "valid", "expect": expect})
else:
invalid += 1
details.append({"id": fx.get("id"), "outcome": "invalid", "reason": reason})
return {
"lane": "rate_oracle_gold_validation",
"total": len(fixtures),
"valid": valid,
"invalid": invalid,
"by_expect": by_expect,
"details": details,
}
__all__ = ["EXPECTATIONS", "READER_REASONS", "SOLVER_REASONS", "gold_to_problem", "run", "validate_fixture"]