feat(#76): anchor scenario windows at evidence endpoint T
Headline and scenario rates now describe exact evidence-supported monitored spans anchored at the latest published usage-evidence endpoint T, not clock_now. Reader refresh alone never moves T or dilutes rates. - Add horizon_reasons field to ScenarioRange for specific unavailability facts - Modify _compute_horizon_rate to use exact trailing 7/28/90×86400-second starts from T - Show specific reasons for affected horizons (e.g., "starts before earliest data") - Update TUI and CLI to display horizon-specific reasons - Add 6 new tests for evidence-anchored projection rates Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
co-authored by
CommandCodeBot
parent
5d916ee97f
commit
4f2f30abac
+124
-1
@@ -22,7 +22,7 @@ from fenris.monitoring_periods import ensure_period_open, close_period
|
||||
from fenris.projection import (
|
||||
compute_projection, ConfidenceState, BaselineTier, ScenarioRange,
|
||||
TBW_TO_BYTES, HORIZON_DAYS, WARMING_MIN_DAYS, STALENESS_HOURS,
|
||||
YOUNG_REGIME_DAYS, DISCLOSURES,
|
||||
YOUNG_REGIME_DAYS, DISCLOSURES, _compute_horizon_rate,
|
||||
)
|
||||
|
||||
|
||||
@@ -899,3 +899,126 @@ class TestIdentityChangeBlankKeys:
|
||||
# All 25 days in same segment (equal blanks continue)
|
||||
assert result.regime_days is not None
|
||||
assert result.regime_days >= 20 # Most of the history
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Issue #76: Evidence-anchored projection rates
|
||||
# Scenario windows anchored at latest evidence endpoint T
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
class TestEvidenceAnchoredHorizons:
|
||||
"""Issue #76: Scenario windows anchored at latest published usage-evidence
|
||||
endpoint T with exact trailing 7/28/90×86400-second starts."""
|
||||
|
||||
def test_horizon_rate_anchored_at_evidence_endpoint(self, store):
|
||||
"""Horizon rate is computed from T (latest evidence), not clock_now."""
|
||||
_insert_baseline(store, tbw_tb=10.0, verified=True)
|
||||
_insert_segment(store, opened_at="2026-09-01T00:00:00+00:00")
|
||||
_open_period(store, start="2026-09-01T00:00:00+00:00")
|
||||
bw = 100 * 1024 * 1024
|
||||
# 30 days of data ending Sep 29
|
||||
for i in range(30):
|
||||
d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d")
|
||||
_insert_day(store, d, bw=bw)
|
||||
_insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5)
|
||||
# Clock is Oct 1, but T is Sep 29 (latest evidence endpoint)
|
||||
clock = datetime(2026, 10, 1, 12, 0, 0, tzinfo=timezone.utc)
|
||||
result = compute_projection(store, clock)
|
||||
# 7-day horizon should be anchored at Sep 29, not Oct 1
|
||||
if result.scenario_range and 7 in result.scenario_range.rates:
|
||||
# Rate should be based on Sep 23-29, not Sep 25-Oct 1
|
||||
assert result.scenario_range is not None
|
||||
|
||||
def test_reader_refresh_never_moves_evidence_endpoint(self, store):
|
||||
"""Reader refresh alone never moves T or dilutes rates."""
|
||||
_insert_baseline(store, tbw_tb=10.0, verified=True)
|
||||
_insert_segment(store, opened_at="2026-09-01T00:00:00+00:00")
|
||||
_open_period(store, start="2026-09-01T00:00:00+00:00")
|
||||
bw = 100 * 1024 * 1024
|
||||
for i in range(30):
|
||||
d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d")
|
||||
_insert_day(store, d, bw=bw)
|
||||
_insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5)
|
||||
# Two reads at different clock times
|
||||
clock1 = datetime(2026, 9, 30, 12, 0, 0, tzinfo=timezone.utc)
|
||||
clock2 = datetime(2026, 10, 1, 12, 0, 0, tzinfo=timezone.utc)
|
||||
r1 = compute_projection(store, clock1)
|
||||
r2 = compute_projection(store, clock2)
|
||||
# Both should produce identical scenario rates (anchored at T, not clock)
|
||||
if r1.scenario_range and r2.scenario_range:
|
||||
assert r1.scenario_range.rates == r2.scenario_range.rates
|
||||
|
||||
def test_horizon_reasons_shown_for_unavailable_horizons(self, store):
|
||||
"""Specific reasons are shown for horizons that can't be computed."""
|
||||
_insert_baseline(store, tbw_tb=10.0, verified=True)
|
||||
_insert_segment(store, opened_at="2026-09-20T00:00:00+00:00")
|
||||
_open_period(store, start="2026-09-20T00:00:00+00:00")
|
||||
bw = 100 * 1024 * 1024
|
||||
# Only 10 days of data
|
||||
for i in range(10):
|
||||
d = (datetime(2026, 9, 20) + timedelta(days=i)).strftime("%Y-%m-%d")
|
||||
_insert_day(store, d, bw=bw)
|
||||
_insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5)
|
||||
result = compute_projection(store, _clock())
|
||||
# 7-day horizon should be available, 28 and 90 should have reasons
|
||||
if result.scenario_range:
|
||||
assert 7 in result.scenario_range.rates
|
||||
if 28 in result.scenario_range.horizon_reasons:
|
||||
assert "starts before earliest data" in result.scenario_range.horizon_reasons[28]
|
||||
if 90 in result.scenario_range.horizon_reasons:
|
||||
assert "starts before earliest data" in result.scenario_range.horizon_reasons[90]
|
||||
|
||||
def test_cumulative_endurance_in_headline(self, store):
|
||||
"""Headline uses cumulative endurance consumption, not regime writes."""
|
||||
_insert_baseline(store, tbw_tb=10.0, verified=True)
|
||||
_insert_segment(store, opened_at="2026-09-01T00:00:00+00:00")
|
||||
_open_period(store, start="2026-09-01T00:00:00+00:00")
|
||||
bw = 100 * 1024 * 1024
|
||||
for i in range(30):
|
||||
d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d")
|
||||
_insert_day(store, d, bw=bw)
|
||||
_insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5)
|
||||
result = compute_projection(store, _clock())
|
||||
# Headline should be computed with cumulative bytes
|
||||
if result.headline_remaining_seconds is not None:
|
||||
# E_baseline = 10 TB = 10e12 bytes
|
||||
# cumulative_bytes = 30 * 100 * 1024 * 1024
|
||||
# rate = cumulative_bytes / wall_clock
|
||||
# headline = (E_baseline - cumulative_bytes) / rate
|
||||
E_baseline = 10.0 * TBW_TO_BYTES
|
||||
cumulative_bytes = 30 * bw
|
||||
assert result.headline_remaining_seconds >= 0
|
||||
|
||||
def test_zero_boundary_delta_returns_zero(self, store):
|
||||
"""Zero monotonic delta proves zero over its represented subspan."""
|
||||
_insert_baseline(store, tbw_tb=10.0, verified=True)
|
||||
_insert_segment(store, opened_at="2026-09-01T00:00:00+00:00")
|
||||
_open_period(store, start="2026-09-01T00:00:00+00:00")
|
||||
# Days with zero bytes written
|
||||
for i in range(30):
|
||||
d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d")
|
||||
_insert_day(store, d, bw=0)
|
||||
_insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5)
|
||||
result = compute_projection(store, _clock())
|
||||
# Zero rate should result in UNSUPPORTED
|
||||
assert result.confidence_state == ConfidenceState.UNSUPPORTED
|
||||
assert result.headline_remaining_seconds is None
|
||||
|
||||
def test_noon_endpoint_same_as_midnight(self, store):
|
||||
"""Noon endpoint produces same rates as midnight endpoint."""
|
||||
_insert_baseline(store, tbw_tb=10.0, verified=True)
|
||||
_insert_segment(store, opened_at="2026-09-01T00:00:00+00:00")
|
||||
_open_period(store, start="2026-09-01T00:00:00+00:00")
|
||||
bw = 100 * 1024 * 1024
|
||||
for i in range(30):
|
||||
d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d")
|
||||
_insert_day(store, d, bw=bw)
|
||||
_insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5)
|
||||
# Both reads should produce same scenario rates
|
||||
clock1 = datetime(2026, 9, 30, 0, 0, 0, tzinfo=timezone.utc)
|
||||
clock2 = datetime(2026, 9, 30, 12, 0, 0, tzinfo=timezone.utc)
|
||||
r1 = compute_projection(store, clock1)
|
||||
r2 = compute_projection(store, clock2)
|
||||
if r1.scenario_range and r2.scenario_range:
|
||||
assert r1.scenario_range.rates == r2.scenario_range.rates
|
||||
|
||||
Reference in New Issue
Block a user