fix(projection): align projection with ADR 0002
Use lifetime written bytes from the current segment's newest published sample in the headline formula, select the Percentage-Used-implied baseline after two increments in the segment (otherwise show the too-coarse fact), and judge horizon agreement across existing horizons. Make the vacuous projection tests assert unconditionally and add coverage for implied-baseline gating and single-horizon agreement.
This commit is contained in:
+110
-22
@@ -74,12 +74,13 @@ def _insert_day(conn, day, bw=1024*1024*100, coverage=0.95, samples=24):
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _insert_sample(conn, ts, pu=5):
|
||||
def _insert_sample(conn, ts, pu=5, bytes_written=512000000000, segment_id=1):
|
||||
conn.execute(
|
||||
"INSERT INTO samples (ts, device, data_units_written, data_units_read, "
|
||||
"percentage_used, bytes_written, bytes_read, power_on_hours) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(ts, "/dev/nvme0n1", 1000000, 500000, pu, 512000000000, 256000000000, 8765),
|
||||
"percentage_used, bytes_written, bytes_read, power_on_hours, segment_id) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(ts, "/dev/nvme0n1", bytes_written // 512000, 500000, pu, bytes_written,
|
||||
256000000000, 8765, segment_id),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
@@ -223,6 +224,88 @@ class TestImpliedBaseline:
|
||||
result = compute_projection(store, _clock())
|
||||
assert result.baseline_tier == BaselineTier.VERIFIED
|
||||
|
||||
def _setup_implied(self, store, pus):
|
||||
"""No override baseline; one sample per PU value inside the segment."""
|
||||
_insert_segment(store)
|
||||
_open_period(store)
|
||||
for i in range(20):
|
||||
d = (datetime(2026, 9, 10) + timedelta(days=i)).strftime("%Y-%m-%d")
|
||||
_insert_day(store, d, bw=1024*1024*100)
|
||||
for i, pu in enumerate(pus):
|
||||
_insert_sample(store, "2026-09-%02dT10:00:00+00:00" % (20 + i), pu=pu)
|
||||
_insert_complete_local_days(store, "2026-09-29", 1)
|
||||
|
||||
@pytest.mark.parametrize("pus", [[5], [5, 5, 5], [5, 6]])
|
||||
def test_implied_unavailable_before_two_increments(self, store, pus):
|
||||
self._setup_implied(store, pus)
|
||||
result = compute_projection(store, _clock())
|
||||
assert result.confidence_state == ConfidenceState.UNSUPPORTED
|
||||
assert result.baseline_tier == BaselineTier.NONE
|
||||
assert result.headline_remaining_seconds is None
|
||||
assert "vendor wear estimate too coarse to imply endurance" in result.contributing_facts
|
||||
|
||||
def test_implied_unavailable_ignores_increments_in_other_segments(self, store):
|
||||
_insert_segment(store, opened_at="2026-09-01T00:00:00+00:00")
|
||||
_insert_segment(store, opened_at="2026-09-15T00:00:00+00:00", identity_key="nqn.new")
|
||||
_open_period(store)
|
||||
for i in range(20):
|
||||
d = (datetime(2026, 9, 10) + timedelta(days=i)).strftime("%Y-%m-%d")
|
||||
_insert_day(store, d, bw=1024*1024*100)
|
||||
# Two increments in the old segment, none in the current one
|
||||
_insert_sample(store, "2026-09-02T10:00:00+00:00", pu=3, segment_id=1)
|
||||
_insert_sample(store, "2026-09-03T10:00:00+00:00", pu=4, segment_id=1)
|
||||
_insert_sample(store, "2026-09-20T10:00:00+00:00", pu=5, segment_id=2)
|
||||
_insert_complete_local_days(store, "2026-09-29", 1)
|
||||
result = compute_projection(store, _clock())
|
||||
assert result.baseline_tier == BaselineTier.NONE
|
||||
assert "vendor wear estimate too coarse to imply endurance" in result.contributing_facts
|
||||
|
||||
def test_implied_unavailable_when_pu_out_of_range(self, store):
|
||||
self._setup_implied(store, [0, 1, 2])
|
||||
store.execute("UPDATE samples SET percentage_used = 255 WHERE id = 3")
|
||||
store.commit()
|
||||
result = compute_projection(store, _clock())
|
||||
assert result.baseline_tier == BaselineTier.NONE
|
||||
assert "vendor wear estimate too coarse to imply endurance" in result.contributing_facts
|
||||
|
||||
def test_implied_eligible_after_two_increments(self, store):
|
||||
self._setup_implied(store, [5, 6, 7])
|
||||
W_t = 512000000000
|
||||
result = compute_projection(store, _clock())
|
||||
assert result.baseline_tier == BaselineTier.IMPLIED
|
||||
# Implied baseline is never Supported-eligible
|
||||
assert result.confidence_state == ConfidenceState.LIMITED
|
||||
assert "vendor wear estimate too coarse to imply endurance" not in result.contributing_facts
|
||||
E_implied = 100 * W_t / 7
|
||||
regime_bytes = 20 * 1024 * 1024 * 100
|
||||
regime_start = datetime(2026, 9, 10, 0, 0, 0, tzinfo=timezone.utc)
|
||||
rate = regime_bytes / int((_clock() - regime_start).total_seconds())
|
||||
assert abs(result.headline_remaining_seconds - max(E_implied - W_t, 0) / rate) < 1.0
|
||||
|
||||
def test_override_wins_over_implied(self, store):
|
||||
_insert_baseline(store, tbw_tb=1.0, verified=True)
|
||||
self._setup_implied(store, [5, 6, 7])
|
||||
result = compute_projection(store, _clock())
|
||||
assert result.baseline_tier == BaselineTier.VERIFIED
|
||||
|
||||
|
||||
class TestHorizonAgreement:
|
||||
def test_single_horizon_passes_agreement(self, store):
|
||||
"""ADR 0002 §8: agreement is judged across existing horizons; one passes."""
|
||||
_insert_baseline(store, tbw_tb=1.0, verified=True)
|
||||
_insert_segment(store)
|
||||
_open_period(store)
|
||||
# 14 days covers only the 7-day horizon
|
||||
for i in range(14):
|
||||
d = (datetime(2026, 9, 17) + timedelta(days=i)).strftime("%Y-%m-%d")
|
||||
_insert_day(store, d, bw=1024*1024*100)
|
||||
_insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5)
|
||||
_insert_complete_local_days(store, "2026-09-29", 1)
|
||||
result = compute_projection(store, _clock())
|
||||
assert list(result.scenario_range.rates) == [7]
|
||||
assert not any("regime only" in f for f in result.contributing_facts)
|
||||
assert result.confidence_state == ConfidenceState.SUPPORTED
|
||||
|
||||
|
||||
class TestZeroRate:
|
||||
def test_zero_rate_fixed_phrase(self, store):
|
||||
@@ -339,17 +422,19 @@ class TestArithmetic:
|
||||
for i in range(30):
|
||||
d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d")
|
||||
_insert_day(store, d, bw=1024*1024*100)
|
||||
_insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5)
|
||||
W_t = 512000000000 # lifetime written bytes of the newest sample
|
||||
_insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5, bytes_written=W_t)
|
||||
_insert_complete_local_days(store, "2026-09-29", 1)
|
||||
result = compute_projection(store, _clock())
|
||||
if result.headline_remaining_seconds is not None:
|
||||
E_rated = 2.0 * TBW_TO_BYTES
|
||||
regime_bytes = 30 * 1024 * 1024 * 100
|
||||
period_start = datetime(2026, 9, 1, 0, 0, 0, tzinfo=timezone.utc)
|
||||
period_end = _clock()
|
||||
actual_wc = int((period_end - period_start).total_seconds())
|
||||
rate = regime_bytes / actual_wc
|
||||
expected = max(E_rated - regime_bytes, 0) / rate
|
||||
assert abs(result.headline_remaining_seconds - expected) < 1.0
|
||||
assert result.headline_remaining_seconds is not None
|
||||
E_rated = 2.0 * TBW_TO_BYTES
|
||||
regime_bytes = 30 * 1024 * 1024 * 100
|
||||
period_start = datetime(2026, 9, 1, 0, 0, 0, tzinfo=timezone.utc)
|
||||
actual_wc = int((_clock() - period_start).total_seconds())
|
||||
rate = regime_bytes / actual_wc
|
||||
# W_t is lifetime written bytes, not the regime delta
|
||||
expected = max(E_rated - W_t, 0) / rate
|
||||
assert abs(result.headline_remaining_seconds - expected) < 1.0
|
||||
|
||||
def test_wearing_rate_proportional(self, store):
|
||||
_insert_baseline(store, tbw_tb=1.0, verified=True)
|
||||
@@ -359,6 +444,7 @@ class TestArithmetic:
|
||||
d = (datetime(2026, 9, 1) + timedelta(days=i)).strftime("%Y-%m-%d")
|
||||
_insert_day(store, d, bw=1024*1024*100)
|
||||
_insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5)
|
||||
_insert_complete_local_days(store, "2026-09-29", 1)
|
||||
r_slow = compute_projection(store, _clock())
|
||||
|
||||
store.execute("DELETE FROM day_aggregates")
|
||||
@@ -368,9 +454,9 @@ class TestArithmetic:
|
||||
_insert_day(store, d, bw=2*1024*1024*100)
|
||||
r_fast = compute_projection(store, _clock())
|
||||
|
||||
if r_slow.headline_remaining_seconds is not None and r_fast.headline_remaining_seconds is not None:
|
||||
assert r_fast.headline_remaining_seconds < r_slow.headline_remaining_seconds
|
||||
|
||||
assert r_slow.headline_remaining_seconds is not None
|
||||
assert r_fast.headline_remaining_seconds is not None
|
||||
assert r_fast.headline_remaining_seconds < r_slow.headline_remaining_seconds
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
@@ -394,17 +480,19 @@ class TestSustainedRegimeRate:
|
||||
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)
|
||||
W_t = 512000000000
|
||||
_insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5, bytes_written=W_t)
|
||||
_insert_complete_local_days(store, "2026-09-29", 1)
|
||||
result = compute_projection(store, _clock())
|
||||
# Regime = full 30 days; rate = 30*bw / wall-clock
|
||||
regime_bytes = 30 * bw
|
||||
period_start = datetime(2026, 9, 1, 0, 0, 0, tzinfo=timezone.utc)
|
||||
wc = int((_clock() - period_start).total_seconds())
|
||||
expected_rate = regime_bytes / wc
|
||||
if result.headline_remaining_seconds is not None:
|
||||
E = 10.0 * TBW_TO_BYTES
|
||||
expected_seconds = max(E - regime_bytes, 0) / expected_rate
|
||||
assert abs(result.headline_remaining_seconds - expected_seconds) < 1.0
|
||||
assert result.headline_remaining_seconds is not None
|
||||
E = 10.0 * TBW_TO_BYTES
|
||||
expected_seconds = max(E - W_t, 0) / expected_rate
|
||||
assert abs(result.headline_remaining_seconds - expected_seconds) < 1.0
|
||||
|
||||
def test_regime_capped_at_90_days(self, store):
|
||||
"""Default regime is full history capped at 90 days."""
|
||||
|
||||
Reference in New Issue
Block a user