apr-cli 0.4.13

CLI tool for APR model inspection, debugging, and operations
Documentation

/// Check CI thresholds
fn check_ci_thresholds(report: &HeadlessReport, config: &CbtopConfig) -> bool {
    let mut passed = true;

    if let Some(threshold) = config.throughput_threshold {
        if report.throughput.tokens_per_sec < threshold {
            eprintln!(
                "cbtop: FAIL - Throughput {:.1} tok/s < threshold {:.1} tok/s",
                report.throughput.tokens_per_sec, threshold
            );
            passed = false;
        } else {
            eprintln!(
                "cbtop: PASS - Throughput {:.1} tok/s >= threshold {:.1} tok/s",
                report.throughput.tokens_per_sec, threshold
            );
        }
    }

    if let Some(threshold) = config.brick_score_threshold {
        let avg_score = if report.brick_scores.is_empty() {
            0
        } else {
            report.brick_scores.iter().map(|b| b.score).sum::<u32>()
                / report.brick_scores.len() as u32
        };
        if avg_score < threshold {
            eprintln!(
                "cbtop: FAIL - Brick score {} < threshold {}",
                avg_score, threshold
            );
            passed = false;
        } else {
            eprintln!(
                "cbtop: PASS - Brick score {} >= threshold {}",
                avg_score, threshold
            );
        }
    }

    passed
}

/// Format report as JSON
fn format_report_as_json(report: &HeadlessReport) -> String {
    // Manual JSON formatting to avoid serde dependency in core path
    let brick_scores_json: String = report
        .brick_scores
        .iter()
        .map(|b| {
            format!(
                r#"    {{
      "name": "{}",
      "score": {},
      "grade": "{}",
      "budget_us": {:.2},
      "actual_us": {:.2},
      "gap_factor": {:.3}
    }}"#,
                b.name, b.score, b.grade, b.budget_us, b.actual_us, b.gap_factor
            )
        })
        .collect::<Vec<_>>()
        .join(",\n");

    format!(
        r#"{{
  "model": "{}",
  "timestamp": "{}",
  "hardware": {{
    "gpu": "{}",
    "cpu": "{}",
    "memory_gb": {}
  }},
  "throughput": {{
    "tokens_per_sec": {:.2},
    "ttft_ms": {:.2},
    "cv_percent": {:.2},
    "p50_us": {:.2},
    "p99_us": {:.2}
  }},
  "brick_scores": [
{}
  ],
  "pmat_scores": {{
    "rust_project_score": {:.1},
    "tdg_score": {:.1},
    "cuda_tdg_score": {:.1},
    "brick_score": {},
    "grade": "{}"
  }},
  "falsification": {{
    "total_points": {},
    "passed": {},
    "failed": {},
    "blocked": {}
  }},
  "status": "{}",
  "ci_result": "{}"
}}"#,
        report.model,
        report.timestamp,
        report.hardware.gpu,
        report.hardware.cpu,
        report.hardware.memory_gb,
        report.throughput.tokens_per_sec,
        report.throughput.ttft_ms,
        report.throughput.cv_percent,
        report.throughput.p50_us,
        report.throughput.p99_us,
        brick_scores_json,
        report.pmat_scores.rust_project_score,
        report.pmat_scores.tdg_score,
        report.pmat_scores.cuda_tdg_score,
        report.pmat_scores.brick_score,
        report.pmat_scores.grade,
        report.falsification.total_points,
        report.falsification.passed,
        report.falsification.failed,
        report.falsification.blocked,
        report.status,
        report.ci_result,
    )
}

/// Print report as plain text
fn print_report_text(report: &HeadlessReport) {
    println!("═══════════════════════════════════════════════════════════════");
    println!("  cbtop Headless Benchmark Report");
    println!("═══════════════════════════════════════════════════════════════");
    println!("  Model:     {}", report.model);
    println!("  Timestamp: {}", report.timestamp);
    println!();
    println!(
        "  Throughput: {:.1} tok/s",
        report.throughput.tokens_per_sec
    );
    println!("  TTFT:       {:.2} ms", report.throughput.ttft_ms);
    println!("  CV:         {:.2}%", report.throughput.cv_percent);
    println!();
    println!("  Brick Scores:");
    for brick in &report.brick_scores {
        let status = if brick.gap_factor <= 1.0 {
            ""
        } else {
            ""
        };
        println!(
            "    {} {:12} {:>3} ({}) - {:.1}µs / {:.1}µs ({:.2}x)",
            status,
            brick.name,
            brick.score,
            brick.grade,
            brick.actual_us,
            brick.budget_us,
            brick.gap_factor
        );
    }
    println!();
    println!(
        "  Falsification: {}/{} passed",
        report.falsification.passed, report.falsification.total_points
    );
    println!("  Status: {} | CI: {}", report.status, report.ci_result);
    println!("═══════════════════════════════════════════════════════════════");
}

/// Run TUI mode (original behavior)
fn run_tui(model: Option<&str>, attach: Option<&str>) -> Result<()> {
    // GH-526: Warn that --attach is not yet implemented for TUI mode
    if attach.is_some() {
        eprintln!("Warning: --attach is not yet implemented for TUI mode. Flag ignored.");
    }

    // Setup terminal
    enable_raw_mode()
        .map_err(|e| CliError::ValidationFailed(format!("Failed to enable raw mode: {e}")))?;
    let mut stdout = io::stdout();
    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)
        .map_err(|e| CliError::ValidationFailed(format!("Failed to setup terminal: {e}")))?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)
        .map_err(|e| CliError::ValidationFailed(format!("Failed to create terminal: {e}")))?;

    // Create app and run
    let mut app = App::new(model);
    let res = run_app(&mut terminal, &mut app);

    // Restore terminal
    disable_raw_mode().ok();
    execute!(
        terminal.backend_mut(),
        LeaveAlternateScreen,
        DisableMouseCapture
    )
    .ok();
    terminal.show_cursor().ok();

    res
}

fn run_app<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Result<()> {
    loop {
        app.tick();
        terminal
            .draw(|f| ui(f, app))
            .map_err(|e| CliError::ValidationFailed(format!("Failed to draw: {e}")))?;

        if event::poll(std::time::Duration::from_millis(100))
            .map_err(|e| CliError::ValidationFailed(format!("Event poll error: {e}")))?
        {
            if let Event::Key(key) = event::read()
                .map_err(|e| CliError::ValidationFailed(format!("Event read error: {e}")))?
            {
                handle_cbtop_key(key, app);
            }
        }

        if app.should_quit {
            return Ok(());
        }
    }
}

fn handle_cbtop_key(key: crossterm::event::KeyEvent, app: &mut App) {
    if key.kind != KeyEventKind::Press {
        return;
    }
    match key.code {
        KeyCode::Char('q') | KeyCode::Esc => app.should_quit = true,
        KeyCode::Char('p') => app.current_view = View::Pipeline,
        KeyCode::Char('b') => app.current_view = View::Budget,
        KeyCode::Char('h') => app.current_view = View::Histogram,
        KeyCode::Char('g') => app.current_view = View::Gpu,
        KeyCode::Char('m') => app.current_view = View::Memory,
        KeyCode::Down | KeyCode::Char('j') => app.next_brick(),
        KeyCode::Up | KeyCode::Char('k') => app.prev_brick(),
        _ => {}
    }
}