core/generate/combined_rate_comprehension/model.py
Shay 36bf2a6456 feat(combined-rate): CMB-a — combined-rate setup ruler (model + gold + oracle)
First rung of the combined-rate ladder, off-serving (imports no generate.derivation /
core.reliability_gate). Claims exactly: the combined-rate setup ruler is defined and
gold-valid. No reader/solver/wiring yet — no capability claim.

- generate/combined_rate_comprehension/: local RateUnit (decoupled from R3), CombinedRateProblem
  (two-explicit-rates + per-query-slot guard; effective_rate = rate_a +/- rate_b; non-positive net
  left to the solver, not the model).
- evals/combined_rate_oracle/: span-free signature (sum commutative / difference ordered) + a
  NON-VACUOUS validator. 17 gold fixtures: 6 solved (full combine_mode x query grid), 4
  solver_refuses, 7 reader_refuses (the complete refusal taxonomy + the 2x2 domain-entry grid).

Adversarial 5-lens verification returned fix_first; the validator now cross-checks every solved
gold and solver_refuses reason against the canonical arithmetic (_canonical_outcome) so a
mislabelled / arithmetically-impossible fixture is rejected (meaningful-fail per CLAUDE.md), with
dedicated tests. Added eff<0 and eff=0/time coverage fixtures; removed a dead determinism guard.

gold 17/17 valid; 25 oracle tests; R1/R2/R3 + router-hygiene + serving all unchanged.
Doc: docs/analysis/cmb-a-combined-rate-ruler-2026-06-08.md
2026-06-08 06:58:17 -07:00

105 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Typed model for a combined-rate problem (CMB-a).
A combined-rate problem is two explicit rates over one shared unit, combined by an explicit mode,
then single-rate algebra over the result:
```text
effective_rate = rate_a + rate_b (combine_mode == "sum")
effective_rate = rate_a - rate_b (combine_mode == "difference")
quantity = effective_rate × time (query == "quantity")
time = quantity ÷ effective_rate (query == "time")
effective_rate (query == "effective_rate")
```
The two rates are **always known** (that is what makes it a combined-rate problem — two explicit
rates); ``rate_unit`` is the single source of unit truth for both. The query selects which derived
slot is asked:
- ``quantity`` — ``time`` is the known, ``quantity`` is the unknown (``None``).
- ``time`` — ``quantity`` is the known, ``time`` is the unknown (``None``).
- ``effective_rate`` — neither ``time`` nor ``quantity`` is needed; both are ``None``.
Pure data with a structural guard: the two rates must be present, and exactly the slots the query
licenses are known/unknown (illegal states — a missing rate, the wrong slot unknown, an
over-specified ``effective_rate`` query — cannot be represented).
``effective_rate`` is derived (a property), and for ``difference`` mode it MAY be ``<= 0``
(``rate_a <= rate_b``). The model does NOT refuse that — a non-positive net rate is the *solver's*
boundary (``non_positive_net_rate``, CMB-b), not a malformed setup. Off-serving; deterministic.
No unit conversion in v1 (``time_unit`` defaults to the rate denominator and the v1 gold never
crosses units).
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
from generate.combined_rate_comprehension.units import RateUnit
CombineMode = Literal["sum", "difference"]
CombinedRateQuery = Literal["quantity", "time", "effective_rate"]
#: The slots each query licenses as (known, unknown). ``effective_rate`` needs neither time nor
#: quantity; the other two trade exactly one known for one unknown.
_QUERY_SLOTS: dict[str, tuple[frozenset[str], frozenset[str]]] = {
"quantity": (frozenset({"time"}), frozenset({"quantity"})),
"time": (frozenset({"quantity"}), frozenset({"time"})),
"effective_rate": (frozenset(), frozenset({"time", "quantity"})),
}
@dataclass(frozen=True, slots=True)
class CombinedRateProblem:
"""Two explicit rates over one unit, combined by ``combine_mode``, with one queried slot."""
rate_a: int
rate_b: int
rate_unit: RateUnit
combine_mode: CombineMode
time: int | None
quantity: int | None
query: CombinedRateQuery
#: The duration's time unit (forward-compat with conversion). Defaults to the rate denominator;
#: v1 never crosses units, so it always equals ``rate_unit.denominator`` here.
time_unit: str | None = None
def __post_init__(self) -> None:
if self.time_unit is None:
object.__setattr__(self, "time_unit", self.rate_unit.denominator)
for role, value in (("rate_a", self.rate_a), ("rate_b", self.rate_b)):
if not isinstance(value, int) or isinstance(value, bool):
raise ValueError(f"{role} must be a known int (two explicit rates); got {value!r}")
if self.combine_mode not in ("sum", "difference"):
raise ValueError(f"combine_mode must be 'sum' or 'difference'; got {self.combine_mode!r}")
if self.query not in _QUERY_SLOTS:
raise ValueError(f"query must be one of {sorted(_QUERY_SLOTS)}; got {self.query!r}")
known_slots, unknown_slots = _QUERY_SLOTS[self.query]
slots: dict[str, int | None] = {"time": self.time, "quantity": self.quantity}
known = {role for role, value in slots.items() if value is not None}
if known != known_slots:
raise ValueError(
f"query={self.query!r} licenses knowns {sorted(known_slots)}; got {sorted(known)}"
)
for role in unknown_slots:
if slots[role] is not None:
raise ValueError(f"query={self.query!r}: slot {role!r} must be the unknown (None)")
for role in known_slots:
value = slots[role]
if not isinstance(value, int) or isinstance(value, bool):
raise ValueError(f"{role} value must be int; got {value!r}")
@property
def effective_rate(self) -> int:
"""The combined rate (pure derivation). MAY be ``<= 0`` for ``difference``; the solver,
not the model, owns the ``non_positive_net_rate`` refusal."""
if self.combine_mode == "sum":
return self.rate_a + self.rate_b
return self.rate_a - self.rate_b
@property
def quantity_unit(self) -> str:
return self.rate_unit.numerator
__all__ = ["CombineMode", "CombinedRateProblem", "CombinedRateQuery"]