diff --git a/src/aelfrice/scoring.py b/src/aelfrice/scoring.py index 0dbc9a942..12719736f 100644 --- a/src/aelfrice/scoring.py +++ b/src/aelfrice/scoring.py @@ -2,9 +2,9 @@ Half-lives (in hours, converted to seconds below): factual 336 (14 days) + requirement 720 (30 days) preference 2016 (12 weeks) correction 4032 (24 weeks) - requirement 4032 (24 weeks) Lock-floor: when a belief's lock_level is "user", decay() is a no-op regardless of age (zero work, sharp step). Above the floor decay is @@ -20,9 +20,9 @@ _HOUR: Final[float] = 3600.0 TYPE_HALF_LIFE_SECONDS: Final[dict[str, float]] = { "factual": 336.0 * _HOUR, # 14 days + "requirement": 720.0 * _HOUR, # 30 days "preference": 2016.0 * _HOUR, # 12 weeks "correction": 4032.0 * _HOUR, # 24 weeks - "requirement": 4032.0 * _HOUR, # 24 weeks } # Jeffreys prior -- decay target. diff --git a/tests/test_type_half_lives.py b/tests/test_type_half_lives.py new file mode 100644 index 000000000..d08308176 --- /dev/null +++ b/tests/test_type_half_lives.py @@ -0,0 +1,53 @@ +"""Type-specific half-lives match the spec exactly. + +Spec (carried from the previous codebase's CHANGELOG and confirmed by R&D): + factual 336h (14 days) + requirement 720h (30 days) + preference 2016h (12 weeks) + correction 4032h (24 weeks) + +If any of these values drifts, decay behavior in production silently changes +and downstream property tests (test_decay_required, test_lock_floor_sharp) +no longer assert what they appear to assert. Lock the values in here. +""" +from __future__ import annotations + +from aelfrice.scoring import TYPE_HALF_LIFE_SECONDS, type_half_life + +_HOUR_SECONDS = 3600.0 + + +def test_factual_half_life_336_hours() -> None: + assert TYPE_HALF_LIFE_SECONDS["factual"] == 336.0 * _HOUR_SECONDS + + +def test_requirement_half_life_720_hours() -> None: + assert TYPE_HALF_LIFE_SECONDS["requirement"] == 720.0 * _HOUR_SECONDS + + +def test_preference_half_life_2016_hours() -> None: + assert TYPE_HALF_LIFE_SECONDS["preference"] == 2016.0 * _HOUR_SECONDS + + +def test_correction_half_life_4032_hours() -> None: + assert TYPE_HALF_LIFE_SECONDS["correction"] == 4032.0 * _HOUR_SECONDS + + +def test_only_four_belief_types_have_half_lives() -> None: + """No drift via accidental extra entries.""" + assert set(TYPE_HALF_LIFE_SECONDS.keys()) == { + "factual", + "requirement", + "preference", + "correction", + } + + +def test_type_half_life_lookup_returns_factual_for_unknown_type() -> None: + """Unknown types fall back to factual (most aggressive decay).""" + assert type_half_life("nonexistent") == TYPE_HALF_LIFE_SECONDS["factual"] + + +def test_type_half_life_lookup_returns_each_known_value() -> None: + for t, hl in TYPE_HALF_LIFE_SECONDS.items(): + assert type_half_life(t) == hl