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.
53 lines
1.9 KiB
Python
53 lines
1.9 KiB
Python
"""Typed model for an R3 single-rate problem (R3b).
|
||
|
||
A single-rate problem is the fixed equation ``quantity = rate × time`` with **exactly one**
|
||
unknown (the query). ``rate_unit`` (e.g. ``mile/hour``) is the single source of unit truth: the
|
||
quantity is measured in ``rate_unit.numerator`` and the time in ``rate_unit.denominator``, so the
|
||
three units are consistent by construction. The two non-query slots carry integer values; the
|
||
query slot is ``None``.
|
||
|
||
Pure data with a structural guard: exactly the query slot is unknown (illegal states — zero or two
|
||
unknowns — cannot be represented). Off-serving; deterministic.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass
|
||
from typing import Literal
|
||
|
||
from generate.rate_comprehension.units import RateUnit
|
||
|
||
RateRole = Literal["rate", "time", "quantity"]
|
||
|
||
|
||
@dataclass(frozen=True, slots=True)
|
||
class RateProblem:
|
||
"""``quantity = rate × time`` with one unknown. ``rate_unit`` fixes all three units."""
|
||
|
||
rate_unit: RateUnit
|
||
rate: int | None
|
||
time: int | None
|
||
quantity: int | None
|
||
query: RateRole
|
||
|
||
def __post_init__(self) -> None:
|
||
slots: dict[str, int | None] = {"rate": self.rate, "time": self.time, "quantity": self.quantity}
|
||
unknown = [role for role, value in slots.items() if value is None]
|
||
if unknown != [self.query]:
|
||
raise ValueError(
|
||
f"exactly the query slot must be the unknown; query={self.query!r}, unknown={unknown}"
|
||
)
|
||
for role, value in slots.items():
|
||
if value is not None and (not isinstance(value, int) or isinstance(value, bool)):
|
||
raise ValueError(f"{role} value must be int; got {value!r}")
|
||
|
||
@property
|
||
def quantity_unit(self) -> str:
|
||
return self.rate_unit.numerator
|
||
|
||
@property
|
||
def time_unit(self) -> str:
|
||
return self.rate_unit.denominator
|
||
|
||
|
||
__all__ = ["RateProblem", "RateRole"]
|