1use crate::signal::ExtractedSignal;
4use crate::source::IntakeEvent;
5use crate::{IntakeError, IntakeResult};
6use serde::{Deserialize, Serialize};
7
8#[derive(Clone, Debug, Serialize, Deserialize)]
10pub struct IntakeMutation {
11 pub mutation_id: String,
13
14 pub intent: String,
16
17 pub target: MutationTarget,
19
20 pub expected_effect: String,
22
23 pub risk: MutationRisk,
25
26 pub signals: Vec<String>,
28
29 pub source_event_ids: Vec<String>,
31
32 pub priority: i32,
34}
35
36#[derive(Clone, Debug, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum MutationTarget {
40 WorkspaceRoot,
42 Crate { name: String },
44 Paths { allow: Vec<String> },
46}
47
48#[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
76pub struct MutationBuilder {
78 default_risk: MutationRisk,
80
81 max_signals: usize,
83}
84
85impl MutationBuilder {
86 pub fn new() -> Self {
88 Self {
89 default_risk: MutationRisk::Medium,
90 max_signals: 5,
91 }
92 }
93
94 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 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
152impl 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}