converge_optimization/packs/anomaly_triage/
mod.rs1mod invariants;
24mod solver;
25mod types;
26
27pub use invariants::*;
28pub use solver::*;
29pub use types::*;
30
31use crate::packs::{InvariantDef, InvariantResult, Pack, PackSolveResult, default_gate_evaluation};
32use converge_pack::CONFIDENCE_STEP_MAJOR;
33use converge_pack::gate::GateResult as Result;
34use converge_pack::gate::{KernelTraceLink, ProblemSpec, PromotionGate, ProposedPlan};
35
36pub struct AnomalyTriagePack;
38
39impl Pack for AnomalyTriagePack {
40 fn name(&self) -> &'static str {
41 "anomaly-triage"
42 }
43
44 fn version(&self) -> &'static str {
45 "1.0.0"
46 }
47
48 fn validate_inputs(&self, inputs: &serde_json::Value) -> Result<()> {
49 let input: AnomalyTriageInput = serde_json::from_value(inputs.clone()).map_err(|e| {
50 converge_pack::GateError::invalid_input(format!("Invalid input: {}", e))
51 })?;
52 input.validate()
53 }
54
55 fn invariants(&self) -> &[InvariantDef] {
56 INVARIANTS
57 }
58
59 fn solve(&self, spec: &ProblemSpec) -> Result<PackSolveResult> {
60 let input: AnomalyTriageInput = spec.inputs_as()?;
61 input.validate()?;
62
63 let solver = ThresholdSolver;
64 let (output, report) = solver.solve_triage(&input, spec)?;
65
66 let trace = KernelTraceLink::audit_only(format!("trace-{}", spec.problem_id));
67 let confidence = calculate_confidence(&output);
68
69 let plan = ProposedPlan::from_payload(
70 format!("plan-{}", spec.problem_id),
71 self.name(),
72 output.summary(),
73 &output,
74 confidence,
75 trace,
76 )?;
77
78 Ok(PackSolveResult::new(plan, report))
79 }
80
81 fn check_invariants(&self, plan: &ProposedPlan) -> Result<Vec<InvariantResult>> {
82 let output: AnomalyTriageOutput = plan.plan_as()?;
83 Ok(check_all_invariants(&output))
84 }
85
86 fn evaluate_gate(
87 &self,
88 _plan: &ProposedPlan,
89 invariant_results: &[InvariantResult],
90 ) -> PromotionGate {
91 default_gate_evaluation(invariant_results, self.invariants())
92 }
93}
94
95fn calculate_confidence(output: &AnomalyTriageOutput) -> f64 {
96 if output.triaged.is_empty() {
97 return 0.5; }
99
100 let mut confidence: f64 = 0.6;
101
102 let all_have_recommendations = output
104 .triaged
105 .iter()
106 .all(|t| !t.recommended_actions.is_empty());
107 if all_have_recommendations {
108 confidence += CONFIDENCE_STEP_MAJOR;
109 }
110
111 let critical_escalated = output
113 .triaged
114 .iter()
115 .filter(|t| t.severity == "critical")
116 .all(|t| t.escalate);
117 if critical_escalated {
118 confidence += CONFIDENCE_STEP_MAJOR;
119 }
120
121 confidence.min(1.0)
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use converge_pack::gate::ObjectiveSpec;
128
129 fn create_test_input() -> AnomalyTriageInput {
130 AnomalyTriageInput {
131 anomalies: vec![
132 Anomaly {
133 id: "a1".to_string(),
134 timestamp: 1700000000,
135 source: "api-server".to_string(),
136 z_score: 5.2,
137 features: serde_json::json!({}),
138 },
139 Anomaly {
140 id: "a2".to_string(),
141 timestamp: 1700000010,
142 source: "database".to_string(),
143 z_score: 2.5,
144 features: serde_json::json!({}),
145 },
146 ],
147 thresholds: SeverityThresholds::default(),
148 escalation_policies: vec![EscalationPolicy {
149 severity_level: "critical".to_string(),
150 auto_escalate: true,
151 notify_channels: vec!["pagerduty".to_string()],
152 response_sla_minutes: 15,
153 }],
154 }
155 }
156
157 #[test]
158 fn test_pack_name() {
159 let pack = AnomalyTriagePack;
160 assert_eq!(pack.name(), "anomaly-triage");
161 assert_eq!(pack.version(), "1.0.0");
162 }
163
164 #[test]
165 fn test_validate_inputs() {
166 let pack = AnomalyTriagePack;
167 let input = create_test_input();
168 let json = serde_json::to_value(&input).unwrap();
169 assert!(pack.validate_inputs(&json).is_ok());
170 }
171
172 #[test]
173 fn test_solve_basic() {
174 let pack = AnomalyTriagePack;
175 let input = create_test_input();
176
177 let spec = ProblemSpec::builder("test-001", "test-tenant")
178 .objective(ObjectiveSpec::minimize("risk"))
179 .inputs(&input)
180 .unwrap()
181 .seed(42)
182 .build()
183 .unwrap();
184
185 let result = pack.solve(&spec).unwrap();
186 assert!(result.is_feasible());
187
188 let output: AnomalyTriageOutput = result.plan.plan_as().unwrap();
189 assert_eq!(output.triaged.len(), 2);
190 }
191
192 #[test]
193 fn test_check_invariants() {
194 let pack = AnomalyTriagePack;
195 let input = create_test_input();
196
197 let spec = ProblemSpec::builder("test-002", "test-tenant")
198 .objective(ObjectiveSpec::minimize("risk"))
199 .inputs(&input)
200 .unwrap()
201 .seed(42)
202 .build()
203 .unwrap();
204
205 let result = pack.solve(&spec).unwrap();
206 let invariants = pack.check_invariants(&result.plan).unwrap();
207
208 let all_pass = invariants.iter().all(|r| r.passed);
209 assert!(all_pass);
210 }
211
212 #[test]
213 fn test_gate_promotes() {
214 let pack = AnomalyTriagePack;
215 let input = create_test_input();
216
217 let spec = ProblemSpec::builder("test-003", "test-tenant")
218 .objective(ObjectiveSpec::minimize("risk"))
219 .inputs(&input)
220 .unwrap()
221 .seed(42)
222 .build()
223 .unwrap();
224
225 let result = pack.solve(&spec).unwrap();
226 let invariants = pack.check_invariants(&result.plan).unwrap();
227 let gate = pack.evaluate_gate(&result.plan, &invariants);
228
229 assert!(gate.is_promoted());
230 }
231
232 #[test]
233 fn test_determinism() {
234 let pack = AnomalyTriagePack;
235 let input = create_test_input();
236
237 let spec1 = ProblemSpec::builder("test-a", "tenant")
238 .objective(ObjectiveSpec::minimize("risk"))
239 .inputs(&input)
240 .unwrap()
241 .seed(99999)
242 .build()
243 .unwrap();
244
245 let spec2 = ProblemSpec::builder("test-b", "tenant")
246 .objective(ObjectiveSpec::minimize("risk"))
247 .inputs(&input)
248 .unwrap()
249 .seed(99999)
250 .build()
251 .unwrap();
252
253 let result1 = pack.solve(&spec1).unwrap();
254 let result2 = pack.solve(&spec2).unwrap();
255
256 let output1: AnomalyTriageOutput = result1.plan.plan_as().unwrap();
257 let output2: AnomalyTriageOutput = result2.plan.plan_as().unwrap();
258
259 for (a, b) in output1.triaged.iter().zip(output2.triaged.iter()) {
260 assert_eq!(a.anomaly_id, b.anomaly_id);
261 assert_eq!(a.priority, b.priority);
262 }
263 }
264}