1use crate::models::{
4 ApprovalDecisionRecord, RiskAssessment, RiskAssessmentReport, WorkflowState, WorkflowStep,
5};
6use chrono::Utc;
7
8#[derive(Debug, Clone)]
10pub struct RiskScorer {
11 pub approval_threshold: u8,
13 pub max_timeout_ms: u64,
15}
16
17impl RiskScorer {
18 pub fn new() -> Self {
20 Self {
21 approval_threshold: 70,
22 max_timeout_ms: 300_000, }
24 }
25
26 pub fn with_threshold(approval_threshold: u8) -> Self {
28 Self {
29 approval_threshold,
30 max_timeout_ms: 300_000,
31 }
32 }
33
34 pub fn calculate_risk_score(&self, step: &WorkflowStep) -> u8 {
43 let factors = &step.risk_factors;
44
45 let impact = factors.impact.min(100);
47 let reversibility = factors.reversibility.min(100);
48 let complexity = factors.complexity.min(100);
49
50 let impact_contribution = (impact as f32) * 0.4;
55 let reversibility_contribution = ((100 - reversibility as u16) as f32) * 0.4;
56 let complexity_contribution = (complexity as f32) * 0.2;
57
58 let score =
59 (impact_contribution + reversibility_contribution + complexity_contribution) as u8;
60 score.min(100)
61 }
62
63 pub fn requires_approval(&self, risk_score: u8) -> bool {
65 risk_score > self.approval_threshold
66 }
67
68 pub fn assess_step(&self, step: &WorkflowStep) -> RiskAssessment {
70 let risk_score = self.calculate_risk_score(step);
71 let approval_required = self.requires_approval(risk_score);
72
73 RiskAssessment {
74 step_id: step.id.clone(),
75 step_name: step.name.clone(),
76 risk_score,
77 risk_factors: step.risk_factors.clone(),
78 approval_required,
79 approval_decision: None,
80 }
81 }
82
83 pub fn generate_report(
85 &self,
86 workflow_id: &str,
87 workflow_name: &str,
88 steps: &[WorkflowStep],
89 state: &WorkflowState,
90 ) -> RiskAssessmentReport {
91 let mut step_assessments = Vec::new();
92 let mut total_score = 0u32;
93
94 for step in steps {
95 let mut assessment = self.assess_step(step);
96
97 if state.step_results.contains_key(&step.id) {
99 if let Some(result) = state.step_results.get(&step.id) {
102 if result.status == crate::models::StepStatus::Completed
103 && assessment.approval_required
104 {
105 assessment.approval_decision = Some(ApprovalDecisionRecord {
106 approved: true,
107 timestamp: Utc::now(),
108 approver: None,
109 comments: None,
110 });
111 }
112 }
113 }
114
115 total_score += assessment.risk_score as u32;
116 step_assessments.push(assessment);
117 }
118
119 let overall_risk_score = if step_assessments.is_empty() {
120 0
121 } else {
122 (total_score / step_assessments.len() as u32) as u8
123 };
124
125 RiskAssessmentReport {
126 workflow_id: workflow_id.to_string(),
127 workflow_name: workflow_name.to_string(),
128 overall_risk_score,
129 step_assessments,
130 safety_violations: Vec::new(),
131 generated_at: Utc::now(),
132 }
133 }
134}
135
136impl Default for RiskScorer {
137 fn default() -> Self {
138 Self::new()
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::models::{AgentStep, ErrorAction, RiskFactors, StepConfig, StepType};
146
147 fn create_test_step(id: &str, impact: u8, reversibility: u8, complexity: u8) -> WorkflowStep {
148 WorkflowStep {
149 id: id.to_string(),
150 name: format!("Step {}", id),
151 step_type: StepType::Agent(AgentStep {
152 agent_id: "test-agent".to_string(),
153 task: "test-task".to_string(),
154 }),
155 config: StepConfig {
156 config: serde_json::json!({}),
157 },
158 dependencies: Vec::new(),
159 approval_required: false,
160 on_error: ErrorAction::Fail,
161 risk_score: None,
162 risk_factors: RiskFactors {
163 impact,
164 reversibility,
165 complexity,
166 },
167 }
168 }
169
170 #[test]
171 fn test_risk_score_calculation_low_risk() {
172 let scorer = RiskScorer::new();
173 let step = create_test_step("1", 10, 90, 10);
174 let score = scorer.calculate_risk_score(&step);
175 assert!(
176 score < 30,
177 "Low risk step should have score < 30, got {}",
178 score
179 );
180 }
181
182 #[test]
183 fn test_risk_score_calculation_high_risk() {
184 let scorer = RiskScorer::new();
185 let step = create_test_step("1", 90, 10, 90);
186 let score = scorer.calculate_risk_score(&step);
187 assert!(
188 score > 70,
189 "High risk step should have score > 70, got {}",
190 score
191 );
192 }
193
194 #[test]
195 fn test_risk_score_in_range() {
196 let scorer = RiskScorer::new();
197 for impact in [0, 25, 50, 75, 100] {
198 for reversibility in [0, 25, 50, 75, 100] {
199 for complexity in [0, 25, 50, 75, 100] {
200 let step = create_test_step("1", impact, reversibility, complexity);
201 let score = scorer.calculate_risk_score(&step);
202 assert!(score <= 100, "Risk score should be <= 100, got {}", score);
203 }
204 }
205 }
206 }
207
208 #[test]
209 fn test_approval_threshold() {
210 let scorer = RiskScorer::with_threshold(70);
211 assert!(!scorer.requires_approval(69));
212 assert!(!scorer.requires_approval(70)); assert!(scorer.requires_approval(71)); }
215
216 #[test]
217 fn test_risk_assessment() {
218 let scorer = RiskScorer::new();
219 let step = create_test_step("1", 80, 20, 80);
220 let assessment = scorer.assess_step(&step);
221 assert_eq!(assessment.step_id, "1");
222 assert!(assessment.approval_required);
223 assert!(assessment.risk_score > 70);
224 }
225}