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:
+22
-9
@@ -18,6 +18,7 @@ from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from rich.markup import escape
|
||||
from rich.text import Text
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
@@ -38,8 +39,12 @@ from .activity_plot import VolumePoint, volume_plot
|
||||
from .projection import (
|
||||
ConfidenceState,
|
||||
ProjectionResult,
|
||||
ProjectionStage,
|
||||
ScenarioRange,
|
||||
compute_projection,
|
||||
ladder_count_text,
|
||||
ladder_lines,
|
||||
write_rate_gb_day,
|
||||
)
|
||||
from .status import (
|
||||
CADENCE_DEFAULT_S,
|
||||
@@ -2061,12 +2066,6 @@ class FenrisTuiApp(App):
|
||||
self._render_headline(
|
||||
"[bold]No observations yet[/bold]\n\n[bold]%s[/bold]" % _RESUME_HINT
|
||||
)
|
||||
elif comp.sample_count <= 1 and comp.day_count == 0:
|
||||
# Single sample: awaiting another sample
|
||||
self._render_headline(
|
||||
"[bold]Awaiting another sample[/bold]\n\n"
|
||||
"Collecting usage data — the first projection requires at least two samples."
|
||||
)
|
||||
else:
|
||||
try:
|
||||
proj = compute_projection(conn, self._clock_now)
|
||||
@@ -2076,7 +2075,8 @@ class FenrisTuiApp(App):
|
||||
summary, _, context = headline.partition("\n")
|
||||
confidence_title, _, facts = confidence.partition("\n")
|
||||
self._render_headline(summary + "\n" + confidence_title + "\n\n"
|
||||
+ context + "\n" + facts + "\n" + scenario)
|
||||
+ context + "\n" + facts + "\n" + scenario
|
||||
+ "\n\n" + self._format_ladder(proj))
|
||||
except Exception:
|
||||
self._render_headline("[bold]No projection available[/bold]")
|
||||
|
||||
@@ -2267,8 +2267,14 @@ class FenrisTuiApp(App):
|
||||
def _format_headline(self, proj: ProjectionResult) -> str:
|
||||
"""Format the lifespan headline (spec §6.11)."""
|
||||
if proj.headline_remaining_seconds is None:
|
||||
if proj.stage == ProjectionStage.NO_OBSERVATIONS:
|
||||
return "[bold]Usage-adjusted theoretical lifespan: [yellow]no projection yet — collecting observations[/yellow][/bold]"
|
||||
if proj.zero_rate_fact:
|
||||
return "[bold]Usage-adjusted theoretical lifespan: [red]no finite projection from this history[/red][/bold]"
|
||||
rate = write_rate_gb_day(proj)
|
||||
if rate is not None:
|
||||
return ("[bold]Write rate: %.2f GB/day[/bold]\nNo lifespan without an endurance baseline"
|
||||
% rate)
|
||||
if proj.warming_fact:
|
||||
return "[bold]Usage-adjusted theoretical lifespan: [yellow]%s[/yellow][/bold]" % proj.warming_fact
|
||||
return "[bold]Usage-adjusted theoretical lifespan: [red]no projection available[/red][/bold]"
|
||||
@@ -2295,13 +2301,20 @@ class FenrisTuiApp(App):
|
||||
if proj.qualifying_days_progress:
|
||||
facts = proj.qualifying_days_progress + " · " + facts
|
||||
|
||||
return "[bold]Projection confidence: [%s]%s[/%s][/bold]\n %s" % (
|
||||
return "[bold]Projection confidence: [%s]%s[/%s] · %s[/bold]\n %s" % (
|
||||
color,
|
||||
proj.confidence_state.value,
|
||||
proj.stage_label,
|
||||
color,
|
||||
ladder_count_text(proj),
|
||||
facts[:1].upper() + facts[1:],
|
||||
)
|
||||
|
||||
def _format_ladder(self, proj: ProjectionResult) -> str:
|
||||
"""Format the full evidence ladder for the outlook (ADR 0012 §5)."""
|
||||
lines = ["[bold]Evidence ladder[/bold] · %s" % ladder_count_text(proj)]
|
||||
lines.extend(" " + escape(line) for line in ladder_lines(proj))
|
||||
return "\n".join(lines)
|
||||
|
||||
def _format_scenario(self, proj: ProjectionResult) -> str:
|
||||
"""Format scenario range with horizon reasons (spec §6.5)."""
|
||||
if not proj.scenario_range:
|
||||
|
||||
Reference in New Issue
Block a user