1use crate::design_choice::{ChoiceId, DesignChoiceSet};
21use crate::{SuggestId, SuggestLocation, SuggestOpportunity};
22use serde::{Deserialize, Serialize};
23use std::fmt;
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
30pub enum VerificationStatus {
31 #[default]
33 Pending,
34
35 LightCheck,
38
39 FullyVerified,
42
43 Failed {
45 errors: Vec<String>,
47 },
48
49 Skipped,
51}
52
53impl VerificationStatus {
54 pub fn is_passed(&self) -> bool {
56 matches!(self, Self::LightCheck | Self::FullyVerified)
57 }
58
59 pub fn is_failed(&self) -> bool {
61 matches!(self, Self::Failed { .. })
62 }
63
64 pub fn is_fully_verified(&self) -> bool {
66 matches!(self, Self::FullyVerified)
67 }
68
69 pub fn errors(&self) -> Option<&[String]> {
71 match self {
72 Self::Failed { errors } => Some(errors),
73 _ => None,
74 }
75 }
76}
77
78impl fmt::Display for VerificationStatus {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 match self {
81 Self::Pending => write!(f, "pending"),
82 Self::LightCheck => write!(f, "light-check"),
83 Self::FullyVerified => write!(f, "verified"),
84 Self::Failed { errors } => {
85 write!(f, "failed ({} errors)", errors.len())
86 }
87 Self::Skipped => write!(f, "skipped"),
88 }
89 }
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct VerifiedCandidate {
98 pub choice_id: ChoiceId,
100
101 pub verification: VerificationStatus,
103
104 pub diff_summary: String,
106
107 pub confidence: f32,
110}
111
112impl VerifiedCandidate {
113 pub fn new(choice_id: impl Into<ChoiceId>, confidence: f32) -> Self {
115 Self {
116 choice_id: choice_id.into(),
117 verification: VerificationStatus::Pending,
118 diff_summary: String::new(),
119 confidence: confidence.clamp(0.0, 1.0),
120 }
121 }
122
123 pub fn with_verification(mut self, status: VerificationStatus) -> Self {
125 self.verification = status;
126 self
127 }
128
129 pub fn with_diff_summary(mut self, summary: impl Into<String>) -> Self {
131 self.diff_summary = summary.into();
132 self
133 }
134
135 pub fn is_verified(&self) -> bool {
137 self.verification.is_passed()
138 }
139
140 pub fn is_fully_verified(&self) -> bool {
142 self.verification.is_fully_verified()
143 }
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, Default)]
150pub struct ApplyCommands {
151 pub preview: String,
153
154 pub apply: String,
156
157 pub apply_verified: String,
159}
160
161impl ApplyCommands {
162 pub fn for_suggestion(id: &SuggestId) -> Self {
164 let id_str = id.to_string();
165 Self {
166 preview: format!("ryo suggest apply {} --dry-run", id_str),
167 apply: format!("ryo suggest apply {} -e", id_str),
168 apply_verified: format!("ryo suggest apply {} -e --verify", id_str),
169 }
170 }
171
172 pub fn for_choice(suggestion_id: &SuggestId, choice_id: &ChoiceId) -> Self {
174 let suggest_str = suggestion_id.to_string();
175 let choice_str = choice_id.as_str();
176 Self {
177 preview: format!(
178 "ryo suggest apply {} --choice {} --dry-run",
179 suggest_str, choice_str
180 ),
181 apply: format!(
182 "ryo suggest apply {} --choice {} -e",
183 suggest_str, choice_str
184 ),
185 apply_verified: format!(
186 "ryo suggest apply {} --choice {} -e --verify",
187 suggest_str, choice_str
188 ),
189 }
190 }
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct EnhancedSuggestion {
201 pub id: SuggestId,
203
204 pub title: String,
206
207 pub location: SuggestLocation,
209
210 pub description: String,
212
213 pub design_choices: Option<DesignChoiceSet>,
215
216 pub verified_candidates: Vec<VerifiedCandidate>,
218
219 pub apply_commands: ApplyCommands,
221
222 pub original_confidence: f32,
224}
225
226impl EnhancedSuggestion {
227 pub fn from_opportunity(opportunity: &SuggestOpportunity, id: SuggestId) -> Self {
229 Self {
230 id,
231 title: opportunity.message.clone(),
232 location: opportunity.location.clone(),
233 description: String::new(),
234 design_choices: None,
235 verified_candidates: Vec::new(),
236 apply_commands: ApplyCommands::for_suggestion(&id),
237 original_confidence: opportunity.confidence,
238 }
239 }
240
241 pub fn with_design_choices(mut self, choices: DesignChoiceSet) -> Self {
243 self.design_choices = Some(choices);
244 self
245 }
246
247 pub fn add_verified_candidate(mut self, candidate: VerifiedCandidate) -> Self {
249 self.verified_candidates.push(candidate);
250 self
251 }
252
253 pub fn with_description(mut self, description: impl Into<String>) -> Self {
255 self.description = description.into();
256 self
257 }
258
259 pub fn has_choices(&self) -> bool {
261 self.design_choices
262 .as_ref()
263 .map(|c| c.has_alternatives())
264 .unwrap_or(false)
265 }
266
267 pub fn verified_count(&self) -> usize {
269 self.verified_candidates
270 .iter()
271 .filter(|c| c.is_verified())
272 .count()
273 }
274
275 pub fn best_candidate(&self) -> Option<&VerifiedCandidate> {
277 self.verified_candidates
278 .iter()
279 .filter(|c| c.is_verified())
280 .max_by(|a, b| {
281 a.confidence
282 .partial_cmp(&b.confidence)
283 .unwrap_or(std::cmp::Ordering::Equal)
284 })
285 }
286
287 pub fn has_fully_verified(&self) -> bool {
289 self.verified_candidates
290 .iter()
291 .any(|c| c.is_fully_verified())
292 }
293
294 pub fn fully_verified_candidates(&self) -> Vec<&VerifiedCandidate> {
296 self.verified_candidates
297 .iter()
298 .filter(|c| c.is_fully_verified())
299 .collect()
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306 use crate::{OpportunityContext, OpportunityId, SuggestIdGenerator};
307
308 fn create_test_opportunity() -> (SuggestOpportunity, SuggestId) {
309 let mut gen = SuggestIdGenerator::new();
310 let id = gen.next_id();
311
312 let opportunity = SuggestOpportunity::new(
313 OpportunityId::new(1),
314 vec![],
315 SuggestLocation::for_test("src/lib.rs", "MyStruct"),
316 "Add #[derive(Default)] to MyStruct",
317 0.95,
318 OpportunityContext::Derive {
319 derive_name: "Default".to_string(),
320 missing_impls: vec![],
321 },
322 );
323
324 (opportunity, id)
325 }
326
327 #[test]
328 fn test_verification_status() {
329 assert!(VerificationStatus::LightCheck.is_passed());
330 assert!(VerificationStatus::FullyVerified.is_passed());
331 assert!(!VerificationStatus::Pending.is_passed());
332 assert!(!VerificationStatus::Failed { errors: vec![] }.is_passed());
333
334 assert!(VerificationStatus::FullyVerified.is_fully_verified());
335 assert!(!VerificationStatus::LightCheck.is_fully_verified());
336
337 let failed = VerificationStatus::Failed {
338 errors: vec!["error 1".to_string()],
339 };
340 assert!(failed.is_failed());
341 assert_eq!(failed.errors().unwrap().len(), 1);
342 }
343
344 #[test]
345 fn test_verification_status_display() {
346 assert_eq!(VerificationStatus::Pending.to_string(), "pending");
347 assert_eq!(VerificationStatus::LightCheck.to_string(), "light-check");
348 assert_eq!(VerificationStatus::FullyVerified.to_string(), "verified");
349 assert_eq!(VerificationStatus::Skipped.to_string(), "skipped");
350
351 let failed = VerificationStatus::Failed {
352 errors: vec!["e1".to_string(), "e2".to_string()],
353 };
354 assert_eq!(failed.to_string(), "failed (2 errors)");
355 }
356
357 #[test]
358 fn test_verified_candidate() {
359 let candidate = VerifiedCandidate::new("A", 0.9)
360 .with_verification(VerificationStatus::FullyVerified)
361 .with_diff_summary("2 files, +20/-5 lines");
362
363 assert!(candidate.is_verified());
364 assert!(candidate.is_fully_verified());
365 assert_eq!(candidate.diff_summary, "2 files, +20/-5 lines");
366 assert_eq!(candidate.confidence, 0.9);
367 }
368
369 #[test]
370 fn test_apply_commands() {
371 let mut gen = SuggestIdGenerator::new();
372 let id = gen.next_id();
373
374 let commands = ApplyCommands::for_suggestion(&id);
375 assert!(commands.preview.contains("--dry-run"));
376 assert!(commands.apply.contains("-e"));
377 assert!(commands.apply_verified.contains("--verify"));
378
379 let choice_commands = ApplyCommands::for_choice(&id, &ChoiceId::new("B"));
380 assert!(choice_commands.apply.contains("--choice B"));
381 }
382
383 #[test]
384 fn test_enhanced_suggestion_from_opportunity() {
385 let (opportunity, id) = create_test_opportunity();
386
387 let enhanced = EnhancedSuggestion::from_opportunity(&opportunity, id);
388
389 assert_eq!(enhanced.title, opportunity.message);
390 assert_eq!(enhanced.location, opportunity.location);
391 assert_eq!(enhanced.original_confidence, opportunity.confidence);
392 assert!(!enhanced.has_choices());
393 assert_eq!(enhanced.verified_count(), 0);
394 }
395
396 #[test]
397 fn test_enhanced_suggestion_with_candidates() {
398 let (opportunity, id) = create_test_opportunity();
399
400 let enhanced = EnhancedSuggestion::from_opportunity(&opportunity, id)
401 .add_verified_candidate(
402 VerifiedCandidate::new("A", 0.9)
403 .with_verification(VerificationStatus::FullyVerified),
404 )
405 .add_verified_candidate(
406 VerifiedCandidate::new("B", 0.7).with_verification(VerificationStatus::LightCheck),
407 )
408 .add_verified_candidate(VerifiedCandidate::new("C", 0.8).with_verification(
409 VerificationStatus::Failed {
410 errors: vec!["type mismatch".to_string()],
411 },
412 ));
413
414 assert_eq!(enhanced.verified_count(), 2); assert!(enhanced.has_fully_verified());
416 assert_eq!(enhanced.fully_verified_candidates().len(), 1);
417
418 let best = enhanced.best_candidate().unwrap();
419 assert_eq!(best.choice_id.as_str(), "A"); }
421
422 #[test]
423 fn test_enhanced_suggestion_serde() {
424 let (opportunity, id) = create_test_opportunity();
425
426 let enhanced = EnhancedSuggestion::from_opportunity(&opportunity, id)
427 .with_description("Test description")
428 .add_verified_candidate(
429 VerifiedCandidate::new("A", 0.9)
430 .with_verification(VerificationStatus::FullyVerified),
431 );
432
433 let json = serde_json::to_string(&enhanced).unwrap();
434 let parsed: EnhancedSuggestion = serde_json::from_str(&json).unwrap();
435
436 assert_eq!(parsed.description, "Test description");
437 assert_eq!(parsed.verified_candidates.len(), 1);
438 }
439}