From 1be72314f34f1eff6d4252c8acda7cf25cd6679d Mon Sep 17 00:00:00 2001 From: AbdelStark Date: Fri, 12 Jun 2026 11:03:22 +0200 Subject: [PATCH] feat: redesign the pwm-tui dashboard A horizontal pipeline flow strip replaces the vertical rail, and a dominant always-visible overview pane carries the run: live download gauges with a throughput sparkline during FETCH, substage progress and a quantize bar during EXPORT, then a KPI grid (checkpoint, params, graph, latency, witness, Freivalds, float gate, z_next) that fills in live from exporter events, op-mix bars, the commitment table, and the verdict banner. A secondary per-stage detail pane (arrow keys) keeps the advanced view. Charm-style truecolor palette, rounded padded panels, NO_COLOR intact, narrow terminals drop the side pane. Header and labels are factual; promotional phrasing removed. --- CHANGELOG.md | 5 + crates/pwm-tui/src/app.rs | 94 ++- crates/pwm-tui/src/main.rs | 6 +- crates/pwm-tui/src/pipeline.rs | 16 +- crates/pwm-tui/src/ui.rs | 1142 +++++++++++++++++++++----------- 5 files changed, 878 insertions(+), 385 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b988e0..f8b7ee6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,11 @@ entry. ### Added +- The `pwm-tui` dashboard layout: a horizontal pipeline flow strip, a dominant + always-visible overview pane (live download gauges with a throughput + sparkline, exporter substage progress, a KPI grid that fills in as the + exporter reports facts, op-mix bars, the commitment table, and the verdict + banner), and a secondary per-stage detail pane with ←/→ navigation. - **`pwm-tui`: a ratatui demo that proves the REAL pretrained checkpoint end to end locally, with no Docker** (#200): downloads and sha256-pin-verifies the `quentinll/lewm-pusht` checkpoint and a real `lerobot/pusht` episode into a diff --git a/crates/pwm-tui/src/app.rs b/crates/pwm-tui/src/app.rs index 5ffa418..d08006b 100644 --- a/crates/pwm-tui/src/app.rs +++ b/crates/pwm-tui/src/app.rs @@ -4,6 +4,7 @@ //! TUI and `--headless` can never disagree about what happened. use std::collections::VecDeque; +use std::time::Instant; use pwm_testkit::report::PredictorReport; @@ -32,6 +33,53 @@ pub(crate) struct ExportSubRow { pub(crate) ms: Option, } +/// Typed key facts parsed out of the exporter's kv stream, so the dashboard +/// can show live numbers before the stage-2 report exists. +#[derive(Default)] +pub(crate) struct Dash { + /// Checkpoint size in bytes (load stage). + pub(crate) checkpoint_bytes: Option, + /// Checkpoint tensor count (load stage). + pub(crate) tensors: Option, + /// Checkpoint float parameter count (load stage). + pub(crate) float_params: Option, + /// Quantized matrices in the exported manifest (quantize stage). + pub(crate) matrices: Option, + /// Exported int8 parameter count (quantize stage; wider scope than the + /// proven relation, which the report narrows to its own count). + pub(crate) int8_params: Option, + /// Measured max |int - float| (calibrate stage). + pub(crate) error: Option, + /// Bundle-carried float tolerance (calibrate stage). + pub(crate) tolerance: Option, + /// Predictor-scoped weights root carried in the bundle (bundle stage). + pub(crate) weights_root: Option, + /// Predictor bundle size in bytes (bundle stage). + pub(crate) bundle_bytes: Option, + /// Input provenance string (encode stage). + pub(crate) source: Option, +} + +impl Dash { + fn absorb(&mut self, stage: &str, key: &str, value: &str) { + let u = || value.parse::().ok(); + let f = || value.parse::().ok(); + match (stage, key) { + ("load", "checkpoint_bytes") => self.checkpoint_bytes = u(), + ("load", "tensors") => self.tensors = u(), + ("load", "float_params") => self.float_params = u(), + ("quantize", "matrices") => self.matrices = u(), + ("quantize", "int8_params") => self.int8_params = u(), + ("calibrate", "error") => self.error = f(), + ("calibrate", "tolerance") => self.tolerance = f(), + ("bundle", "weights_root") => self.weights_root = Some(value.to_string()), + ("bundle", "predictor_bundle_bytes") => self.bundle_bytes = u(), + ("encode", "source") => self.source = Some(value.to_string()), + _ => {} + } + } +} + /// Everything the front ends render. pub(crate) struct App { /// Per-stage lifecycle, indexed by [`Stage::idx`]. @@ -58,6 +106,16 @@ pub(crate) struct App { pub(crate) cache_root: String, /// Spinner frame counter (advanced by the draw loop). pub(crate) spin: usize, + /// Typed dashboard facts parsed from the exporter kv stream. + pub(crate) dash: Dash, + /// Wall-clock start of the run. + pub(crate) started: Instant, + /// Wall-clock end of the run (freezes the elapsed display). + pub(crate) finished_at: Option, + /// Per-tick downloaded-bytes deltas, newest last (throughput sparkline). + pub(crate) net_history: Vec, + /// Total fetched bytes at the previous tick. + last_net: u64, } impl App { @@ -84,7 +142,33 @@ impl App { show_logs: false, cache_root, spin: 0, + dash: Dash::default(), + started: Instant::now(), + finished_at: None, + net_history: Vec::new(), + last_net: 0, + } + } + + /// Advance animation state and sample the download throughput. Called once + /// per draw tick. + pub(crate) fn on_tick(&mut self) { + self.spin = self.spin.wrapping_add(1); + let total: u64 = self.fetch.iter().map(|r| r.done).sum(); + let fetching = matches!(self.stages[Stage::Fetch.idx()], StageState::Running); + if fetching { + self.net_history.push(total.saturating_sub(self.last_net)); + if self.net_history.len() > 72 { + self.net_history.remove(0); + } } + self.last_net = total; + } + + /// Elapsed wall time, frozen once the pipeline finishes. + pub(crate) fn elapsed_secs(&self) -> f64 { + let end = self.finished_at.unwrap_or_else(Instant::now); + end.duration_since(self.started).as_secs_f64() } /// Fold one pipeline event into the state. @@ -119,7 +203,10 @@ impl App { } } Event::ExportProgress { done, total } => self.export_progress = Some((done, total)), - Event::Kv { stage, key, value } => self.kvs.push((stage, key, value)), + Event::Kv { stage, key, value } => { + self.dash.absorb(&stage, &key, &value); + self.kvs.push((stage, key, value)); + } Event::Log(line) => { self.logs.push_back(line); while self.logs.len() > 500 { @@ -127,7 +214,10 @@ impl App { } } Event::Report(r) => self.report = Some(*r), - Event::Finished { ok } => self.finished = Some(ok), + Event::Finished { ok } => { + self.finished = Some(ok); + self.finished_at = Some(Instant::now()); + } } } diff --git a/crates/pwm-tui/src/main.rs b/crates/pwm-tui/src/main.rs index eeb7858..40f5592 100644 --- a/crates/pwm-tui/src/main.rs +++ b/crates/pwm-tui/src/main.rs @@ -232,7 +232,7 @@ fn run_tui(opts: &Options, json: bool) -> ExitCode { while let Ok(ev) = rx.try_recv() { app.handle(ev); } - app.spin = app.spin.wrapping_add(1); + app.on_tick(); if terminal.draw(|f| ui::render(f, &app)).is_err() { break; } @@ -247,8 +247,8 @@ fn run_tui(opts: &Options, json: bool) -> ExitCode { break 'outer; } (KeyCode::Char('l'), _) => app.show_logs = !app.show_logs, - (KeyCode::Up, _) => app.select(-1), - (KeyCode::Down, _) => app.select(1), + (KeyCode::Up | KeyCode::Left | KeyCode::Char('k'), _) => app.select(-1), + (KeyCode::Down | KeyCode::Right | KeyCode::Char('j'), _) => app.select(1), (KeyCode::Esc, _) => app.selected = None, _ => {} } diff --git a/crates/pwm-tui/src/pipeline.rs b/crates/pwm-tui/src/pipeline.rs index c46b065..3de2c1e 100644 --- a/crates/pwm-tui/src/pipeline.rs +++ b/crates/pwm-tui/src/pipeline.rs @@ -67,15 +67,13 @@ impl Stage { /// One-line description for the detail panel header. pub(crate) fn blurb(self) -> &'static str { match self { - Stage::Fetch => "pin-verified checkpoint + real episode files (cache-first)", - Stage::Export => { - "PyTorch exporter: extract V0, quantize int8, commit, encode, calibrate" - } - Stage::Load => "parse the bundle, bind the export weights root, build the circuit", - Stage::Infer => "exact integer forward pass (the world model runs; no float, no GPU)", - Stage::Commit => "bind execution to the Fiat-Shamir transcript and commitments", - Stage::Verify => "no_std float-free audit: commitments, Freivalds, exact recompute", - Stage::Tamper => "forge one matmul output; the audit must reject it", + Stage::Fetch => "fetch the checkpoint and episode assets, sha256-pinned, cache-first", + Stage::Export => "extract the V0 subgraph, quantize to int8, commit, encode, calibrate", + Stage::Load => "parse the bundle, bind the weights root, build the circuit", + Stage::Infer => "exact integer forward pass, no float, no GPU", + Stage::Commit => "bind the execution to the Fiat-Shamir transcript", + Stage::Verify => "audit: commitments, Freivalds checks, exact recompute", + Stage::Tamper => "forge one matmul output, the audit must reject it", } } diff --git a/crates/pwm-tui/src/ui.rs b/crates/pwm-tui/src/ui.rs index 425c100..8a58164 100644 --- a/crates/pwm-tui/src/ui.rs +++ b/crates/pwm-tui/src/ui.rs @@ -1,11 +1,13 @@ // SPDX-License-Identifier: Apache-2.0 -//! Rendering: the header, the stage rail, the per-stage detail panel, the log -//! pane, and the metrics footer. Pure projection of [`App`]; no pipeline logic. +//! Rendering: a header, a horizontal pipeline flow strip, a dominant overview +//! dashboard (live activity, KPIs, op mix, commitments, verdicts), a secondary +//! per-stage detail pane, and a status footer. Pure projection of [`App`]; no +//! pipeline logic. -use ratatui::layout::{Constraint, Layout, Rect}; +use ratatui::layout::{Alignment, Constraint, Layout, Rect}; use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span}; -use ratatui::widgets::{Block, BorderType, Borders, Paragraph, Wrap}; +use ratatui::widgets::{Block, BorderType, Borders, Gauge, Padding, Paragraph, Sparkline, Wrap}; use ratatui::Frame; use crate::app::App; @@ -13,6 +15,17 @@ use crate::pipeline::{Stage, StageState}; const SPINNER: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; +// Charm-style truecolor palette (NO_COLOR falls back to terminal defaults). +const LAVENDER: Color = Color::Rgb(0x7d, 0x56, 0xf4); +const PINK: Color = Color::Rgb(0xf2, 0x5d, 0x94); +const MINT: Color = Color::Rgb(0x04, 0xb5, 0x75); +const GOLD: Color = Color::Rgb(0xff, 0xb4, 0x54); +const CORAL: Color = Color::Rgb(0xff, 0x5f, 0x87); +const SKY: Color = Color::Rgb(0x74, 0xc7, 0xec); +const SUBTLE: Color = Color::Rgb(0x6c, 0x70, 0x85); +const TEXT: Color = Color::Rgb(0xcd, 0xd6, 0xf4); +const TRACK: Color = Color::Rgb(0x2a, 0x2b, 0x3c); + fn color_on() -> bool { std::env::var_os("NO_COLOR").is_none() } @@ -30,16 +43,16 @@ fn bold(c: Color) -> Style { } fn dim() -> Style { - if color_on() { - Style::default().fg(Color::DarkGray) - } else { - Style::default() - } + fg(SUBTLE) } -fn hex16(root: &[u8; 32]) -> String { +fn body() -> Style { + fg(TEXT) +} + +fn hex12(root: &[u8; 32]) -> String { root.iter() - .take(8) + .take(6) .map(|b| format!("{b:02x}")) .collect::() + "…" @@ -79,6 +92,19 @@ fn ms_str(ms: f64) -> String { } } +fn commas(n: usize) -> String { + let s = n.to_string(); + let b = s.as_bytes(); + let mut out = String::new(); + for (i, c) in b.iter().enumerate() { + if i > 0 && (b.len() - i) % 3 == 0 { + out.push(','); + } + out.push(*c as char); + } + out +} + fn bar(ratio: f64, width: usize) -> String { let filled = ((ratio.clamp(0.0, 1.0)) * width as f64).round() as usize; format!( @@ -88,407 +114,646 @@ fn bar(ratio: f64, width: usize) -> String { ) } +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + let cut: String = s.chars().take(max.saturating_sub(1)).collect(); + format!("{cut}…") + } +} + +fn panel(title: &str) -> Block<'static> { + Block::default() + .title(Line::from(vec![ + Span::styled(" ✦ ", fg(PINK)), + Span::styled(format!("{title} "), bold(LAVENDER)), + ])) + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(dim()) + .padding(Padding::new(1, 1, 0, 0)) +} + /// Draw the whole frame. pub(crate) fn render(f: &mut Frame<'_>, app: &App) { - let [header, body, footer] = Layout::vertical([ - Constraint::Length(4), - Constraint::Min(8), + let [header, flow, body_area, footer] = Layout::vertical([ + Constraint::Length(3), Constraint::Length(4), + Constraint::Min(10), + Constraint::Length(1), ]) .areas(f.area()); render_header(f, app, header); - let [rail, detail] = - Layout::horizontal([Constraint::Length(26), Constraint::Min(30)]).areas(body); - render_rail(f, app, rail); - if app.show_logs { - render_logs(f, app, detail); + render_flow(f, app, flow); + let wide = body_area.width >= 96; + if wide { + let [main, side] = + Layout::horizontal([Constraint::Min(56), Constraint::Length(40)]).areas(body_area); + render_overview(f, app, main); + if app.show_logs { + render_logs(f, app, side); + } else { + render_detail(f, app, side); + } + } else if app.show_logs { + render_logs(f, app, body_area); } else { - render_detail(f, app, detail); + render_overview(f, app, body_area); } render_footer(f, app, footer); } fn render_header(f: &mut Frame<'_>, app: &App, area: Rect) { - let block = Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(dim()); - let fetched: Vec<&str> = app.fetch.iter().filter_map(|r| r.outcome).collect(); - let cache_note = if fetched.is_empty() { - "resolving cache".to_string() - } else { - fetched.join(" · ") - }; let lines = vec![ Line::from(vec![ - Span::styled("ProvableWorldModel", bold(Color::Cyan)), - Span::raw(" real demo · REAL pretrained checkpoint, end to end · no docker"), + Span::styled(" ◆ ", bold(PINK)), + Span::styled("Provable", bold(LAVENDER)), + Span::styled("WorldModel", bold(PINK)), + Span::styled(" · commit and audit a world-model inference", body()), ]), Line::from(vec![ - Span::styled("checkpoint ", dim()), - Span::raw("quentinll/lewm-pusht"), - Span::styled(" assets ", dim()), - Span::raw(cache_note), - Span::styled(" cache ", dim()), - Span::raw(app.cache_root.clone()), + Span::styled(" le-wm V0 predictor", dim()), + Span::styled(" · quentinll/lewm-pusht · lerobot/pusht episode", dim()), + Span::styled(format!(" · cache {}", app.cache_root), dim()), ]), ]; - f.render_widget(Paragraph::new(lines).block(block), area); + f.render_widget(Paragraph::new(lines), area); } -fn stage_line<'a>(app: &'a App, stage: Stage, selected: bool) -> Line<'a> { - let state = &app.stages[stage.idx()]; - let (icon, style) = match state { - StageState::Pending => (Span::styled("·", dim()), dim()), - StageState::Running => ( - Span::styled(SPINNER[app.spin % SPINNER.len()], fg(Color::Yellow)), - fg(Color::Yellow), - ), - StageState::Done(_) => (Span::styled("✔", fg(Color::Green)), Style::default()), - StageState::Skipped(_) => (Span::styled("↷", fg(Color::Cyan)), fg(Color::Cyan)), - StageState::Failed(_) => (Span::styled("✗", bold(Color::Red)), bold(Color::Red)), - }; - let time = match state { - StageState::Done(ms) => ms_str(*ms), - StageState::Skipped(_) => "cached".to_string(), - _ => String::new(), - }; - let marker = if selected { "▸" } else { " " }; - Line::from(vec![ - Span::styled(marker.to_string(), bold(Color::Cyan)), - icon, - Span::raw(" "), - Span::styled(format!("{:<7}", stage.label()), style), - Span::styled(format!("{time:>9}"), dim()), - ]) +fn stage_glyph(app: &App, stage: Stage) -> (String, Style) { + match &app.stages[stage.idx()] { + StageState::Pending => ("◌".into(), dim()), + StageState::Running => (SPINNER[app.spin % SPINNER.len()].into(), bold(GOLD)), + StageState::Done(_) => ("●".into(), fg(MINT)), + StageState::Skipped(_) => ("◍".into(), fg(SKY)), + StageState::Failed(_) => ("✗".into(), bold(CORAL)), + } } -fn render_rail(f: &mut Frame<'_>, app: &App, area: Rect) { +fn render_flow(f: &mut Frame<'_>, app: &App, area: Rect) { let block = Block::default() - .title(Span::styled(" PIPELINE ", bold(Color::Cyan))) .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(dim()); + .border_style(dim()) + .padding(Padding::new(1, 1, 0, 0)); + let inner_w = area.width.saturating_sub(4) as usize; + let short = inner_w < 7 * 10 + 6 * 4; let view = app.view_stage(); + + let mut nodes: Vec> = Vec::new(); + let mut times: Vec> = Vec::new(); + for (i, stage) in Stage::ALL.iter().enumerate() { + let (icon, icon_style) = stage_glyph(app, *stage); + let label = if short { + &stage.label()[..3.min(stage.label().len())] + } else { + stage.label() + }; + let cell_w = 2 + label.chars().count(); + let selected = *stage == view; + let label_style = match (&app.stages[stage.idx()], selected) { + (StageState::Failed(_), _) => bold(CORAL), + (_, true) => bold(TEXT).add_modifier(Modifier::UNDERLINED), + (StageState::Pending, false) => dim(), + (StageState::Running, false) => bold(GOLD), + _ => body(), + }; + nodes.push(Span::styled(icon, icon_style)); + nodes.push(Span::raw(" ")); + nodes.push(Span::styled(label.to_string(), label_style)); + let t = match &app.stages[stage.idx()] { + StageState::Done(ms) => ms_str(*ms), + StageState::Skipped(_) => "cached".to_string(), + StageState::Running => "…".to_string(), + _ => String::new(), + }; + times.push(Span::styled(format!("{t:, app: &App, area: Rect) { + let block = panel("OVERVIEW"); + let inner = block.inner(area); + f.render_widget(block, area); + let [activity, rest] = + Layout::vertical([Constraint::Length(6), Constraint::Min(0)]).areas(inner); + render_activity(f, app, activity); + render_dashboard(f, app, rest); +} + +/// What is happening right now: download gauges + a throughput sparkline during +/// FETCH, substage progress during EXPORT, a stage ticker for the proof stages, +/// and the verdict banner once finished. +fn render_activity(f: &mut Frame<'_>, app: &App, area: Rect) { + if app.finished.is_some() { + render_verdict_banner(f, app, area); + return; + } + if matches!(app.stages[Stage::Fetch.idx()], StageState::Running) { + let rows = Layout::vertical([ + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(3), + ]) + .split(area); + for (i, row) in app.fetch.iter().enumerate() { + let ratio = if row.total > 0 { + row.done as f64 / row.total as f64 + } else { + 0.0 + }; + let [name_a, gauge_a, stat_a] = Layout::horizontal([ + Constraint::Length(17), + Constraint::Min(10), + Constraint::Length(32), + ]) + .areas(rows[i]); + f.render_widget( + Paragraph::new(Line::from(Span::styled(row.name.to_string(), body()))), + name_a, + ); + let g = Gauge::default() + .ratio(ratio) + .label(Span::styled(format!("{:>4.0}%", ratio * 100.0), body())) + .use_unicode(true) + .gauge_style(if color_on() { + Style::default().fg(LAVENDER).bg(TRACK) + } else { + Style::default() + }); + f.render_widget(g, gauge_a); + let stat = Line::from(vec![ + Span::styled( + format!(" {:>9} / {:<9}", mib(row.done), mib(row.total)), + dim(), + ), + Span::styled( + row.outcome.map(|o| format!(" {o}")).unwrap_or_default(), + fg(MINT), + ), + ]); + f.render_widget(Paragraph::new(stat), stat_a); + } + let spark = Sparkline::default().data(&app.net_history).style(fg(PINK)); + f.render_widget(spark, rows[3]); + return; + } + if matches!(app.stages[Stage::Export.idx()], StageState::Running) { + let mut lines: Vec> = Vec::new(); + let mut row: Vec> = Vec::new(); + for sub in &app.export_subs { + let (icon, style) = if sub.done { + ("●".to_string(), fg(MINT)) + } else { + (SPINNER[app.spin % SPINNER.len()].to_string(), bold(GOLD)) + }; + row.push(Span::styled(icon, style)); + row.push(Span::styled( + format!(" {}", sub.name), + if sub.done { body() } else { bold(GOLD) }, + )); + if let Some(ms) = sub.ms { + row.push(Span::styled(format!(" {}", ms_str(ms)), dim())); + } + row.push(Span::raw(" ")); + } + lines.push(Line::from(row)); + lines.push(Line::raw("")); + if let Some((d, t)) = app.export_progress { + let ratio = if t > 0 { d as f64 / t as f64 } else { 0.0 }; + lines.push(Line::from(vec![ + Span::styled("quantize ", dim()), + Span::styled(bar(ratio, 40), fg(LAVENDER)), + Span::styled(format!(" {d}/{t} matrices"), body()), + ])); + } + if let Some(last) = app.logs.back() { + lines.push(Line::raw("")); + lines.push(Line::from(Span::styled( + truncate(last, area.width as usize), + dim(), + ))); + } + f.render_widget(Paragraph::new(lines), area); + return; + } + // Proof stages: a ticker for whichever stage is live. + let live = Stage::ALL + .iter() + .find(|s| matches!(app.stages[s.idx()], StageState::Running)); + let mut lines: Vec> = vec![Line::raw("")]; + match live { + Some(stage) => lines.push(Line::from(vec![ + Span::styled(SPINNER[app.spin % SPINNER.len()].to_string(), bold(GOLD)), + Span::styled(format!(" {}", stage.label()), bold(TEXT)), + Span::styled(format!(" {}", stage.blurb()), dim()), + ])), + None => lines.push(Line::from(Span::styled("starting…", dim()))), + } + lines.push(Line::raw("")); + lines.push(Line::from(Span::styled( + format!("elapsed {:.1} s", app.elapsed_secs()), + dim(), + ))); + f.render_widget(Paragraph::new(lines), area); +} + +fn render_verdict_banner(f: &mut Frame<'_>, app: &App, area: Rect) { + let mut lines: Vec> = vec![Line::raw("")]; + if app.ok() { + lines.push(Line::from(vec![ + Span::styled(" ✔ ACCEPT ", bold(MINT).add_modifier(Modifier::REVERSED)), + Span::styled(" honest proof verified", body()), + ])); + if let Some(r) = &app.report { + if let (Some(op), Some(e)) = (&r.forged_op, &r.reject) { + lines.push(Line::from(vec![ + Span::styled(" ✔ TAMPER CAUGHT ", bold(MINT)), + Span::styled(format!(" forged matmul op {op} rejected: {e}"), dim()), + ])); + } + } + } else { + let why = app + .stages + .iter() + .find_map(|s| match s { + StageState::Failed(e) => Some(e.clone()), + _ => None, + }) + .unwrap_or_else(|| "see the log (l)".to_string()); + lines.push(Line::from(vec![ + Span::styled(" ✗ FAILED ", bold(CORAL).add_modifier(Modifier::REVERSED)), + Span::styled( + format!( + " {}", + truncate(&why, (area.width as usize).saturating_sub(12)) + ), + body(), + ), + ])); + } + lines.push(Line::from(Span::styled( + format!(" done in {:.1} s", app.elapsed_secs()), + dim(), + ))); + f.render_widget(Paragraph::new(lines), area); +} + +fn kpi<'a>(label: &str, value: Option) -> Vec> { + vec![ + Span::styled(format!("{label:<11} "), dim()), + match value { + Some(v) => Span::styled(v, body()), + None => Span::styled("·".to_string(), dim()), + }, + ] +} + +/// Two-column KPI grid + op mix bars + commitments. Fills in live as the +/// exporter reports facts, then sharpens once the proof report lands. +#[allow(clippy::too_many_lines)] +fn render_dashboard(f: &mut Frame<'_>, app: &App, area: Rect) { + let d = &app.dash; + let r = app.report.as_ref(); + let half = area.width as usize / 2; + + // --- KPI pairs (left, right) --- + let model = r + .map(|r| format!("{} · {}", commas(r.params), mib(r.model_bytes as u64))) + .or_else(|| { + d.int8_params + .map(|p| format!("{} exported", commas(p as usize))) + }); + let checkpoint = d.checkpoint_bytes.map(|b| { + format!( + "{} · {} tensors", + mib(b), + d.tensors.map_or_else(|| "?".into(), |t| t.to_string()) + ) + }); + let graph = r.map(|r| format!("{} ops · {} MAC", commas(r.ops), macs_human(r.macs))); + let config = r.map(|r| format!("dim {} · depth {} · h{}", r.dims.d, r.dims.depth, r.dims.h)); + let latency = r.map(|r| { + format!( + "infer {}·verify {}", + ms_str(r.infer.as_secs_f64() * 1000.0).replace(' ', ""), + ms_str(r.verify.as_secs_f64() * 1000.0).replace(' ', "") + ) + }); + let witness = r.map(|r| { + format!( + "{} vals · {}", + commas(r.witness_vals), + bytes_human(r.proof_bytes) + ) + }); + let freivalds = r.map(|r| format!("{} linear projections", r.linear_ops)); + let float_gate = match ( + r.and_then(|r| r.float_error), + r.and_then(|r| r.float_tolerance), + ) { + (Some(e), Some(t)) => Some(format!("{e:.6} ≤ {t:.6}")), + _ => match (d.error, d.tolerance) { + (Some(e), Some(t)) => Some(format!("{e:.6} ≤ {t:.6}")), + _ => None, + }, + }; + let z_next = r.map(|r| format!("{:?}", r.z_out_head)); + let bundle = d.bundle_bytes.map(mib); + + let pairs: [(&str, Option, &str, Option); 5] = [ + ("checkpoint", checkpoint, "int8 params", model), + ("graph", graph, "config", config), + ("latency", latency, "witness", witness), + ("freivalds", freivalds, "float gate", float_gate), + ("z_next", z_next, "bundle", bundle), + ]; let mut lines: Vec> = Vec::new(); - for stage in Stage::ALL { - lines.push(stage_line(app, stage, stage == view)); + for (l1, v1, l2, v2) in pairs { + let mut spans = kpi(l1, v1.map(|v| truncate(&v, half.saturating_sub(13)))); + let used: usize = spans.iter().map(|s| s.content.chars().count()).sum(); + spans.push(Span::raw(" ".repeat(half.saturating_sub(used)))); + spans.extend(kpi(l2, v2.map(|v| truncate(&v, half.saturating_sub(13))))); + lines.push(Line::from(spans)); } + + // --- op mix --- lines.push(Line::raw("")); - if let Some(ok) = app.finished { - let (txt, style) = if ok && app.ok() { - (" ACCEPT", bold(Color::Green)) - } else { - (" FAILED", bold(Color::Red)) - }; - lines.push(Line::from(Span::styled(txt, style))); + if let Some(r) = r { + lines.push(Line::from(Span::styled("op mix", bold(LAVENDER)))); + let max = r.hist.first().map_or(1, |(_, n)| *n); + let bar_w = (area.width as usize).saturating_sub(30); + for (k, n) in r.hist.iter().take(6) { + let w = ((*n as f64 / max as f64) * bar_w as f64).max(1.0) as usize; + lines.push(Line::from(vec![ + Span::styled(format!("{k:<15}"), dim()), + Span::styled("▰".repeat(w), fg(LAVENDER)), + Span::styled(format!(" {}", commas(*n)), body()), + ])); + } + } else { + lines.push(Line::from(Span::styled( + "op mix appears once the graph is loaded", + dim(), + ))); } - f.render_widget(Paragraph::new(lines).block(block), area); + + // --- commitments --- + lines.push(Line::raw("")); + lines.push(Line::from(Span::styled("commitments", bold(LAVENDER)))); + match r { + Some(r) => { + let bound = if r.weights_bound_to_export { + Span::styled(" export-bound", bold(MINT)) + } else { + Span::styled(" prover-computed", fg(GOLD)) + }; + lines.push(Line::from(vec![ + Span::styled("weights ", dim()), + Span::styled(hex12(&r.weights_root), body()), + bound, + ])); + lines.push(Line::from(vec![ + Span::styled("model ", dim()), + Span::styled(hex12(&r.model_commitment), body()), + Span::styled(" quant ", dim()), + Span::styled(hex12(&r.quantization_commitment), body()), + ])); + lines.push(Line::from(vec![ + Span::styled("inputs ", dim()), + Span::styled(hex12(&r.input_commitment), body()), + Span::styled(" output ", dim()), + Span::styled(hex12(&r.output_commitment), body()), + ])); + lines.push(Line::from(vec![ + Span::styled("trace ", dim()), + Span::styled(hex12(&r.trace_root), body()), + ])); + } + None => match &d.weights_root { + Some(w) => lines.push(Line::from(vec![ + Span::styled("weights ", dim()), + Span::styled(format!("{}…", &w[..12.min(w.len())]), body()), + Span::styled( + " carried in the bundle; the prover must reproduce it", + dim(), + ), + ])), + None => lines.push(Line::from(Span::styled( + "commitments appear at the bundle step", + dim(), + ))), + }, + } + + f.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), area); } -fn panel<'a>(title: &'a str, blurb: &'a str) -> Block<'a> { - Block::default() +#[allow(clippy::too_many_lines)] +fn render_detail(f: &mut Frame<'_>, app: &App, area: Rect) { + let stage = app.view_stage(); + let block = Block::default() .title(Line::from(vec![ - Span::styled(format!(" {title} "), bold(Color::Magenta)), - Span::styled(blurb, dim()), + Span::styled(" ◇ ", fg(PINK)), + Span::styled(format!("{} ", stage.label()), bold(LAVENDER)), ])) .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(dim()) -} - -#[allow(clippy::too_many_lines)] -fn render_detail(f: &mut Frame<'_>, app: &App, area: Rect) { - let stage = app.view_stage(); - let mut lines: Vec> = vec![Line::raw("")]; + .padding(Padding::new(1, 1, 0, 0)); + let mut lines: Vec> = vec![ + Line::from(Span::styled(stage.blurb().to_string(), dim())), + Line::raw(""), + ]; + let kv = |lines: &mut Vec>, key: &str, value: String| { + lines.push(Line::from(vec![ + Span::styled(format!("{key:<11} "), dim()), + Span::styled(value, body()), + ])); + }; match stage { Stage::Fetch => { for row in &app.fetch { - let ratio = if row.total > 0 { - row.done as f64 / row.total as f64 - } else { - 0.0 - }; - lines.push(Line::from(vec![ - Span::raw(format!(" {:<18}", row.name)), - Span::styled(bar(ratio, 28), fg(Color::Cyan)), - Span::raw(format!(" {:>9} / {:<9}", mib(row.done), mib(row.total))), - Span::styled( - row.outcome.map(|o| format!(" {o}")).unwrap_or_default(), - fg(Color::Green), + kv( + &mut lines, + row.name, + format!( + "{} / {} {}", + mib(row.done), + mib(row.total), + row.outcome.unwrap_or("") ), - ])); - lines.push(Line::raw("")); + ); } + lines.push(Line::raw("")); lines.push(Line::from(Span::styled( - " every asset is sha256-pinned; a corrupted or rotated file fails closed", + "every asset is sha256-pinned; a corrupt or rotated file fails closed", dim(), ))); } Stage::Export => { for sub in &app.export_subs { - let icon = if sub.done { - Span::styled("✔", fg(Color::Green)) - } else { - Span::styled(SPINNER[app.spin % SPINNER.len()], fg(Color::Yellow)) - }; - let mut spans = vec![ - Span::raw(" "), - icon, - Span::raw(format!(" {:<10}", sub.name)), - Span::styled(sub.ms.map(ms_str).unwrap_or_default(), dim()), - ]; - if sub.name == "quantize" && !sub.done { - if let Some((d, t)) = app.export_progress { - let ratio = if t > 0 { d as f64 / t as f64 } else { 0.0 }; - spans.push(Span::raw(" ")); - spans.push(Span::styled(bar(ratio, 22), fg(Color::Cyan))); - spans.push(Span::raw(format!(" {d}/{t}"))); - } - } - lines.push(Line::from(spans)); + let icon = if sub.done { "●" } else { "…" }; + kv( + &mut lines, + &format!("{icon} {}", sub.name), + sub.ms.map(ms_str).unwrap_or_default(), + ); } - let interesting = [ - "error", - "tolerance", - "weights_root", - "int8_params", - "source", - ]; + lines.push(Line::raw("")); for (k, v) in app .kvs .iter() - .filter_map(|(_, k, v)| interesting.contains(&k.as_str()).then_some((k, v))) + .filter(|(s, _, _)| s == "commit") + .map(|(_, k, v)| (k, v)) { - lines.push(Line::from(vec![ - Span::styled(format!(" {k:<14}"), dim()), - Span::raw(truncate(v, area.width as usize - 20)), - ])); + kv(&mut lines, k, format!("{}…", &v[..12.min(v.len())])); } } Stage::Load => { if let Some(r) = &app.report { - let d = &r.dims; - push_kv(&mut lines, "model", r.label.to_string()); - push_kv(&mut lines, "source", r.input_source.clone()); - push_kv( + kv( &mut lines, "config", format!( - "dim={}, history={}, heads={}, dim_head={}, mlp={}, depth={}", - d.d, d.s, d.h, d.dh, d.mlp, d.depth + "dim={} history={} heads={} mlp={}", + r.dims.d, r.dims.s, r.dims.h, r.dims.mlp ), ); - push_kv( + kv( &mut lines, "tensors", - format!( - "{} int8 weight matrices, {} committed tables", - r.weight_tensors, r.table_count - ), - ); - push_kv( - &mut lines, - "params", - format!( - "{} int8 weights ({} on the wire)", - commas(r.params), - bytes_human(r.model_bytes) - ), - ); - push_kv( - &mut lines, - "weights_root", - format!( - "{} {}", - hex16(&r.weights_root), - if r.weights_bound_to_export { - "(export-bound: verify rejects unless reproduced bit-for-bit)" - } else { - "(prover-computed)" - } - ), - ); - push_kv( - &mut lines, - "inputs", - format!("z[..6] {:?} action[..6] {:?}", r.z_in_head, r.a_in_head), + format!("{} int8 + {} tables", r.weight_tensors, r.table_count), ); - } else { - lines.push(Line::from(Span::styled(" parsing the bundle…", dim()))); + kv(&mut lines, "z[..6]", format!("{:?}", r.z_in_head)); + kv(&mut lines, "a[..6]", format!("{:?}", r.a_in_head)); + } + if let Some(src) = &app.dash.source { + lines.push(Line::raw("")); + lines.push(Line::from(Span::styled(format!("inputs: {src}"), dim()))); } } Stage::Infer => { if let Some(r) = &app.report { - push_kv( - &mut lines, - "graph", - format!("{} ops over the named-buffer block DAG", commas(r.ops)), - ); - push_kv( - &mut lines, - "compute", - format!( - "{} MAC, exact integer (no float, no GPU)", - macs_human(r.macs) - ), - ); - push_kv( - &mut lines, - "z_next", - format!("{:?} (predicted next-latent head)", r.z_out_head), - ); + kv(&mut lines, "ops", commas(r.ops)); + kv(&mut lines, "compute", format!("{} MAC", macs_human(r.macs))); + kv(&mut lines, "z_next", format!("{:?}", r.z_out_head)); lines.push(Line::raw("")); - let max = r.hist.first().map_or(1, |(_, n)| *n); - for (k, n) in &r.hist { - let w = ((*n as f64 / max as f64) * 30.0).max(1.0) as usize; - lines.push(Line::from(vec![ - Span::raw(format!(" {k:<16}")), - Span::styled("█".repeat(w), fg(Color::Cyan)), - Span::raw(format!(" {}", commas(*n))), - ])); + for (k, n) in r.hist.iter().take(8) { + kv(&mut lines, k, commas(*n)); } - } else { - lines.push(Line::from(Span::styled( - " running the exact integer forward pass…", - dim(), - ))); } } Stage::Commit => { if let Some(r) = &app.report { - push_kv( + kv( &mut lines, "witness", - format!( - "{} claimed op outputs ({})", - commas(r.witness_vals), - bytes_human(r.proof_bytes) - ), - ); - push_kv(&mut lines, "trace_root", hex16(&r.trace_root)); - push_kv(&mut lines, "model", hex16(&r.model_commitment)); - push_kv( - &mut lines, - "quantization", - hex16(&r.quantization_commitment), - ); - push_kv( - &mut lines, - "inputs", - format!( - "{} (machine-local: ViT floats differ per host)", - hex16(&r.input_commitment) - ), + format!("{} vals", commas(r.witness_vals)), ); - push_kv(&mut lines, "output", hex16(&r.output_commitment)); + kv(&mut lines, "size", bytes_human(r.proof_bytes)); + kv(&mut lines, "trace", hex12(&r.trace_root)); + kv(&mut lines, "model", hex12(&r.model_commitment)); + kv(&mut lines, "quant", hex12(&r.quantization_commitment)); + kv(&mut lines, "inputs", hex12(&r.input_commitment)); + kv(&mut lines, "output", hex12(&r.output_commitment)); lines.push(Line::raw("")); lines.push(Line::from(Span::styled( - " absorbed public input + inputs + trace, then squeezed the Freivalds r (non-adaptive)", + "the transcript absorbs public input + inputs + trace, then squeezes the Freivalds challenge", dim(), ))); - } else { - lines.push(Line::from(Span::styled(" binding the transcript…", dim()))); } } Stage::Verify => { if let Some(r) = &app.report { - push_kv( + kv(&mut lines, "challenge", format!("{} linears", r.linear_ops)); + kv(&mut lines, "checks", "v·x == r·z per linear".to_string()); + kv( &mut lines, - "binding", - "recomputed model, quantization, planner, input + output commitments" - .to_string(), - ); - push_kv( - &mut lines, - "challenge", - format!( - "derived the Freivalds r for {} linear projections", - r.linear_ops - ), - ); - push_kv( - &mut lines, - "checks", - "Freivalds v·x == r·z; exact recompute of attention, softmax, GELU, LayerNorm" - .to_string(), + "recompute", + "attention, softmax, GELU, LayerNorm".to_string(), ); if let (Some(e), Some(t)) = (r.float_error, r.float_tolerance) { - push_kv( - &mut lines, - "faith", - format!("max |int - float| {e:.6} <= {t:.6} (bundle reference)"), - ); + kv(&mut lines, "float", format!("{e:.6} ≤ {t:.6}")); } lines.push(Line::raw("")); match &r.verify_err { None => lines.push(Line::from(vec![ - Span::raw(" verdict "), - Span::styled("ACCEPT", bold(Color::Green)), + Span::styled(" ACCEPT ", bold(MINT).add_modifier(Modifier::REVERSED)), Span::styled( - format!(" in {}", ms_str(r.verify.as_secs_f64() * 1000.0)), + format!(" {}", ms_str(r.verify.as_secs_f64() * 1000.0)), dim(), ), ])), Some(e) => lines.push(Line::from(vec![ - Span::raw(" verdict "), - Span::styled("REJECT", bold(Color::Red)), - Span::raw(format!(" {e}")), + Span::styled(" REJECT ", bold(CORAL).add_modifier(Modifier::REVERSED)), + Span::styled(format!(" {e}"), body()), ])), } - } else { - lines.push(Line::from(Span::styled(" auditing…", dim()))); } } Stage::Tamper => { if let Some(r) = &app.report { match (&r.forged_op, &r.reject) { (Some(op), Some(e)) => { - push_kv( - &mut lines, - "forged", - format!("output of matmul op {op} (a fake projection result)"), - ); + kv(&mut lines, "forged", format!("matmul op {op} output")); + lines.push(Line::raw("")); lines.push(Line::from(vec![ - Span::raw(" verdict "), - Span::styled("REJECT", bold(Color::Green)), - Span::raw(format!(" {e} ")), - Span::styled("(caught)", fg(Color::Green)), + Span::styled(" REJECT ", bold(MINT).add_modifier(Modifier::REVERSED)), + Span::styled(format!(" {e}"), body()), ])); + lines.push(Line::from(Span::styled("the forgery is caught", dim()))); } - _ => lines.push(Line::from(vec![ - Span::raw(" "), - Span::styled("tamper went undetected (this is a bug)", bold(Color::Red)), - ])), + _ => lines.push(Line::from(Span::styled( + "tamper went undetected (this is a bug)", + bold(CORAL), + ))), } - } else { - lines.push(Line::from(Span::styled( - " forging one matmul output…", - dim(), - ))); } } } if let StageState::Failed(e) = &app.stages[stage.idx()] { lines.push(Line::raw("")); lines.push(Line::from(vec![ - Span::styled(" error ", bold(Color::Red)), - Span::raw(e.clone()), + Span::styled("error ", bold(CORAL)), + Span::styled(e.clone(), body()), ])); } if let StageState::Skipped(why) = &app.stages[stage.idx()] { + lines.push(Line::raw("")); lines.push(Line::from(vec![ - Span::styled(" skipped ", fg(Color::Cyan)), - Span::raw(why.clone()), + Span::styled("skipped ", fg(SKY)), + Span::styled(why.clone(), body()), ])); } let p = Paragraph::new(lines) - .block(panel(stage.label(), stage.blurb())) + .block(block) .wrap(Wrap { trim: false }); f.render_widget(p, area); } fn render_logs(f: &mut Frame<'_>, app: &App, area: Rect) { + let block = Block::default() + .title(Line::from(vec![ + Span::styled(" ◇ ", fg(PINK)), + Span::styled("LOG ", bold(LAVENDER)), + Span::styled("l closes", dim()), + ])) + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(dim()) + .padding(Padding::new(1, 1, 0, 0)); let take = area.height.saturating_sub(2) as usize; let lines: Vec> = app .logs @@ -498,87 +763,42 @@ fn render_logs(f: &mut Frame<'_>, app: &App, area: Rect) { .rev() .map(|l| Line::from(Span::styled(l.clone(), dim()))) .collect(); - let p = - Paragraph::new(lines).block(panel("LOG", "raw exporter + pipeline output (l to close)")); - f.render_widget(p, area); + f.render_widget(Paragraph::new(lines).block(block), area); } fn render_footer(f: &mut Frame<'_>, app: &App, area: Rect) { - let block = Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(dim()); - let metrics = app.report.as_ref().map_or_else( - || Line::from(Span::styled("metrics pending…", dim())), - |r| { - let verdict = if r.accepted() && r.reject.is_some() { - Span::styled("ACCEPT", bold(Color::Green)) - } else { - Span::styled("REJECT", bold(Color::Red)) - }; - Line::from(vec![ - Span::styled("metrics ", bold(Color::Magenta)), - Span::raw(format!( - "infer {} · verify {} · {} int8 model · {} MAC · {} ops · ", - ms_str(r.infer.as_secs_f64() * 1000.0), - ms_str(r.verify.as_secs_f64() * 1000.0), - bytes_human(r.model_bytes), - macs_human(r.macs), - commas(r.ops), - )), - verdict, - ]) - }, - ); - let keys = Line::from(Span::styled( - " q quit · l logs · ↑/↓ select stage · esc follow active stage", - dim(), - )); - f.render_widget(Paragraph::new(vec![metrics, keys]).block(block), area); -} - -fn push_kv(lines: &mut Vec>, key: &str, value: String) { - lines.push(Line::from(vec![ - Span::styled( - format!(" {key:<14}"), - Style::default().add_modifier(Modifier::DIM), - ), - Span::raw(value), - ])); -} - -fn commas(n: usize) -> String { - let s = n.to_string(); - let b = s.as_bytes(); - let mut out = String::new(); - for (i, c) in b.iter().enumerate() { - if i > 0 && (b.len() - i) % 3 == 0 { - out.push(','); + let status = match app.finished { + Some(_) if app.ok() => { + Span::styled(" ACCEPT ", bold(MINT).add_modifier(Modifier::REVERSED)) } - out.push(*c as char); - } - out -} - -fn truncate(s: &str, max: usize) -> String { - if s.chars().count() <= max { - s.to_string() - } else { - let cut: String = s.chars().take(max.saturating_sub(1)).collect(); - format!("{cut}…") - } + Some(_) => Span::styled(" FAILED ", bold(CORAL).add_modifier(Modifier::REVERSED)), + None => Span::styled( + format!( + " {} {:.0}s ", + SPINNER[app.spin % SPINNER.len()], + app.elapsed_secs() + ), + bold(GOLD), + ), + }; + let line = Line::from(vec![ + Span::raw(" "), + status, + Span::styled(" ←/→ stage · esc follow · l log · q quit", dim()), + ]); + f.render_widget(Paragraph::new(line).alignment(Alignment::Left), area); } #[cfg(test)] mod tests { use super::*; use crate::app::App; - use crate::pipeline::{Event, Stage, StageState}; + use crate::pipeline::Event; use ratatui::backend::TestBackend; use ratatui::Terminal; - fn draw(app: &App) -> String { - let mut terminal = Terminal::new(TestBackend::new(100, 30)).unwrap(); + fn draw(app: &App, w: u16, h: u16) -> String { + let mut terminal = Terminal::new(TestBackend::new(w, h)).unwrap(); terminal.draw(|f| render(f, app)).unwrap(); let buffer = terminal.backend().buffer().clone(); let mut out = String::new(); @@ -591,8 +811,52 @@ mod tests { out } + fn report() -> pwm_testkit::report::PredictorReport { + pwm_testkit::report::PredictorReport { + label: "le-wm V0 predictor", + is_real: true, + input_source: "lerobot/pusht episode".into(), + dims: pwm_testkit::lewm_predictor::Dims { + d: 192, + s: 3, + h: 16, + dh: 64, + mlp: 2048, + depth: 6, + }, + inner: 1024, + weight_tensors: 30, + table_count: 4, + params: 10_764_288, + model_bytes: 10_764_288, + ops: 2437, + linear_ops: 30, + witness_vals: 689_760, + proof_bytes: 5_518_080, + macs: 32_403_456, + hist: vec![("slice", 1389), ("concat", 373), ("requant", 234)], + weights_bound_to_export: true, + weights_root: [0x37; 32], + model_commitment: [0x59; 32], + quantization_commitment: [0xa6; 32], + input_commitment: [0xd1; 32], + output_commitment: [0x18; 32], + trace_root: [0xab; 32], + infer: std::time::Duration::from_millis(40), + verify: std::time::Duration::from_millis(390), + z_out_head: vec![0, 7, -40, -11, -10, 3], + float_error: Some(0.282_924), + float_tolerance: Some(0.282_925), + z_in_head: vec![3, 14, 8], + a_in_head: vec![-18, 27, -20], + verify_err: None, + forged_op: Some(2), + reject: Some("FreivaldsCheckFailed { op_id: 2 }".into()), + } + } + #[test] - fn renders_the_rail_header_and_fetch_detail() { + fn renders_header_flow_and_fetch_activity() { let mut app = App::new("/tmp/cache".into()); app.handle(Event::Stage(Stage::Fetch, StageState::Running)); app.handle(Event::FetchProgress { @@ -601,41 +865,177 @@ mod tests { total: 72_290_721, outcome: None, }); - let s = draw(&app); + let s = draw(&app, 120, 34); assert!(s.contains("ProvableWorldModel"), "{s}"); - assert!(s.contains("PIPELINE"), "{s}"); assert!(s.contains("FETCH"), "{s}"); + assert!(s.contains("TAMPER"), "{s}"); + assert!(s.contains("OVERVIEW"), "{s}"); assert!(s.contains("weights.pt"), "{s}"); assert!(s.contains("34.5 MiB"), "{s}"); assert!(s.contains("q quit"), "{s}"); + assert!(!s.contains("docker"), "no meta fluff: {s}"); + assert!(!s.contains("REAL"), "no meta fluff: {s}"); } #[test] - fn renders_skipped_export_and_log_pane() { + fn dashboard_shows_kpis_op_mix_and_commitments_after_report() { let mut app = App::new("/tmp/cache".into()); - app.handle(Event::Stage( - Stage::Export, - StageState::Skipped("bundle cache hit (pure Rust from here)".into()), - )); - app.handle(Event::Log("export skipped: /x/lewm_predictor.json".into())); - let s = draw(&app); - assert!(s.contains("bundle cache hit"), "{s}"); - app.show_logs = true; - let s = draw(&app); - assert!(s.contains("export skipped"), "{s}"); + app.handle(Event::Report(Box::new(report()))); + app.handle(Event::Finished { ok: true }); + let s = draw(&app, 120, 38); + assert!(s.contains("ACCEPT"), "{s}"); + assert!(s.contains("TAMPER CAUGHT"), "{s}"); + assert!(s.contains("10,764,288"), "{s}"); + assert!(s.contains("2,437 ops"), "{s}"); + assert!(s.contains("op mix"), "{s}"); + assert!(s.contains("slice"), "{s}"); + assert!(s.contains("373737373737"), "weights root hex: {s}"); + assert!(s.contains("export-bound"), "{s}"); + assert!(s.contains("0.282924"), "{s}"); + } + + #[test] + fn narrow_terminal_drops_the_side_pane_but_keeps_the_overview() { + let mut app = App::new("/tmp/cache".into()); + app.handle(Event::Report(Box::new(report()))); + app.handle(Event::Finished { ok: true }); + let s = draw(&app, 80, 30); + assert!(s.contains("OVERVIEW"), "{s}"); + assert!(s.contains("ACCEPT"), "{s}"); } #[test] - fn renders_failure_state_in_detail() { + fn export_activity_shows_progress_and_live_kpis() { + let mut app = App::new("/tmp/cache".into()); + app.handle(Event::Stage(Stage::Fetch, StageState::Done(150.0))); + app.handle(Event::Stage(Stage::Export, StageState::Running)); + app.handle(Event::ExportSub { + name: "quantize".into(), + state: "start".into(), + ms: None, + }); + app.handle(Event::ExportProgress { + done: 24, + total: 34, + }); + app.handle(Event::Kv { + stage: "calibrate".into(), + key: "error".into(), + value: "0.282924".into(), + }); + app.handle(Event::Kv { + stage: "calibrate".into(), + key: "tolerance".into(), + value: "0.282925".into(), + }); + let s = draw(&app, 120, 34); + assert!(s.contains("quantize"), "{s}"); + assert!(s.contains("24/34"), "{s}"); + assert!(s.contains("0.282924"), "{s}"); + } + + #[test] + fn failure_renders_in_banner_and_footer() { let mut app = App::new("/tmp/cache".into()); app.handle(Event::Stage( Stage::Fetch, - StageState::Failed("--offline: weights.pt is not in the cache".into()), + StageState::Failed("weights.pt is not in the cache".into()), )); app.handle(Event::Finished { ok: false }); - let s = draw(&app); - assert!(s.contains("error"), "{s}"); - assert!(s.contains("--offline"), "{s}"); + let s = draw(&app, 120, 34); assert!(s.contains("FAILED"), "{s}"); + assert!(s.contains("not in the cache"), "{s}"); + } + + #[test] + fn dump_frames_for_eyeball() { + let mut app = App::new("~/Library/Caches/ProvableWorldModel".into()); + app.handle(Event::Stage(Stage::Fetch, StageState::Running)); + app.handle(Event::FetchProgress { + idx: 0, + done: 36_145_360, + total: 72_290_721, + outcome: None, + }); + app.handle(Event::FetchProgress { + idx: 1, + done: 674_393, + total: 674_393, + outcome: Some("cache HIT"), + }); + for v in [2u64, 5, 9, 14, 11, 18, 25, 22, 30, 28, 35, 31] { + app.net_history.push(v * 100_000); + } + println!("=== FETCH ===\n{}", draw(&app, 118, 34)); + app.handle(Event::Stage(Stage::Fetch, StageState::Done(769.0))); + app.handle(Event::Stage(Stage::Export, StageState::Running)); + for name in ["load", "extract", "quantize"] { + app.handle(Event::ExportSub { + name: name.into(), + state: "start".into(), + ms: None, + }); + } + app.handle(Event::ExportSub { + name: "load".into(), + state: "done".into(), + ms: Some(69.0), + }); + app.handle(Event::ExportSub { + name: "extract".into(), + state: "done".into(), + ms: Some(1.0), + }); + app.handle(Event::ExportProgress { + done: 24, + total: 34, + }); + app.handle(Event::Kv { + stage: "load".into(), + key: "checkpoint_bytes".into(), + value: "72290721".into(), + }); + app.handle(Event::Kv { + stage: "load".into(), + key: "tensors".into(), + value: "303".into(), + }); + app.handle(Event::Kv { + stage: "quantize".into(), + key: "int8_params".into(), + value: "11705856".into(), + }); + app.handle(Event::Log( + " [QUANTIZE] every linear to int8 with per-tensor power-of-two scales".into(), + )); + println!("=== EXPORT ===\n{}", draw(&app, 118, 34)); + app.handle(Event::Stage(Stage::Export, StageState::Done(21988.0))); + for (s, st) in [ + (Stage::Load, StageState::Done(243.0)), + (Stage::Infer, StageState::Done(40.0)), + (Stage::Commit, StageState::Done(0.0)), + (Stage::Verify, StageState::Done(390.0)), + (Stage::Tamper, StageState::Done(389.0)), + ] { + app.handle(Event::Stage(s, st)); + } + app.handle(Event::Kv { + stage: "bundle".into(), + key: "predictor_bundle_bytes".into(), + value: "45613056".into(), + }); + app.handle(Event::Report(Box::new(report()))); + app.handle(Event::Finished { ok: true }); + println!("=== DONE ===\n{}", draw(&app, 118, 36)); + } + + #[test] + fn log_pane_toggles() { + let mut app = App::new("/tmp/cache".into()); + app.handle(Event::Log("export skipped: bundle cache hit".into())); + app.show_logs = true; + let s = draw(&app, 120, 34); + assert!(s.contains("LOG"), "{s}"); + assert!(s.contains("bundle cache hit"), "{s}"); } }