ai-dispatch 11.2.0

Multi-AI CLI team orchestrator
// Text leaderboard evidence alongside advised candidates.
// Exports: leaderboard_lines; no fetching or scoring.
// Deps: typed score evidence, advice payload and model labels.

use super::{AdviceReport, candidate_mark, unrated_served_line};

pub(super) fn leaderboard_lines(evidence: &crate::routing::scores::Evidence) -> Vec<String> {
    let mut lines = vec![format!(
        "     capability: {} ({})",
        evidence
            .capability
            .map(|value| format!("{value:.2}/10"))
            .unwrap_or_else(|| "unknown".into()),
        evidence.harness
    )];
    for score in &evidence.scores {
        lines.push(format!(
            "     {} / {}: {} {} rank {}/{} effort {} cli {} date {}",
            score.source,
            score.board,
            score.value,
            score.unit,
            score
                .rank
                .map(|rank| rank.to_string())
                .unwrap_or_else(|| "unknown".into()),
            score
                .of
                .map(|of| of.to_string())
                .unwrap_or_else(|| "unknown".into()),
            score.effort.as_deref().unwrap_or("unknown"),
            score.cli.as_deref().unwrap_or("model-level"),
            score.date
        ));
    }
    lines
}

pub(super) fn print_candidates(report: &AdviceReport) {
    for (index, candidate) in report.candidates.iter().enumerate() {
        println!("{}", candidate_line(index, candidate));
        for line in leaderboard_lines(&candidate.capability_evidence) {
            println!("{line}");
        }
        if let Some(line) = unrated_served_line(candidate) {
            println!("{line}");
        }
    }
    print_custom_candidates(report);
}

fn candidate_line(index: usize, candidate: &crate::agent::selection::AdviceCandidate) -> String {
    let availability = candidate_mark(candidate.exclusion_reason.as_deref()
        .or(candidate.demotion_reason.as_deref()));
    let item = &candidate.breakdown;
    let billing_term = candidate.billing.as_ref()
        .map(|_| format!("  {:+.1} billing", item.billing)).unwrap_or_default();
    let billing_state = candidate.billing.as_ref()
        .map(|billing| format!("  {}", billing.label())).unwrap_or_default();
    format!(
        concat!(
            "  {}. {:<10} {:>5.1}  base {:.1}  {:+.1} model  {:+.1} budget  {:+.1} limit",
            "  {:+.1} history  {:+.1} team  {:+.1} headroom{}  model {}{}{}"
        ),
        index + 1, candidate.agent, candidate.score, item.base, item.model_capability,
        item.budget_penalty, item.rate_limit_penalty, item.history_bonus, item.team_bonus,
        item.headroom_penalty, billing_term,
        crate::agent::run_model::model_label(candidate.model.as_deref(), candidate.pinned, candidate.source),
        availability, billing_state,
    )
}

fn print_custom_candidates(report: &AdviceReport) {
    if !report.custom_candidates.is_empty() {
        println!("Custom agents (separate capability scale):");
        for candidate in &report.custom_candidates {
            for line in leaderboard_lines(&candidate.capability_evidence) {
                println!("{line}");
            }
            let availability = candidate_mark(candidate.exclusion_reason.as_deref());
            let preference = if candidate.team_preferred {
                "  team preferred"
            } else {
                ""
            };
            println!(
                "  {:<20} capability {}  +{} strength{}{}",
                candidate.agent,
                candidate.category_capability,
                candidate.strength_bonus,
                preference,
                availability,
            );
        }
    }
}

#[cfg(test)]
#[path = "advise_leaderboard_tests.rs"]
mod tests;