Skip to main content

oris_intake/
mutation.rs

1//! Mutation builder for converting intake events to evolution mutations
2
3use crate::signal::ExtractedSignal;
4use crate::source::IntakeEvent;
5use crate::{IntakeError, IntakeResult};
6use serde::{Deserialize, Serialize};
7
8/// A mutation proposal generated from intake events
9#[derive(Clone, Debug, Serialize, Deserialize)]
10pub struct IntakeMutation {
11    /// Unique mutation ID
12    pub mutation_id: String,
13
14    /// Intent description
15    pub intent: String,
16
17    /// Target for the mutation
18    pub target: MutationTarget,
19
20    /// Expected effect
21    pub expected_effect: String,
22
23    /// Risk level
24    pub risk: MutationRisk,
25
26    /// Extracted signals that triggered this mutation
27    pub signals: Vec<String>,
28
29    /// Source event IDs
30    pub source_event_ids: Vec<String>,
31
32    /// Priority based on severity
33    pub priority: i32,
34}
35
36/// Mutation target
37#[derive(Clone, Debug, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum MutationTarget {
40    /// Apply to entire workspace
41    WorkspaceRoot,
42    /// Apply to specific crate
43    Crate { name: String },
44    /// Apply to specific paths
45    Paths { allow: Vec<String> },
46}
47
48/// Mutation risk level
49#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
50#[serde(rename_all = "lowercase")]
51pub enum MutationRisk {
52    Low,
53    Medium,
54    High,
55    Critical,
56}
57
58impl Default for MutationRisk {
59    fn default() -> Self {
60        Self::Medium
61    }
62}
63
64impl From<crate::source::IssueSeverity> for MutationRisk {
65    fn from(severity: crate::source::IssueSeverity) -> Self {
66        match severity {
67            crate::source::IssueSeverity::Critical => MutationRisk::Critical,
68            crate::source::IssueSeverity::High => MutationRisk::High,
69            crate::source::IssueSeverity::Medium => MutationRisk::Medium,
70            crate::source::IssueSeverity::Low => MutationRisk::Low,
71            crate::source::IssueSeverity::Info => MutationRisk::Low,
72        }
73    }
74}
75
76/// Builder for creating mutations from intake events
77pub struct MutationBuilder {
78    /// Default risk level
79    default_risk: MutationRisk,
80
81    /// Maximum signals per mutation
82    max_signals: usize,
83}
84
85impl MutationBuilder {
86    /// Create a new mutation builder
87    pub fn new() -> Self {
88        Self {
89            default_risk: MutationRisk::Medium,
90            max_signals: 5,
91        }
92    }
93
94    /// Build a mutation from an intake event and extracted signals
95    pub fn build(&self, event: &IntakeEvent, signals: &[ExtractedSignal]) -> IntakeMutation {
96        let risk: MutationRisk = event.severity.clone().into();
97
98        let signals_str: Vec<String> = signals
99            .iter()
100            .take(self.max_signals)
101            .map(|s| s.content.clone())
102            .collect();
103
104        let intent = format!("Auto-intake: {} - {}", event.title, signals_str.join(", "));
105
106        let expected_effect = format!(
107            "Resolve {} from {} source",
108            event.title,
109            event.source_type.to_string()
110        );
111
112        let priority = match event.severity {
113            crate::source::IssueSeverity::Critical => 100,
114            crate::source::IssueSeverity::High => 75,
115            crate::source::IssueSeverity::Medium => 50,
116            crate::source::IssueSeverity::Low => 25,
117            crate::source::IssueSeverity::Info => 10,
118        };
119
120        IntakeMutation {
121            mutation_id: uuid::Uuid::new_v4().to_string(),
122            intent,
123            target: MutationTarget::WorkspaceRoot,
124            expected_effect,
125            risk,
126            signals: signals_str,
127            source_event_ids: vec![event.event_id.clone()],
128            priority,
129        }
130    }
131
132    /// Build mutations from multiple events
133    pub fn build_batch(
134        &self,
135        events: &[IntakeEvent],
136        signals_map: &[Vec<ExtractedSignal>],
137    ) -> Vec<IntakeMutation> {
138        events
139            .iter()
140            .zip(signals_map.iter())
141            .map(|(event, signals)| self.build(event, signals))
142            .collect()
143    }
144}
145
146impl Default for MutationBuilder {
147    fn default() -> Self {
148        Self::new()
149    }
150}
151
152/// Convert intake mutation to oris-evolution MutationIntent
153impl From<&IntakeMutation> for oris_evolution::MutationIntent {
154    fn from(mutation: &IntakeMutation) -> Self {
155        let target = match &mutation.target {
156            MutationTarget::WorkspaceRoot => oris_evolution::MutationTarget::WorkspaceRoot,
157            MutationTarget::Crate { name } => {
158                oris_evolution::MutationTarget::Crate { name: name.clone() }
159            }
160            MutationTarget::Paths { allow } => oris_evolution::MutationTarget::Paths {
161                allow: allow.clone(),
162            },
163        };
164
165        let risk = match mutation.risk {
166            MutationRisk::Low => oris_evolution::RiskLevel::Low,
167            MutationRisk::Medium => oris_evolution::RiskLevel::Medium,
168            MutationRisk::High | MutationRisk::Critical => oris_evolution::RiskLevel::High,
169        };
170
171        oris_evolution::MutationIntent {
172            id: mutation.mutation_id.clone(),
173            intent: mutation.intent.clone(),
174            target,
175            expected_effect: mutation.expected_effect.clone(),
176            risk,
177            signals: mutation.signals.clone(),
178            spec_id: None,
179        }
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use crate::signal::SignalType;
187    use crate::source::{IntakeSourceType, IssueSeverity};
188
189    #[test]
190    fn test_mutation_builder() {
191        let builder = MutationBuilder::new();
192
193        let event = IntakeEvent {
194            event_id: "event-1".to_string(),
195            source_type: IntakeSourceType::Github,
196            source_event_id: Some("run-123".to_string()),
197            title: "Build failed".to_string(),
198            description: "Borrow checker error in main.rs".to_string(),
199            severity: IssueSeverity::High,
200            signals: vec![],
201            raw_payload: None,
202            timestamp_ms: 0,
203        };
204
205        let signals = vec![ExtractedSignal {
206            signal_id: "sig-1".to_string(),
207            content: "compiler_error:borrow checker".to_string(),
208            signal_type: SignalType::CompilerError,
209            confidence: 0.8,
210            source: "github".to_string(),
211        }];
212
213        let mutation = builder.build(&event, &signals);
214        assert_eq!(mutation.risk, MutationRisk::High);
215        assert!(mutation.intent.contains("Build failed"));
216    }
217
218    #[test]
219    fn test_severity_to_risk() {
220        assert_eq!(
221            MutationRisk::from(IssueSeverity::Critical),
222            MutationRisk::Critical
223        );
224        assert_eq!(MutationRisk::from(IssueSeverity::High), MutationRisk::High);
225        assert_eq!(
226            MutationRisk::from(IssueSeverity::Medium),
227            MutationRisk::Medium
228        );
229        assert_eq!(MutationRisk::from(IssueSeverity::Low), MutationRisk::Low);
230        assert_eq!(MutationRisk::from(IssueSeverity::Info), MutationRisk::Low);
231    }
232}