met 1.0.0-beta-preview

Metrale Engine: pure Rust LLM inference server (HTTP API, scheduler, TUI, CLI).
// SPDX-License-Identifier: MIT OR Apache-2.0

//! 2026-09-26: Rendering of a certify campaign: the plan, per-unit outcome
//! and summary lines on stderr, and the JSON summary.
//!
//! Owner: server CLI (`met benchmark certify`).
//! Invariants: none beyond the types.

use super::plan::{self, Unit};
use super::runner::RunOutcome;
use super::state;

pub(in crate::cli::bench_certify) fn human(secs: u64) -> String {
    if secs >= 3600 {
        format!("{:.1} h", secs as f64 / 3600.0)
    } else if secs >= 60 {
        format!("{} min", secs / 60)
    } else {
        format!("{secs} s")
    }
}

pub(in crate::cli::bench_certify) fn describe(o: &RunOutcome) -> String {
    match o {
        RunOutcome::Passed { record } => format!("PASS ({})", record.display()),
        RunOutcome::MemberDone { record } => format!("shard done ({})", record.display()),
        RunOutcome::VerdictFail { reason, .. } => format!("FAIL — {reason}"),
        RunOutcome::Harness { reason, .. } => format!("harness — {reason}"),
        RunOutcome::TimedOut => "timed out".into(),
        RunOutcome::Cancelled => "cancelled".into(),
    }
}

pub(super) fn print_plan(
    anchor: &str,
    hardware: &str,
    guard_ref: Option<&str>,
    gates: &[&str],
    units: &[Unit],
    serial: u64,
) {
    eprintln!(
        "certify: anchor {anchor} · hardware {hardware} · guard {}",
        guard_ref.unwrap_or("(none)")
    );
    if gates.is_empty() {
        eprintln!("certify: every required gate already passes at {anchor}");
        return;
    }
    eprintln!(
        "certify: {} gate(s) open → {} unit(s), ~{} serial:",
        gates.len(),
        units.len(),
        human(serial)
    );
    for u in units {
        eprintln!(
            "  {:<28} {:>8}  {:?}{}",
            u.label(),
            human(u.secs()),
            u.class,
            match u.estimate {
                plan::Estimate::Declared(_) => "",
                plan::Estimate::Measured { .. } => "  (measured)",
            }
        );
    }
}

pub(super) fn print_summary(s: &state::Summary) {
    eprintln!();
    eprintln!(
        "certify: {} passed, {} shard(s) done, {} failed, {} skipped{}",
        s.passed.len(),
        s.member_done.len(),
        s.failed.len(),
        s.skipped.len(),
        s.aborted
            .as_deref()
            .map_or(String::new(), |a| format!(" — ABORTED: {a}"))
    );
    for (id, why) in &s.failed {
        eprintln!("  failed  {id}: {why}");
    }
}

#[derive(serde::Serialize)]
pub(super) struct SummaryJson {
    passed: Vec<String>,
    member_done: Vec<String>,
    failed: Vec<(String, String)>,
    skipped: Vec<(String, String)>,
    aborted: Option<String>,
}

impl From<&state::Summary> for SummaryJson {
    fn from(s: &state::Summary) -> Self {
        Self {
            passed: s.passed.clone(),
            member_done: s.member_done.clone(),
            failed: s.failed.clone(),
            skipped: s.skipped.clone(),
            aborted: s.aborted.clone(),
        }
    }
}