feat(projection): show staged projection with evidence ladder

Report a projection stage, an evidence ladder and the sustained-regime
write rate from compute_projection, per ADR 0012. A provisional lifespan
appears after 3 observed hours and carries the hours observed and a
short-horizon spread; the complete-local-day condition becomes a fact
instead of a gate, and the write rate replaces the lifespan when no
baseline applies. The TUI and CLI render the stage, ladder count and
full ladder, and drop their own two-sample pre-gates.
This commit is contained in:
xavierk
2026-10-05 19:57:46 +05:30
parent 96fd1702fa
commit e26a851115
8 changed files with 615 additions and 148 deletions
+22 -20
View File
@@ -1,16 +1,18 @@
"""Complete observation day gate tests (issue #94).
"""Complete observation day condition tests (issues #94, #106).
Verifies that the endurance projection is withheld until at least one
complete local calendar day has been observed within a monitoring period.
Originally a gate (issue #94); ADR 0012 turned it into a contributing
fact. The projection is no longer withheld until one complete local
calendar day has been observed within a monitoring period, but it says
so while that condition is unmet.
Seams:
- compute_projection() → gate check via local_days table
- ProjectionResult.contributing_facts → "waiting for a full local observation day"
Acceptance criteria:
- Gate-1: No complete local day → UNSUPPORTED with waiting fact
- Gate-2: One complete local day → Limited confidence (if other conditions met)
- Gate-3: Partial days don't satisfy the gate
- Gate-1: No complete local day → projection renders with waiting fact
- Gate-2: One complete local day → no waiting fact
- Gate-3: Partial days don't satisfy the condition
- Gate-4: CLI and TUI share the same gate via compute_projection()
"""
import sqlite3
@@ -111,14 +113,14 @@ def _open_period(conn, start="2026-09-01T00:00:00+00:00"):
# ---------------------------------------------------------------------------
# Gate-1: No complete local day → UNSUPPORTED with waiting fact
# Gate-1: No complete local day → projection renders with waiting fact
# ---------------------------------------------------------------------------
class TestGateNoCompleteDay:
"""Projection is unavailable before any complete local observation day."""
"""Projection still renders before any complete local observation day."""
def test_no_local_days_unsupported(self, store):
"""With no local_days entries, projection is UNSUPPORTED."""
def test_no_local_days_states_waiting_fact(self, store):
"""With no local_days entries, the projection renders and says it is waiting."""
_insert_baseline(store)
_insert_segment(store)
_open_period(store)
@@ -128,11 +130,11 @@ class TestGateNoCompleteDay:
_insert_day(store, d, bw=1024*1024*100)
_insert_sample(store, "2026-09-30T10:00:00+00:00", pu=5)
result = compute_projection(store, _clock())
assert result.confidence_state == ConfidenceState.UNSUPPORTED
assert result.confidence_state != ConfidenceState.UNSUPPORTED
assert any("full local observation day" in f for f in result.contributing_facts)
assert result.headline_remaining_seconds is None
assert result.headline_remaining_seconds is not None
def test_only_partial_local_days_unsupported(self, store):
def test_only_partial_local_days_state_waiting_fact(self, store):
"""Partial (incomplete) local days don't satisfy the gate."""
_insert_baseline(store)
_insert_segment(store)
@@ -146,7 +148,7 @@ class TestGateNoCompleteDay:
d = (datetime(2026, 9, 25) + timedelta(days=i)).strftime("%Y-%m-%d")
_insert_local_day(store, d, complete=False, coverage=0.3)
result = compute_projection(store, _clock())
assert result.confidence_state == ConfidenceState.UNSUPPORTED
assert result.confidence_state != ConfidenceState.UNSUPPORTED
assert any("full local observation day" in f for f in result.contributing_facts)
def test_gate_before_warming_check(self, store):
@@ -166,7 +168,7 @@ class TestGateNoCompleteDay:
# The key assertion: gate message should NOT appear when gate IS met
assert not any("full local observation day" in f for f in result.contributing_facts)
def test_complete_legacy_day_without_trusted_activity_does_not_open_gate(
def test_complete_legacy_day_without_trusted_activity_keeps_waiting_fact(
self, store,
):
"""A complete flag cannot make unavailable local activity qualify."""
@@ -183,11 +185,11 @@ class TestGateNoCompleteDay:
"WHERE local_date = '2026-09-29'"
)
result = compute_projection(store, _clock())
assert result.confidence_state == ConfidenceState.UNSUPPORTED
assert result.confidence_state != ConfidenceState.UNSUPPORTED
assert any("full local observation day" in fact
for fact in result.contributing_facts)
def test_day_split_by_deliberate_pause_does_not_open_gate(self, store):
def test_day_split_by_deliberate_pause_keeps_waiting_fact(self, store):
"""A complete-looking summary cannot span separate monitoring periods."""
_insert_baseline(store)
_insert_segment(store)
@@ -216,7 +218,7 @@ class TestGateNoCompleteDay:
result = compute_projection(store, _clock())
assert result.confidence_state == ConfidenceState.UNSUPPORTED
assert result.confidence_state != ConfidenceState.UNSUPPORTED
assert any("full local observation day" in fact
for fact in result.contributing_facts)
@@ -290,7 +292,7 @@ class TestGateOneCompleteDay:
class TestGatePartialFirstDay:
"""Starting monitoring at noon means the partial first day doesn't count."""
def test_partial_first_day_not_enough(self, store):
def test_partial_first_day_keeps_waiting_fact(self, store):
"""A single incomplete local day (started at noon) doesn't open the gate."""
_insert_baseline(store)
_insert_segment(store, opened_at="2026-09-29T12:00:00+00:00")
@@ -303,7 +305,7 @@ class TestGatePartialFirstDay:
_insert_local_day(store, "2026-09-29", complete=False, coverage=0.5)
_insert_local_day(store, "2026-09-30", complete=False, coverage=0.5)
result = compute_projection(store, _clock())
assert result.confidence_state == ConfidenceState.UNSUPPORTED
assert result.confidence_state != ConfidenceState.UNSUPPORTED
assert any("full local observation day" in f for f in result.contributing_facts)