Skip to main content

ryo_suggest/
enhanced.rs

1//! Enhanced suggestion types with verification and design choices.
2//!
3//! This module extends the basic suggestion model with:
4//! - Design choice sets for alternative implementations
5//! - Verification status for each candidate
6//! - Apply commands for easy execution
7//!
8//! # Architecture
9//!
10//! ```text
11//! SuggestOpportunity (basic)
12//!        │
13//!        ▼
14//! EnhancedSuggestion
15//!   ├── design_choices: Option<DesignChoiceSet>
16//!   ├── verified_candidates: Vec<VerifiedCandidate>
17//!   └── apply_commands: ApplyCommands
18//! ```
19
20use crate::design_choice::{ChoiceId, DesignChoiceSet};
21use crate::{SuggestId, SuggestLocation, SuggestOpportunity};
22use serde::{Deserialize, Serialize};
23use std::fmt;
24
25/// Verification status for a candidate.
26///
27/// Indicates how thoroughly a candidate has been verified
28/// before presenting it to the user.
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
30pub enum VerificationStatus {
31    /// Not yet verified.
32    #[default]
33    Pending,
34
35    /// Passed GraphChecker pre-check only (in-memory, ~100ms).
36    /// Fast but not complete verification.
37    LightCheck,
38
39    /// Passed cargo check in TempWorkspace (complete verification).
40    /// Guarantees the code will compile.
41    FullyVerified,
42
43    /// Verification failed with errors.
44    Failed {
45        /// Error messages from verification.
46        errors: Vec<String>,
47    },
48
49    /// Verification was skipped (e.g., dry-run mode).
50    Skipped,
51}
52
53impl VerificationStatus {
54    /// Check if verification passed (LightCheck or FullyVerified).
55    pub fn is_passed(&self) -> bool {
56        matches!(self, Self::LightCheck | Self::FullyVerified)
57    }
58
59    /// Check if verification failed.
60    pub fn is_failed(&self) -> bool {
61        matches!(self, Self::Failed { .. })
62    }
63
64    /// Check if fully verified (cargo check passed).
65    pub fn is_fully_verified(&self) -> bool {
66        matches!(self, Self::FullyVerified)
67    }
68
69    /// Get error messages (if failed).
70    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/// A verified candidate representing a potential change.
93///
94/// Links a design choice to its verification result and
95/// provides summary information for display.
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct VerifiedCandidate {
98    /// Reference to the choice this candidate represents.
99    pub choice_id: ChoiceId,
100
101    /// Verification status.
102    pub verification: VerificationStatus,
103
104    /// Summary of changes (e.g., "3 files, +45/-12 lines").
105    pub diff_summary: String,
106
107    /// Confidence score (0.0 - 1.0).
108    /// May be adjusted based on verification results.
109    pub confidence: f32,
110}
111
112impl VerifiedCandidate {
113    /// Create a new verified candidate.
114    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    /// Set verification status.
124    pub fn with_verification(mut self, status: VerificationStatus) -> Self {
125        self.verification = status;
126        self
127    }
128
129    /// Set diff summary.
130    pub fn with_diff_summary(mut self, summary: impl Into<String>) -> Self {
131        self.diff_summary = summary.into();
132        self
133    }
134
135    /// Check if this candidate passed verification.
136    pub fn is_verified(&self) -> bool {
137        self.verification.is_passed()
138    }
139
140    /// Check if this candidate is fully verified.
141    pub fn is_fully_verified(&self) -> bool {
142        self.verification.is_fully_verified()
143    }
144}
145
146/// Commands for applying a suggestion.
147///
148/// Provides ready-to-use CLI commands for users.
149#[derive(Debug, Clone, Serialize, Deserialize, Default)]
150pub struct ApplyCommands {
151    /// Command to preview changes (dry-run).
152    pub preview: String,
153
154    /// Command to apply changes.
155    pub apply: String,
156
157    /// Command to apply with verification.
158    pub apply_verified: String,
159}
160
161impl ApplyCommands {
162    /// Create apply commands for a given suggestion ID.
163    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    /// Create apply commands for a specific choice.
173    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/// An enhanced suggestion with design choices and verification.
194///
195/// This extends the basic `SuggestOpportunity` with:
196/// - Multiple design choice alternatives
197/// - Pre-verified candidates with confidence scores
198/// - Ready-to-use CLI commands
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct EnhancedSuggestion {
201    /// Unique suggestion ID.
202    pub id: SuggestId,
203
204    /// Human-readable title.
205    pub title: String,
206
207    /// Primary location for display.
208    pub location: SuggestLocation,
209
210    /// Detailed description.
211    pub description: String,
212
213    /// Optional design choices (for suggestions with alternatives).
214    pub design_choices: Option<DesignChoiceSet>,
215
216    /// Verified candidates (subset of choices that passed verification).
217    pub verified_candidates: Vec<VerifiedCandidate>,
218
219    /// Commands for applying this suggestion.
220    pub apply_commands: ApplyCommands,
221
222    /// Original confidence from pattern detection.
223    pub original_confidence: f32,
224}
225
226impl EnhancedSuggestion {
227    /// Create an enhanced suggestion from a basic opportunity.
228    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    /// Set design choices.
242    pub fn with_design_choices(mut self, choices: DesignChoiceSet) -> Self {
243        self.design_choices = Some(choices);
244        self
245    }
246
247    /// Add a verified candidate.
248    pub fn add_verified_candidate(mut self, candidate: VerifiedCandidate) -> Self {
249        self.verified_candidates.push(candidate);
250        self
251    }
252
253    /// Set description.
254    pub fn with_description(mut self, description: impl Into<String>) -> Self {
255        self.description = description.into();
256        self
257    }
258
259    /// Check if this suggestion has design choices.
260    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    /// Get the number of verified candidates.
268    pub fn verified_count(&self) -> usize {
269        self.verified_candidates
270            .iter()
271            .filter(|c| c.is_verified())
272            .count()
273    }
274
275    /// Get the best verified candidate (highest confidence).
276    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    /// Check if any candidate is fully verified.
288    pub fn has_fully_verified(&self) -> bool {
289        self.verified_candidates
290            .iter()
291            .any(|c| c.is_fully_verified())
292    }
293
294    /// Get all fully verified candidates.
295    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); // A and B passed
415        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"); // Highest confidence among verified
420    }
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}