depyler_tooling/hunt_mode/
mod.rs1pub mod five_whys;
25pub mod hansei;
26pub mod hunt_shim;
27pub mod isolator;
28pub mod kaizen;
29pub mod planner;
30pub mod repair;
31pub mod verifier;
32
33pub use five_whys::{FiveWhysAnalyzer, RootCauseChain, WhyStep};
35pub use hansei::{CycleOutcome, HanseiReflector, Lesson};
36pub use isolator::{MinimalReproducer, ReproCase};
37pub use kaizen::KaizenMetrics;
38pub use planner::{ErrorCluster, FailurePattern, HuntPlanner};
39pub use repair::{JidokaRepairEngine, Mutator, RepairResult};
40pub use verifier::{AndonStatus, AndonVerifier, VerifyResult};
41
42use std::path::PathBuf;
43
44#[derive(Debug, Clone)]
46pub struct HuntConfig {
47 pub max_cycles: u32,
49 pub quality_threshold: f64,
51 pub target_rate: f64,
53 pub plateau_threshold: u32,
55 pub enable_five_whys: bool,
57 pub lessons_database: PathBuf,
59}
60
61impl Default for HuntConfig {
62 fn default() -> Self {
63 Self {
64 max_cycles: 100,
65 quality_threshold: 0.85,
66 target_rate: 0.80,
67 plateau_threshold: 5,
68 enable_five_whys: true,
69 lessons_database: PathBuf::from(".depyler/lessons.db"),
70 }
71 }
72}
73
74pub struct HuntEngine {
76 config: HuntConfig,
77 metrics: KaizenMetrics,
78 planner: HuntPlanner,
79 reproducer: MinimalReproducer,
80 repair_engine: JidokaRepairEngine,
81 verifier: AndonVerifier,
82 reflector: HanseiReflector,
83}
84
85impl HuntEngine {
86 pub fn new(config: HuntConfig) -> Self {
88 Self {
89 metrics: KaizenMetrics::new(),
90 planner: HuntPlanner::new(),
91 reproducer: MinimalReproducer::new(),
92 repair_engine: JidokaRepairEngine::new(config.quality_threshold),
93 verifier: AndonVerifier::new(),
94 reflector: HanseiReflector::new(),
95 config,
96 }
97 }
98
99 pub fn run_cycle(&mut self) -> anyhow::Result<CycleOutcome> {
103 let pattern = self
105 .planner
106 .select_next_target()
107 .ok_or_else(|| anyhow::anyhow!("No failure patterns to process"))?;
108
109 let repro = self.reproducer.synthesize_repro(&pattern)?;
111
112 let repair_result = self.repair_engine.attempt_repair(&repro)?;
114
115 let verify_result = match repair_result {
117 RepairResult::Success(fix) => self.verifier.verify_and_commit(&fix, &repro)?,
118 RepairResult::NeedsHumanReview {
119 fix,
120 confidence,
121 reason,
122 } => VerifyResult::NeedsReview {
123 fix,
124 confidence,
125 reason,
126 },
127 RepairResult::NoFixFound => VerifyResult::NoFixAvailable,
128 };
129
130 let outcome = CycleOutcome {
132 pattern,
133 repro,
134 verify_result,
135 metrics_snapshot: self.metrics.clone(),
136 };
137
138 let _lessons = self.reflector.reflect_on_cycle(&outcome);
140
141 self.metrics.record_cycle(&outcome);
143
144 Ok(outcome)
145 }
146
147 pub fn run_until_complete(&mut self) -> anyhow::Result<Vec<CycleOutcome>> {
149 let mut outcomes = Vec::new();
150
151 for cycle in 0..self.config.max_cycles {
152 if self.metrics.compilation_rate >= self.config.target_rate {
154 tracing::info!(
155 "Target rate {:.1}% achieved after {} cycles",
156 self.config.target_rate * 100.0,
157 cycle
158 );
159 break;
160 }
161
162 if self.metrics.cycles_since_improvement >= self.config.plateau_threshold {
164 tracing::warn!(
165 "Plateau detected after {} cycles without improvement",
166 self.metrics.cycles_since_improvement
167 );
168 break;
169 }
170
171 match self.run_cycle() {
172 Ok(outcome) => outcomes.push(outcome),
173 Err(e) => {
174 tracing::error!("Cycle {} failed: {}", cycle, e);
175 }
177 }
178 }
179
180 Ok(outcomes)
181 }
182
183 pub fn andon_status(&self) -> &AndonStatus {
185 self.verifier.status()
186 }
187
188 pub fn metrics(&self) -> &KaizenMetrics {
190 &self.metrics
191 }
192
193 pub fn export_lessons(&self) -> Vec<Lesson> {
195 self.reflector.lessons().to_vec()
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn test_hunt_config_default() {
205 let config = HuntConfig::default();
206 assert_eq!(config.max_cycles, 100);
207 assert!((config.quality_threshold - 0.85).abs() < f64::EPSILON);
208 assert!((config.target_rate - 0.80).abs() < f64::EPSILON);
209 assert_eq!(config.plateau_threshold, 5);
210 assert!(config.enable_five_whys);
211 }
212
213 #[test]
214 fn test_hunt_config_custom() {
215 let config = HuntConfig {
216 max_cycles: 50,
217 quality_threshold: 0.90,
218 target_rate: 0.95,
219 plateau_threshold: 10,
220 enable_five_whys: false,
221 lessons_database: PathBuf::from("/custom/path"),
222 };
223 assert_eq!(config.max_cycles, 50);
224 assert!((config.quality_threshold - 0.90).abs() < f64::EPSILON);
225 assert!((config.target_rate - 0.95).abs() < f64::EPSILON);
226 assert_eq!(config.plateau_threshold, 10);
227 assert!(!config.enable_five_whys);
228 assert_eq!(config.lessons_database.to_str(), Some("/custom/path"));
229 }
230
231 #[test]
232 fn test_hunt_config_clone() {
233 let config = HuntConfig::default();
234 let cloned = config.clone();
235 assert_eq!(cloned.max_cycles, config.max_cycles);
236 assert!((cloned.quality_threshold - config.quality_threshold).abs() < f64::EPSILON);
237 }
238
239 #[test]
240 fn test_hunt_config_debug() {
241 let config = HuntConfig::default();
242 let debug_str = format!("{:?}", config);
243 assert!(debug_str.contains("HuntConfig"));
244 assert!(debug_str.contains("max_cycles"));
245 assert!(debug_str.contains("quality_threshold"));
246 }
247
248 #[test]
249 fn test_hunt_engine_creation() {
250 let config = HuntConfig::default();
251 let engine = HuntEngine::new(config);
252 assert_eq!(engine.metrics().compilation_rate, 0.0);
253 assert_eq!(engine.metrics().cumulative_fixes, 0);
254 }
255
256 #[test]
257 fn test_hunt_engine_andon_status() {
258 let config = HuntConfig::default();
259 let engine = HuntEngine::new(config);
260 let status = engine.andon_status();
261 let _ = format!("{:?}", status); }
265
266 #[test]
267 fn test_hunt_engine_export_lessons_empty() {
268 let config = HuntConfig::default();
269 let engine = HuntEngine::new(config);
270 let lessons = engine.export_lessons();
271 assert!(lessons.is_empty());
272 }
273
274 #[test]
275 fn test_hunt_engine_with_custom_config() {
276 let config = HuntConfig {
277 max_cycles: 10,
278 quality_threshold: 0.99,
279 target_rate: 0.5,
280 plateau_threshold: 2,
281 enable_five_whys: false,
282 lessons_database: PathBuf::from("test.db"),
283 };
284 let engine = HuntEngine::new(config);
285 assert_eq!(engine.metrics().compilation_rate, 0.0);
286 }
287
288 #[test]
291 fn test_hunt_config_all_fields() {
292 let config = HuntConfig::default();
293 assert_eq!(config.max_cycles, 100);
295 assert!((config.quality_threshold - 0.85).abs() < f64::EPSILON);
296 assert!((config.target_rate - 0.80).abs() < f64::EPSILON);
297 assert_eq!(config.plateau_threshold, 5);
298 assert!(config.enable_five_whys);
299 assert_eq!(
300 config.lessons_database,
301 PathBuf::from(".depyler/lessons.db")
302 );
303 }
304
305 #[test]
306 fn test_hunt_config_lessons_database_custom() {
307 let config = HuntConfig {
308 lessons_database: PathBuf::from("/custom/db/lessons.sqlite"),
309 ..Default::default()
310 };
311 assert_eq!(
312 config.lessons_database,
313 PathBuf::from("/custom/db/lessons.sqlite")
314 );
315 }
316
317 #[test]
318 fn test_hunt_engine_metrics_initial_state() {
319 let config = HuntConfig::default();
320 let engine = HuntEngine::new(config);
321 let metrics = engine.metrics();
322
323 assert_eq!(metrics.compilation_rate, 0.0);
324 assert_eq!(metrics.cumulative_fixes, 0);
325 assert_eq!(metrics.cycles_since_improvement, 0);
326 }
327
328 #[test]
329 fn test_hunt_engine_export_lessons_type() {
330 let config = HuntConfig::default();
331 let engine = HuntEngine::new(config);
332 let lessons: Vec<Lesson> = engine.export_lessons();
333
334 assert!(lessons.is_empty());
336 let _: &[Lesson] = &lessons;
338 }
339
340 #[test]
341 fn test_hunt_engine_andon_status_type() {
342 let config = HuntConfig::default();
343 let engine = HuntEngine::new(config);
344 let status: &AndonStatus = engine.andon_status();
345
346 let _debug = format!("{:?}", status);
348 }
349
350 #[test]
351 fn test_hunt_config_debug_format() {
352 let config = HuntConfig {
353 max_cycles: 50,
354 quality_threshold: 0.95,
355 target_rate: 0.90,
356 plateau_threshold: 3,
357 enable_five_whys: false,
358 lessons_database: PathBuf::from("test.db"),
359 };
360
361 let debug = format!("{:?}", config);
362 assert!(debug.contains("max_cycles"));
363 assert!(debug.contains("50"));
364 assert!(debug.contains("quality_threshold"));
365 assert!(debug.contains("0.95"));
366 assert!(debug.contains("target_rate"));
367 assert!(debug.contains("0.9"));
368 assert!(debug.contains("plateau_threshold"));
369 assert!(debug.contains("3"));
370 assert!(debug.contains("enable_five_whys"));
371 assert!(debug.contains("false"));
372 assert!(debug.contains("lessons_database"));
373 assert!(debug.contains("test.db"));
374 }
375
376 #[test]
377 fn test_hunt_config_clone_independence() {
378 let original = HuntConfig {
379 max_cycles: 25,
380 quality_threshold: 0.75,
381 target_rate: 0.60,
382 plateau_threshold: 8,
383 enable_five_whys: true,
384 lessons_database: PathBuf::from("original.db"),
385 };
386
387 let mut cloned = original.clone();
388 cloned.max_cycles = 50;
389 cloned.quality_threshold = 0.99;
390
391 assert_eq!(original.max_cycles, 25);
393 assert!((original.quality_threshold - 0.75).abs() < f64::EPSILON);
394
395 assert_eq!(cloned.max_cycles, 50);
397 assert!((cloned.quality_threshold - 0.99).abs() < f64::EPSILON);
398 }
399
400 #[test]
401 fn test_hunt_config_edge_values() {
402 let config = HuntConfig {
404 max_cycles: 0,
405 quality_threshold: 0.0,
406 target_rate: 1.0,
407 plateau_threshold: u32::MAX,
408 enable_five_whys: false,
409 lessons_database: PathBuf::new(),
410 };
411
412 assert_eq!(config.max_cycles, 0);
413 assert_eq!(config.quality_threshold, 0.0);
414 assert_eq!(config.target_rate, 1.0);
415 assert_eq!(config.plateau_threshold, u32::MAX);
416 assert!(!config.enable_five_whys);
417 assert_eq!(config.lessons_database, PathBuf::new());
418 }
419
420 #[test]
421 fn test_hunt_engine_config_preserved() {
422 let config = HuntConfig {
423 max_cycles: 42,
424 quality_threshold: 0.77,
425 target_rate: 0.88,
426 plateau_threshold: 7,
427 enable_five_whys: true,
428 lessons_database: PathBuf::from("preserved.db"),
429 };
430
431 let engine = HuntEngine::new(config);
432
433 assert_eq!(engine.metrics().compilation_rate, 0.0);
435 assert!(engine.export_lessons().is_empty());
436 }
437
438 #[test]
439 fn test_hunt_engine_multiple_instances() {
440 let config1 = HuntConfig::default();
441 let config2 = HuntConfig {
442 max_cycles: 10,
443 ..Default::default()
444 };
445
446 let engine1 = HuntEngine::new(config1);
447 let engine2 = HuntEngine::new(config2);
448
449 assert_eq!(engine1.metrics().compilation_rate, 0.0);
451 assert_eq!(engine2.metrics().compilation_rate, 0.0);
452 }
453
454 #[test]
455 fn test_re_exports_available() {
456 let _: KaizenMetrics = KaizenMetrics::new();
458 let _: HuntPlanner = HuntPlanner::new();
459 let _: MinimalReproducer = MinimalReproducer::new();
460 let _: JidokaRepairEngine = JidokaRepairEngine::new(0.85);
461 let _: AndonVerifier = AndonVerifier::new();
462 let _: HanseiReflector = HanseiReflector::new();
463 let _: FiveWhysAnalyzer = FiveWhysAnalyzer::new();
464 }
465}