core/generate/combined_rate_comprehension/units.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

41 lines
1.7 KiB
Python

"""Compound-unit value type for the combined-rate organ (CMB-a).
A combined-rate problem carries two rates over ONE shared compound unit (``rooms/hour``,
``liters/minute``): both contributors are measured the same way, so the model holds a single
``RateUnit``. Two rates with *different* units do not compose — that is a reader refusal
(``rate_unit_mismatch``), representable only because there is one unit slot, not two.
Deliberately a **local** copy of the single-rate organ's ``RateUnit`` rather than an import from
``generate.rate_comprehension`` — the two rate organs are kept disjoint (CMB-a does not depend on
R3) until a shared rate algebra is extracted. The duplication is intentional and tiny; if/when the
rate organs converge (a later slice), promote this to a shared ``rate_algebra`` module and have
both import it. No unit conversion in CMB v1 (mirrors R3's v1 boundary). Deterministic.
"""
from __future__ import annotations
from dataclasses import dataclass
class UnitError(ValueError):
"""A malformed or non-composing unit — refuse, never fabricate a unit."""
@dataclass(frozen=True, slots=True)
class RateUnit:
"""A compound rate unit ``numerator / denominator`` — ``room/hour``, ``liter/minute``."""
numerator: str
denominator: str
def __post_init__(self) -> None:
if not self.numerator or not self.denominator:
raise UnitError(f"RateUnit needs non-empty units; got {self!r}")
if self.numerator == self.denominator:
raise UnitError(f"a rate's numerator and denominator must differ; got {self}")
def __str__(self) -> str:
return f"{self.numerator}/{self.denominator}"
__all__ = ["RateUnit", "UnitError"]