fix(core): adaptive_threshold input validation + boundary tests
Risponde alla code review di 7dc2fda:
- Valida percentile in [0,1] e 0 < min_days < target_days, raise
ValueError quando out-of-range. Fail-fast invece di IndexError o
silent wrong result.
- Aggiunge test boundary esattamente a min_days*96 e target_days*96
(spec §9.1 item 9 era mancante).
- Aggiunge 4 test sulle nuove guards.
- Fix typo docstring (Determinismic → Deterministic).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Spec: ``docs/superpowers/specs/2026-05-08-iv-rv-adaptive-gate-design.md``.
|
Spec: ``docs/superpowers/specs/2026-05-08-iv-rv-adaptive-gate-design.md``.
|
||||||
|
|
||||||
Determinismic, no I/O. La query del repository è effettuata dal caller
|
Deterministic, no I/O. La query del repository è effettuata dal caller
|
||||||
(``runtime/entry_cycle``) prima di chiamare questa funzione.
|
(``runtime/entry_cycle``) prima di chiamare questa funzione.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -45,6 +45,15 @@ def compute_adaptive_threshold(
|
|||||||
disabilitato), altrimenti il percentile della finestra,
|
disabilitato), altrimenti il percentile della finestra,
|
||||||
bounded dal floor.
|
bounded dal floor.
|
||||||
"""
|
"""
|
||||||
|
if not (Decimal(0) <= percentile <= Decimal(1)):
|
||||||
|
raise ValueError(
|
||||||
|
f"percentile must be in [0, 1], got {percentile}"
|
||||||
|
)
|
||||||
|
if min_days <= 0 or target_days <= 0 or min_days >= target_days:
|
||||||
|
raise ValueError(
|
||||||
|
f"require 0 < min_days < target_days, "
|
||||||
|
f"got min_days={min_days}, target_days={target_days}"
|
||||||
|
)
|
||||||
if not history:
|
if not history:
|
||||||
return None
|
return None
|
||||||
n_ticks = len(history)
|
n_ticks = len(history)
|
||||||
|
|||||||
@@ -140,3 +140,82 @@ def test_median_percentile_returns_p50() -> None:
|
|||||||
# P50 di [1..200] = (200+1)/2 = 100.5
|
# P50 di [1..200] = (200+1)/2 = 100.5
|
||||||
assert out is not None
|
assert out is not None
|
||||||
assert Decimal("100") <= out <= Decimal("101")
|
assert Decimal("100") <= out <= Decimal("101")
|
||||||
|
|
||||||
|
|
||||||
|
def test_exactly_min_days_uses_min_window() -> None:
|
||||||
|
"""Boundary: history == min_days*96 → window is min_days (per spec 9.1 item 9)."""
|
||||||
|
history = _ramp(30 * 96) # exactly 2880 ticks
|
||||||
|
out = compute_adaptive_threshold(
|
||||||
|
history=history,
|
||||||
|
percentile=Decimal("0.25"),
|
||||||
|
absolute_floor=Decimal("0"),
|
||||||
|
min_days=30,
|
||||||
|
target_days=60,
|
||||||
|
)
|
||||||
|
# Window = last 2880 = all history; P25 of ramp [1..2880] ≈ 720.75
|
||||||
|
assert out is not None
|
||||||
|
assert Decimal("720") <= out <= Decimal("721")
|
||||||
|
|
||||||
|
|
||||||
|
def test_exactly_target_days_uses_target_window() -> None:
|
||||||
|
"""Boundary: history == target_days*96 → window is target_days."""
|
||||||
|
history = _ramp(60 * 96) # exactly 5760 ticks
|
||||||
|
out = compute_adaptive_threshold(
|
||||||
|
history=history,
|
||||||
|
percentile=Decimal("0.25"),
|
||||||
|
absolute_floor=Decimal("0"),
|
||||||
|
min_days=30,
|
||||||
|
target_days=60,
|
||||||
|
)
|
||||||
|
# Window = last 5760 = all history; P25 of ramp [1..5760] ≈ 1440.75
|
||||||
|
assert out is not None
|
||||||
|
assert Decimal("1440") <= out <= Decimal("1441")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Input validation
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_percentile_above_one_raises() -> None:
|
||||||
|
with pytest.raises(ValueError, match="percentile must be in"):
|
||||||
|
compute_adaptive_threshold(
|
||||||
|
history=[Decimal("1")] * 200,
|
||||||
|
percentile=Decimal("1.5"),
|
||||||
|
absolute_floor=Decimal("0"),
|
||||||
|
min_days=30,
|
||||||
|
target_days=60,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_percentile_negative_raises() -> None:
|
||||||
|
with pytest.raises(ValueError, match="percentile must be in"):
|
||||||
|
compute_adaptive_threshold(
|
||||||
|
history=[Decimal("1")] * 200,
|
||||||
|
percentile=Decimal("-0.1"),
|
||||||
|
absolute_floor=Decimal("0"),
|
||||||
|
min_days=30,
|
||||||
|
target_days=60,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_window_inverted_raises() -> None:
|
||||||
|
with pytest.raises(ValueError, match="min_days < target_days"):
|
||||||
|
compute_adaptive_threshold(
|
||||||
|
history=[Decimal("1")] * 200,
|
||||||
|
percentile=Decimal("0.25"),
|
||||||
|
absolute_floor=Decimal("0"),
|
||||||
|
min_days=60,
|
||||||
|
target_days=30,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_window_zero_raises() -> None:
|
||||||
|
with pytest.raises(ValueError, match="min_days < target_days"):
|
||||||
|
compute_adaptive_threshold(
|
||||||
|
history=[Decimal("1")] * 200,
|
||||||
|
percentile=Decimal("0.25"),
|
||||||
|
absolute_floor=Decimal("0"),
|
||||||
|
min_days=0,
|
||||||
|
target_days=60,
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user