"""Gates T1-T5 for the slope-stability gate (PREREG_slope_gate_2026-09-21.md). The progress gate refused on the wrong currency (bracket progress) and charged APS 4449 against 2806. This gate refuses on the SHAPE of the model instead: two consecutive slope estimates of opposite sign mean the local model is not established, so the evaluation is spent halving. A SIGN is compared — no threshold, no factor, no constant. """ from __future__ import annotations import inspect import math import re import pytest from simroot import pure as P SMOOTH = [ # simple roots, stable slope sign: the gate must be inert (lambda t: t ** 3 - 2.0 * t - 5.0, 2.0, 3.0), (lambda t: math.exp(t) - 5.0, 0.0, 3.0), (lambda t: t ** 2 - 2.0, 0.5, 2.0), (lambda t: math.cos(t) - t, 0.0, 1.0), ] # the bracket must straddle zero AND contain a sign change of the SLOPE: f' = 3t^2 - 1 # vanishes at t = +-0.577, so the local slope estimate flips sign inside (-2, 2). # (An earlier version used sin(8t)e^-t + 0.05 on (0,1), which does not straddle zero at # all — f(0) = +0.05, f(1) = +0.41 — and the method rightly refused the call.) WOBBLY = (lambda t: t ** 3 - t + 0.2, -2.0, 2.0) def test_T1_the_published_path_is_bit_identical_with_the_gate_off(): for f, a, b in SMOOTH: for guard in ("credit", "project", "safe", "step"): base = P.pure_root(f, a, b, 1e-12, 300, similarity="ratio", scale="derived3", guard=guard) off = P.pure_root(f, a, b, 1e-12, 300, similarity="ratio", scale="derived3", guard=guard, prestep="none") assert base == off, (guard, base, off) def test_T2_the_gate_carries_no_constant(): src = inspect.getsource(P.pure_root) # anchor on the CONDITION, never on the whole line: on 2026-09-21 the slot-2 readings # were inserted ahead of this gate and turned the line into # `if step is None and prestep == "slope":`, which broke a pin that was testing # something else entirely. A pin about constants must not depend on line spelling. start = src.index('prestep == "slope":') end = src.index('prestep == "progress"', start) code = "\n".join(line.split("#", 1)[0] for line in src[start:end].splitlines()) literals = set(re.findall(r"(?" in code and "!=" in code # a SIGN comparison, not a magnitude test def test_T3_the_gate_is_inert_where_the_slope_sign_is_stable(): """This is what the progress gate could not do — it fired on excellent steps.""" for f, a, b in SMOOTH: off = P.pure_root(f, a, b, 1e-12, 300, similarity="ratio", scale="derived3", guard="credit") on = P.pure_root(f, a, b, 1e-12, 300, similarity="ratio", scale="derived3", guard="credit", prestep="slope") assert on[1] == off[1], (off, on) # identical evaluation count def test_T4_the_gate_fires_where_the_slope_sign_flips(): f, a, b = WOBBLY st = {} x, n, ok = P.pure_root(f, a, b, 1e-10, 300, similarity="ratio", scale="derived3", guard="credit", prestep="slope", stats=st) assert ok and abs(f(x)) < 1e-6 assert st["pre_refused"] >= 1, st def test_T5_a_refusal_costs_no_extra_evaluation(): f, a, b = WOBBLY st = {} _x, n, ok = P.pure_root(f, a, b, 1e-10, 300, similarity="ratio", scale="derived3", guard="credit", prestep="slope", stats=st) assert ok assert st["pre_refused"] <= n - 2, (st, n) def test_the_mode_needs_a_slope_to_judge(): with pytest.raises(ValueError): P.pure_root(lambda t: t, -1.0, 1.0, 1e-10, 100, scale="bracket", prestep="slope") with pytest.raises(ValueError): P.pure_root(lambda t: t, -1.0, 1.0, 1e-10, 100, prestep="nonsense") def test_both_gates_remain_available_and_distinct(): """"progress" is kept as a MEASURED NEGATIVE (APS 4449 vs 2806), not deleted.""" f, a, b = WOBBLY a1 = P.pure_root(f, a, b, 1e-10, 300, similarity="ratio", scale="derived3", guard="credit", prestep="progress") a2 = P.pure_root(f, a, b, 1e-10, 300, similarity="ratio", scale="derived3", guard="credit", prestep="slope") assert a1[2] and a2[2] assert a1 != a2 or a1[1] == a2[1] # they may coincide, but both must run