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 -9
View File
@@ -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: