use crate::design_choice::{ChoiceId, DesignChoiceSet};
use crate::{SuggestId, SuggestLocation, SuggestOpportunity};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub enum VerificationStatus {
#[default]
Pending,
LightCheck,
FullyVerified,
Failed {
errors: Vec<String>,
},
Skipped,
}
impl VerificationStatus {
pub fn is_passed(&self) -> bool {
matches!(self, Self::LightCheck | Self::FullyVerified)
}
pub fn is_failed(&self) -> bool {
matches!(self, Self::Failed { .. })
}
pub fn is_fully_verified(&self) -> bool {
matches!(self, Self::FullyVerified)
}
pub fn errors(&self) -> Option<&[String]> {
match self {
Self::Failed { errors } => Some(errors),
_ => None,
}
}
}
impl fmt::Display for VerificationStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Pending => write!(f, "pending"),
Self::LightCheck => write!(f, "light-check"),
Self::FullyVerified => write!(f, "verified"),
Self::Failed { errors } => {
write!(f, "failed ({} errors)", errors.len())
}
Self::Skipped => write!(f, "skipped"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerifiedCandidate {
pub choice_id: ChoiceId,
pub verification: VerificationStatus,
pub diff_summary: String,
pub confidence: f32,
}
impl VerifiedCandidate {
pub fn new(choice_id: impl Into<ChoiceId>, confidence: f32) -> Self {
Self {
choice_id: choice_id.into(),
verification: VerificationStatus::Pending,
diff_summary: String::new(),
confidence: confidence.clamp(0.0, 1.0),
}
}
pub fn with_verification(mut self, status: VerificationStatus) -> Self {
self.verification = status;
self
}
pub fn with_diff_summary(mut self, summary: impl Into<String>) -> Self {
self.diff_summary = summary.into();
self
}
pub fn is_verified(&self) -> bool {
self.verification.is_passed()
}
pub fn is_fully_verified(&self) -> bool {
self.verification.is_fully_verified()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ApplyCommands {
pub preview: String,
pub apply: String,
pub apply_verified: String,
}
impl ApplyCommands {
pub fn for_suggestion(id: &SuggestId) -> Self {
let id_str = id.to_string();
Self {
preview: format!("ryo suggest apply {} --dry-run", id_str),
apply: format!("ryo suggest apply {} -e", id_str),
apply_verified: format!("ryo suggest apply {} -e --verify", id_str),
}
}
pub fn for_choice(suggestion_id: &SuggestId, choice_id: &ChoiceId) -> Self {
let suggest_str = suggestion_id.to_string();
let choice_str = choice_id.as_str();
Self {
preview: format!(
"ryo suggest apply {} --choice {} --dry-run",
suggest_str, choice_str
),
apply: format!(
"ryo suggest apply {} --choice {} -e",
suggest_str, choice_str
),
apply_verified: format!(
"ryo suggest apply {} --choice {} -e --verify",
suggest_str, choice_str
),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnhancedSuggestion {
pub id: SuggestId,
pub title: String,
pub location: SuggestLocation,
pub description: String,
pub design_choices: Option<DesignChoiceSet>,
pub verified_candidates: Vec<VerifiedCandidate>,
pub apply_commands: ApplyCommands,
pub original_confidence: f32,
}
impl EnhancedSuggestion {
pub fn from_opportunity(opportunity: &SuggestOpportunity, id: SuggestId) -> Self {
Self {
id,
title: opportunity.message.clone(),
location: opportunity.location.clone(),
description: String::new(),
design_choices: None,
verified_candidates: Vec::new(),
apply_commands: ApplyCommands::for_suggestion(&id),
original_confidence: opportunity.confidence,
}
}
pub fn with_design_choices(mut self, choices: DesignChoiceSet) -> Self {
self.design_choices = Some(choices);
self
}
pub fn add_verified_candidate(mut self, candidate: VerifiedCandidate) -> Self {
self.verified_candidates.push(candidate);
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
pub fn has_choices(&self) -> bool {
self.design_choices
.as_ref()
.map(|c| c.has_alternatives())
.unwrap_or(false)
}
pub fn verified_count(&self) -> usize {
self.verified_candidates
.iter()
.filter(|c| c.is_verified())
.count()
}
pub fn best_candidate(&self) -> Option<&VerifiedCandidate> {
self.verified_candidates
.iter()
.filter(|c| c.is_verified())
.max_by(|a, b| {
a.confidence
.partial_cmp(&b.confidence)
.unwrap_or(std::cmp::Ordering::Equal)
})
}
pub fn has_fully_verified(&self) -> bool {
self.verified_candidates
.iter()
.any(|c| c.is_fully_verified())
}
pub fn fully_verified_candidates(&self) -> Vec<&VerifiedCandidate> {
self.verified_candidates
.iter()
.filter(|c| c.is_fully_verified())
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{OpportunityContext, OpportunityId, SuggestIdGenerator};
fn create_test_opportunity() -> (SuggestOpportunity, SuggestId) {
let mut gen = SuggestIdGenerator::new();
let id = gen.next_id();
let opportunity = SuggestOpportunity::new(
OpportunityId::new(1),
vec![],
SuggestLocation::for_test("src/lib.rs", "MyStruct"),
"Add #[derive(Default)] to MyStruct",
0.95,
OpportunityContext::Derive {
derive_name: "Default".to_string(),
missing_impls: vec![],
},
);
(opportunity, id)
}
#[test]
fn test_verification_status() {
assert!(VerificationStatus::LightCheck.is_passed());
assert!(VerificationStatus::FullyVerified.is_passed());
assert!(!VerificationStatus::Pending.is_passed());
assert!(!VerificationStatus::Failed { errors: vec![] }.is_passed());
assert!(VerificationStatus::FullyVerified.is_fully_verified());
assert!(!VerificationStatus::LightCheck.is_fully_verified());
let failed = VerificationStatus::Failed {
errors: vec!["error 1".to_string()],
};
assert!(failed.is_failed());
assert_eq!(failed.errors().unwrap().len(), 1);
}
#[test]
fn test_verification_status_display() {
assert_eq!(VerificationStatus::Pending.to_string(), "pending");
assert_eq!(VerificationStatus::LightCheck.to_string(), "light-check");
assert_eq!(VerificationStatus::FullyVerified.to_string(), "verified");
assert_eq!(VerificationStatus::Skipped.to_string(), "skipped");
let failed = VerificationStatus::Failed {
errors: vec!["e1".to_string(), "e2".to_string()],
};
assert_eq!(failed.to_string(), "failed (2 errors)");
}
#[test]
fn test_verified_candidate() {
let candidate = VerifiedCandidate::new("A", 0.9)
.with_verification(VerificationStatus::FullyVerified)
.with_diff_summary("2 files, +20/-5 lines");
assert!(candidate.is_verified());
assert!(candidate.is_fully_verified());
assert_eq!(candidate.diff_summary, "2 files, +20/-5 lines");
assert_eq!(candidate.confidence, 0.9);
}
#[test]
fn test_apply_commands() {
let mut gen = SuggestIdGenerator::new();
let id = gen.next_id();
let commands = ApplyCommands::for_suggestion(&id);
assert!(commands.preview.contains("--dry-run"));
assert!(commands.apply.contains("-e"));
assert!(commands.apply_verified.contains("--verify"));
let choice_commands = ApplyCommands::for_choice(&id, &ChoiceId::new("B"));
assert!(choice_commands.apply.contains("--choice B"));
}
#[test]
fn test_enhanced_suggestion_from_opportunity() {
let (opportunity, id) = create_test_opportunity();
let enhanced = EnhancedSuggestion::from_opportunity(&opportunity, id);
assert_eq!(enhanced.title, opportunity.message);
assert_eq!(enhanced.location, opportunity.location);
assert_eq!(enhanced.original_confidence, opportunity.confidence);
assert!(!enhanced.has_choices());
assert_eq!(enhanced.verified_count(), 0);
}
#[test]
fn test_enhanced_suggestion_with_candidates() {
let (opportunity, id) = create_test_opportunity();
let enhanced = EnhancedSuggestion::from_opportunity(&opportunity, id)
.add_verified_candidate(
VerifiedCandidate::new("A", 0.9)
.with_verification(VerificationStatus::FullyVerified),
)
.add_verified_candidate(
VerifiedCandidate::new("B", 0.7).with_verification(VerificationStatus::LightCheck),
)
.add_verified_candidate(VerifiedCandidate::new("C", 0.8).with_verification(
VerificationStatus::Failed {
errors: vec!["type mismatch".to_string()],
},
));
assert_eq!(enhanced.verified_count(), 2); assert!(enhanced.has_fully_verified());
assert_eq!(enhanced.fully_verified_candidates().len(), 1);
let best = enhanced.best_candidate().unwrap();
assert_eq!(best.choice_id.as_str(), "A"); }
#[test]
fn test_enhanced_suggestion_serde() {
let (opportunity, id) = create_test_opportunity();
let enhanced = EnhancedSuggestion::from_opportunity(&opportunity, id)
.with_description("Test description")
.add_verified_candidate(
VerifiedCandidate::new("A", 0.9)
.with_verification(VerificationStatus::FullyVerified),
);
let json = serde_json::to_string(&enhanced).unwrap();
let parsed: EnhancedSuggestion = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.description, "Test description");
assert_eq!(parsed.verified_candidates.len(), 1);
}
}