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:
+19
-16
@@ -23,7 +23,10 @@ from typing import Any, Dict, Iterator, List, Optional, Tuple, TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from .status_composition import StatusComposition
|
||||
|
||||
from .projection import compute_projection, ConfidenceState, DISCLOSURES
|
||||
from .projection import (
|
||||
compute_projection, ConfidenceState, DISCLOSURES, ProjectionStage, ladder_count_text,
|
||||
ladder_lines, write_rate_gb_day,
|
||||
)
|
||||
from .store import SCHEMA_VERSION
|
||||
from .init_system import (
|
||||
query_service_state as _init_query_service_state,
|
||||
@@ -255,8 +258,7 @@ def check_retired_flag(flag: str) -> Optional[str]:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _format_projection(proj, freshness: str, drive_facts: List[str],
|
||||
config_error: Optional[str],
|
||||
sample_count: int = 0, day_count: int = 0) -> str:
|
||||
config_error: Optional[str]) -> str:
|
||||
"""Format projection details; monitoring status has its own renderer."""
|
||||
lines = []
|
||||
|
||||
@@ -272,15 +274,6 @@ def _format_projection(proj, freshness: str, drive_facts: List[str],
|
||||
lines.append("Enable monitoring: fenris monitor resume")
|
||||
return "\n".join(lines)
|
||||
|
||||
# --- Single sample: awaiting another sample (issue #73 AC3) ---
|
||||
# Only show awaiting state when there are no day aggregates (e.g., legacy import
|
||||
# or hand-crafted stores can have 1 sample but sufficient day data for projection)
|
||||
if sample_count <= 1 and day_count == 0:
|
||||
lines.append("awaiting another sample")
|
||||
lines.append("")
|
||||
lines.append("Collecting usage data — the first projection requires at least two samples.")
|
||||
return "\n".join(lines)
|
||||
|
||||
if proj is None:
|
||||
lines.append("no projection available")
|
||||
return "\n".join(lines)
|
||||
@@ -290,6 +283,9 @@ def _format_projection(proj, freshness: str, drive_facts: List[str],
|
||||
lines.append(headline)
|
||||
lines.append("")
|
||||
|
||||
# --- Stage and ladder count (ADR 0012) ---
|
||||
lines.append("Projection stage: %s · %s" % (proj.stage_label, ladder_count_text(proj)))
|
||||
|
||||
# --- Confidence state + contributing facts (§6.7, §6.11) ---
|
||||
state_label = proj.confidence_state.value
|
||||
facts_list = list(proj.contributing_facts) if proj.contributing_facts else []
|
||||
@@ -305,6 +301,11 @@ def _format_projection(proj, freshness: str, drive_facts: List[str],
|
||||
lines.append("%s evidence" % state_label)
|
||||
lines.append("")
|
||||
|
||||
# --- Full evidence ladder in the outlook (ADR 0012 §5) ---
|
||||
lines.append("Evidence ladder")
|
||||
lines.extend(" " + line for line in ladder_lines(proj))
|
||||
lines.append("")
|
||||
|
||||
# --- Scenario range (§6.5) with horizon reasons ---
|
||||
if proj.scenario_range:
|
||||
parts = []
|
||||
@@ -333,8 +334,13 @@ def _format_projection(proj, freshness: str, drive_facts: List[str],
|
||||
def _format_headline(proj) -> str:
|
||||
"""Format the lifespan headline or its no-projection wording (§6.11)."""
|
||||
if proj.headline_remaining_seconds is None:
|
||||
if proj.stage == ProjectionStage.NO_OBSERVATIONS:
|
||||
return "no projection yet — collecting observations"
|
||||
if proj.zero_rate_fact:
|
||||
return "no finite projection from this history"
|
||||
rate = write_rate_gb_day(proj)
|
||||
if rate is not None:
|
||||
return "write rate: %.2f GB/day · no lifespan without an endurance baseline" % rate
|
||||
if proj.warming_fact:
|
||||
return proj.warming_fact
|
||||
return "no projection available"
|
||||
@@ -507,10 +513,7 @@ def get_status(store_path: Optional[Path] = None, clock_now: Optional[datetime]
|
||||
proj = compute_projection(conn, clock_now)
|
||||
except (sqlite3.Error, ValueError, TypeError):
|
||||
pass
|
||||
parts.append(_format_projection(
|
||||
proj, comp.freshness, drive_facts, config_error,
|
||||
comp.sample_count, comp.day_count,
|
||||
))
|
||||
parts.append(_format_projection(proj, comp.freshness, drive_facts, config_error))
|
||||
if query_journal and (
|
||||
comp.store_fault or comp.last_collect_ok is False
|
||||
or comp.freshness in ("missed", "stale")
|
||||
|
||||
Reference in New Issue
Block a user