core/docs/analysis/r3-2-unit-conversion-2026-06-08.md
Shay 1c59f331f4 feat(rate): exact time-unit conversion for single-rate (R3.2b-e)
Option A (text-faithful): RateProblem gains time_unit (the duration's ORIGINAL unit; time stays int; defaults to the rate denominator). The SOLVER converts via convert_time -> exact Fraction -> int-or-refuse(non_integer); Fraction never leaves the solver, no floats. The reader accepts a convertible (minute<->hour) duration mismatch (keeps original time_unit); a non-convertible one still refuses rate_unit_mismatch. Signature includes time_unit ('30 minutes' != '30 hours').

Gold: r3-09 flips reader_refuses -> SOLVED (60 mph for 30 min = 30 mile, exact 1/2 hour; distractor 1800 rejected); new r3-13 non-convertible (3 gallons) stays rate_unit_mismatch. R3 gold 13/13; reader 9/0/0 -> answers 7/0/6 (setup_correct 8->9). Failure-family: rate_unit_mismatch now non-convertible-only -> still unsupported_rate_duration proposal surface; router-hygiene invariant stays green; R1 7/0/3, R2 10/0/0 unchanged. 119-test smoke green incl. invariants. Off-serving, no float path.
2026-06-08 05:43:51 -07:00

3 KiB
Raw Blame History

R3.2 — explicit time-unit conversion (single-rate)

As of: R3.2, on main + feat/r3-2-conversion.

R3.2 adds exactly one capability to the single-rate organ: a duration whose unit converts to the rate's denominator (minutehour) is now solved instead of refused. It closes the most common rate gap — 60 miles per hour for 30 minutes → 30 miles — which the R3.1 loop surfaced as a proposal.

Lane state

  • R3 gold 13 / 13 valid (7 solved / 2 solver_refuses / 4 reader_refuses)
  • R3 reader 9 setup_correct / 0 setup_wrong / 4 refused → answers 7 / 0 / 6
  • R1 7/0/3 · R2 10/0/0 · serving unchanged · router-hygiene invariant green · off-serving

Design — Option A (text-faithful model, Fraction in the solver)

  • Model (RateProblem) gains time_unit — the duration's original unit from the text; time stays the original int. rate_unit.denominator is the target unit for composition. The setup remembers what the text said ("30 minutes"), not a normalized internal.
  • Conversion (conversion.py): convert_time(value, from, to) -> Fraction — exact rational (fractions.Fraction), identity for the same unit (incl. non-time like box), minute ↔ hour otherwise, refuse else. No floats.
  • Solver confines Fraction: rate × convert(time) (or the inverses), then exact-int-or-refuse (non_integer_solution). A non-convertible duration raises ConversionErrorrate_unit_mismatch.
  • Reader accepts a convertible mismatch (keeps the original time_unit; the solver converts); a non-convertible mismatch still refuses rate_unit_mismatch.
  • Signature includes time_unit — "30 minutes" and "30 hours" are different setups at the same rate.

Gold change

  • r3-09 flips reader_refuses → solved: "60 mph for 30 minutes" → 30 minute = Fraction(1,2) hour → 60 × 1/2 = 30 mile (the never-convert distractor 1800 is rejected).
  • New r3-13 (non-convertible): "…per hour for 3 gallons" → rate_unit_mismatch (gallon is not a time unit). Stays a proposal surface.

Failure-family / hygiene (confirmed, no code change)

  • rate_unit_mismatch now fires only for non-convertible durations → still maps to the unsupported_rate_duration growth surface (propose adding that unit pair). Convertible mismatches now solve (no longer a failure). temporal_state / not_rate_shaped unchanged.
  • The router-organ-hygiene invariant stays green — R3 still refuses foreign R1/R2 text as input_shape, never a substantive boundary.

Acceptance (met)

R3 setup_correct 8 → 9 · answer_wrong = 0 · r3-09 refused/proposal → solved · non-convertible mismatch still refused/proposal · R1/R2 unchanged · no float path (Fraction → int-or-refuse).

Deferred (named for later)

length (mile ↔ km), currency (dollar ↔ cent), compound conversions, combined/multi-rate, clock-time intervals. The dimensional substrate is now proven for exact rational time conversion.