1use std::collections::{HashMap, VecDeque};
26use std::time::{Duration, Instant};
27
28use llmtrace_core::{SecurityFinding, SecuritySeverity};
29
30#[derive(Debug, Clone)]
36pub struct FprMonitorConfig {
37 pub window_duration: Duration,
39 pub drift_threshold: f64,
41 pub min_window_samples: usize,
43 pub categories: Vec<String>,
45}
46
47impl Default for FprMonitorConfig {
48 fn default() -> Self {
49 Self {
50 window_duration: Duration::from_secs(600),
51 drift_threshold: 0.02,
52 min_window_samples: 100,
53 categories: Vec::new(),
54 }
55 }
56}
57
58#[derive(Debug, Clone)]
64struct FprEvent {
65 timestamp: Instant,
66 was_flagged: bool,
67 confidence: f64,
68}
69
70#[derive(Debug, Clone)]
76pub struct FprDriftAlert {
77 pub category: String,
79 pub baseline_fpr: f64,
81 pub current_fpr: f64,
83 pub deviation: f64,
85 pub window_size: usize,
87 pub detected_at: Instant,
89}
90
91#[derive(Debug, Clone)]
97pub struct FprCategorySummary {
98 pub category: String,
100 pub current_fpr: f64,
102 pub sample_count: usize,
104 pub flagged_count: usize,
106 pub avg_confidence: f64,
108}
109
110#[derive(Debug, Clone)]
112pub struct FprMonitorSummary {
113 pub categories: Vec<FprCategorySummary>,
115}
116
117#[derive(Debug)]
123pub struct FprMonitor {
124 config: FprMonitorConfig,
125 windows: HashMap<String, VecDeque<FprEvent>>,
126}
127
128impl FprMonitor {
129 #[must_use]
133 pub fn new(config: FprMonitorConfig) -> Self {
134 let mut windows = HashMap::new();
135 for cat in &config.categories {
136 windows.insert(cat.clone(), VecDeque::new());
137 }
138 Self { config, windows }
139 }
140
141 pub fn record_event(&mut self, category: &str, was_flagged: bool, confidence: f64) {
145 self.record_event_at(category, Instant::now(), was_flagged, confidence);
146 }
147
148 pub fn record_event_at(
150 &mut self,
151 category: &str,
152 at: Instant,
153 was_flagged: bool,
154 confidence: f64,
155 ) {
156 let window = self.windows.entry(category.to_string()).or_default();
157 prune_window(window, self.config.window_duration);
158 window.push_back(FprEvent {
159 timestamp: at,
160 was_flagged,
161 confidence: confidence.clamp(0.0, 1.0),
162 });
163 }
164
165 #[must_use]
169 pub fn current_fpr(&mut self, category: &str) -> Option<f64> {
170 let window = self.windows.get_mut(category)?;
171 prune_window(window, self.config.window_duration);
172 if window.is_empty() {
173 return None;
174 }
175 let flagged = window.iter().filter(|e| e.was_flagged).count();
176 Some(flagged as f64 / window.len() as f64)
177 }
178
179 pub fn check_drift(&mut self, category: &str, baseline_fpr: f64) -> Option<FprDriftAlert> {
185 let window = self.windows.get_mut(category)?;
186 prune_window(window, self.config.window_duration);
187
188 let count = window.len();
189 if count < self.config.min_window_samples {
190 return None;
191 }
192
193 let flagged = window.iter().filter(|e| e.was_flagged).count();
194 let current = flagged as f64 / count as f64;
195 let deviation = (current - baseline_fpr).abs();
196
197 if deviation < self.config.drift_threshold {
198 return None;
199 }
200
201 Some(FprDriftAlert {
202 category: category.to_string(),
203 baseline_fpr,
204 current_fpr: current,
205 deviation,
206 window_size: count,
207 detected_at: Instant::now(),
208 })
209 }
210
211 pub fn check_all_drift(&mut self, baselines: &HashMap<String, f64>) -> Vec<FprDriftAlert> {
213 let categories: Vec<String> = baselines.keys().cloned().collect();
214 let mut alerts = Vec::new();
215 for cat in categories {
216 if let Some(baseline) = baselines.get(&cat) {
217 if let Some(alert) = self.check_drift(&cat, *baseline) {
218 alerts.push(alert);
219 }
220 }
221 }
222 alerts
223 }
224
225 #[must_use]
227 pub fn to_security_findings(alerts: &[FprDriftAlert]) -> Vec<SecurityFinding> {
228 alerts.iter().map(alert_to_finding).collect()
229 }
230
231 pub fn summary(&mut self) -> FprMonitorSummary {
233 let categories: Vec<String> = self.windows.keys().cloned().collect();
234 let mut summaries = Vec::with_capacity(categories.len());
235 for cat in categories {
236 if let Some(cs) = self.category_summary(&cat) {
237 summaries.push(cs);
238 }
239 }
240 summaries.sort_by(|a, b| a.category.cmp(&b.category));
241 FprMonitorSummary {
242 categories: summaries,
243 }
244 }
245
246 fn category_summary(&mut self, category: &str) -> Option<FprCategorySummary> {
248 let window = self.windows.get_mut(category)?;
249 prune_window(window, self.config.window_duration);
250
251 let sample_count = window.len();
252 let flagged_count = window.iter().filter(|e| e.was_flagged).count();
253 let current_fpr = if sample_count == 0 {
254 0.0
255 } else {
256 flagged_count as f64 / sample_count as f64
257 };
258 let avg_confidence = if sample_count == 0 {
259 0.0
260 } else {
261 window.iter().map(|e| e.confidence).sum::<f64>() / sample_count as f64
262 };
263
264 Some(FprCategorySummary {
265 category: category.to_string(),
266 current_fpr,
267 sample_count,
268 flagged_count,
269 avg_confidence,
270 })
271 }
272}
273
274fn prune_window(window: &mut VecDeque<FprEvent>, window_duration: Duration) {
280 let cutoff = Instant::now() - window_duration;
281 while window.front().is_some_and(|e| e.timestamp < cutoff) {
282 window.pop_front();
283 }
284}
285
286#[must_use]
288fn alert_to_finding(alert: &FprDriftAlert) -> SecurityFinding {
289 let severity = if alert.deviation >= 0.10 {
290 SecuritySeverity::High
291 } else if alert.deviation >= 0.05 {
292 SecuritySeverity::Medium
293 } else {
294 SecuritySeverity::Low
295 };
296
297 let description = format!(
298 "FPR drift detected for category '{}': baseline={:.4}, current={:.4}, deviation={:.4} ({} samples)",
299 alert.category,
300 alert.baseline_fpr,
301 alert.current_fpr,
302 alert.deviation,
303 alert.window_size,
304 );
305
306 let requires_alert = severity >= SecuritySeverity::High;
307 let mut finding = SecurityFinding::new(
308 severity,
309 "fpr_drift".to_string(),
310 description,
311 1.0 - alert.deviation.min(1.0),
312 );
313 finding
314 .metadata
315 .insert("category".to_string(), alert.category.clone());
316 finding.metadata.insert(
317 "baseline_fpr".to_string(),
318 format!("{:.6}", alert.baseline_fpr),
319 );
320 finding.metadata.insert(
321 "current_fpr".to_string(),
322 format!("{:.6}", alert.current_fpr),
323 );
324 finding
325 .metadata
326 .insert("deviation".to_string(), format!("{:.6}", alert.deviation));
327 finding
328 .metadata
329 .insert("window_size".to_string(), alert.window_size.to_string());
330 finding.requires_alert = requires_alert;
331 finding
332}
333
334#[cfg(test)]
339mod tests {
340 use super::*;
341
342 fn default_config() -> FprMonitorConfig {
343 FprMonitorConfig {
344 window_duration: Duration::from_secs(600),
345 drift_threshold: 0.02,
346 min_window_samples: 10,
347 categories: vec!["injection".into(), "jailbreak".into()],
348 }
349 }
350
351 #[test]
354 fn empty_monitor_returns_no_drift() {
355 let mut monitor = FprMonitor::new(default_config());
356 let result = monitor.check_drift("injection", 0.05);
357 assert!(result.is_none());
358 }
359
360 #[test]
361 fn empty_monitor_current_fpr_is_none_for_unknown() {
362 let mut monitor = FprMonitor::new(default_config());
363 assert!(monitor.current_fpr("nonexistent").is_none());
364 }
365
366 #[test]
367 fn empty_monitor_current_fpr_is_none_for_known_category() {
368 let mut monitor = FprMonitor::new(default_config());
369 assert!(monitor.current_fpr("injection").is_none());
370 }
371
372 #[test]
375 fn recording_events_updates_counts() {
376 let mut monitor = FprMonitor::new(default_config());
377 monitor.record_event("injection", true, 0.9);
378 monitor.record_event("injection", false, 0.2);
379 monitor.record_event("injection", true, 0.85);
380
381 let summary = monitor.summary();
382 let inj = summary
383 .categories
384 .iter()
385 .find(|c| c.category == "injection")
386 .unwrap();
387 assert_eq!(inj.sample_count, 3);
388 assert_eq!(inj.flagged_count, 2);
389 }
390
391 #[test]
392 fn current_fpr_reflects_recorded_events() {
393 let mut monitor = FprMonitor::new(default_config());
394 for _ in 0..3 {
395 monitor.record_event("injection", true, 0.9);
396 }
397 for _ in 0..7 {
398 monitor.record_event("injection", false, 0.1);
399 }
400 let fpr = monitor.current_fpr("injection").unwrap();
401 assert!((fpr - 0.3).abs() < f64::EPSILON);
402 }
403
404 #[test]
407 fn per_category_tracking() {
408 let mut monitor = FprMonitor::new(default_config());
409 monitor.record_event("injection", true, 0.9);
410 monitor.record_event("injection", false, 0.1);
411 monitor.record_event("jailbreak", true, 0.8);
412
413 let inj_fpr = monitor.current_fpr("injection").unwrap();
414 let jb_fpr = monitor.current_fpr("jailbreak").unwrap();
415
416 assert!((inj_fpr - 0.5).abs() < f64::EPSILON);
417 assert!((jb_fpr - 1.0).abs() < f64::EPSILON);
418 }
419
420 #[test]
423 fn drift_fires_when_exceeding_threshold() {
424 let config = FprMonitorConfig {
425 min_window_samples: 10,
426 drift_threshold: 0.02,
427 ..default_config()
428 };
429 let mut monitor = FprMonitor::new(config);
430
431 for _ in 0..3 {
433 monitor.record_event("injection", true, 0.9);
434 }
435 for _ in 0..7 {
436 monitor.record_event("injection", false, 0.1);
437 }
438
439 let alert = monitor.check_drift("injection", 0.05);
440 assert!(alert.is_some());
441 let alert = alert.unwrap();
442 assert_eq!(alert.category, "injection");
443 assert!((alert.baseline_fpr - 0.05).abs() < f64::EPSILON);
444 assert!((alert.current_fpr - 0.3).abs() < f64::EPSILON);
445 assert!((alert.deviation - 0.25).abs() < f64::EPSILON);
446 assert_eq!(alert.window_size, 10);
447 }
448
449 #[test]
452 fn no_drift_below_threshold() {
453 let config = FprMonitorConfig {
454 min_window_samples: 10,
455 drift_threshold: 0.05,
456 ..default_config()
457 };
458 let mut monitor = FprMonitor::new(config);
459
460 monitor.record_event("injection", true, 0.9);
462 for _ in 0..9 {
463 monitor.record_event("injection", false, 0.1);
464 }
465
466 let alert = monitor.check_drift("injection", 0.10);
467 assert!(alert.is_none());
468 }
469
470 #[test]
471 fn drift_fires_when_exactly_at_threshold() {
472 let config = FprMonitorConfig {
473 min_window_samples: 10,
474 drift_threshold: 0.10,
475 ..default_config()
476 };
477 let mut monitor = FprMonitor::new(config);
478
479 for _ in 0..2 {
481 monitor.record_event("injection", true, 0.9);
482 }
483 for _ in 0..8 {
484 monitor.record_event("injection", false, 0.1);
485 }
486
487 let alert = monitor.check_drift("injection", 0.10);
489 assert!(alert.is_some());
490 let alert = alert.unwrap();
491 assert!((alert.deviation - 0.10).abs() < f64::EPSILON);
492 }
493
494 #[test]
497 fn min_samples_enforcement() {
498 let config = FprMonitorConfig {
499 min_window_samples: 100,
500 drift_threshold: 0.02,
501 ..default_config()
502 };
503 let mut monitor = FprMonitor::new(config);
504
505 for _ in 0..10 {
507 monitor.record_event("injection", true, 0.9);
508 }
509
510 let alert = monitor.check_drift("injection", 0.05);
511 assert!(alert.is_none());
512 }
513
514 #[test]
515 fn drift_fires_once_min_samples_reached() {
516 let config = FprMonitorConfig {
517 min_window_samples: 20,
518 drift_threshold: 0.02,
519 ..default_config()
520 };
521 let mut monitor = FprMonitor::new(config);
522
523 for _ in 0..19 {
525 monitor.record_event("injection", true, 0.9);
526 }
527 assert!(monitor.check_drift("injection", 0.05).is_none());
528
529 monitor.record_event("injection", true, 0.9);
531 assert!(monitor.check_drift("injection", 0.05).is_some());
532 }
533
534 #[test]
537 fn window_prunes_old_events() {
538 let config = FprMonitorConfig {
539 window_duration: Duration::from_secs(2),
540 min_window_samples: 1,
541 drift_threshold: 0.02,
542 ..default_config()
543 };
544 let mut monitor = FprMonitor::new(config);
545
546 let old = Instant::now() - Duration::from_secs(10);
547 monitor.record_event_at("injection", old, true, 0.9);
548 monitor.record_event_at("injection", old, true, 0.9);
549
550 monitor.record_event("injection", false, 0.1);
552
553 let fpr = monitor.current_fpr("injection").unwrap();
555 assert!(fpr.abs() < f64::EPSILON);
556
557 let summary = monitor.summary();
558 let inj = summary
559 .categories
560 .iter()
561 .find(|c| c.category == "injection")
562 .unwrap();
563 assert_eq!(inj.sample_count, 1);
564 assert_eq!(inj.flagged_count, 0);
565 }
566
567 #[test]
568 fn pruned_events_do_not_affect_drift() {
569 let config = FprMonitorConfig {
570 window_duration: Duration::from_secs(1),
571 min_window_samples: 5,
572 drift_threshold: 0.02,
573 ..default_config()
574 };
575 let mut monitor = FprMonitor::new(config);
576
577 let old = Instant::now() - Duration::from_secs(10);
578 for _ in 0..10 {
580 monitor.record_event_at("injection", old, true, 0.9);
581 }
582 for _ in 0..5 {
584 monitor.record_event("injection", false, 0.1);
585 }
586
587 let fpr = monitor.current_fpr("injection").unwrap();
590 assert!(fpr.abs() < f64::EPSILON);
591 }
592
593 #[test]
596 fn multiple_categories_independent() {
597 let mut monitor = FprMonitor::new(default_config());
598
599 for _ in 0..10 {
601 monitor.record_event("injection", true, 0.9);
602 }
603 for _ in 0..10 {
605 monitor.record_event("jailbreak", false, 0.1);
606 }
607
608 let baselines: HashMap<String, f64> =
609 [("injection".into(), 0.05), ("jailbreak".into(), 0.05)]
610 .into_iter()
611 .collect();
612
613 let alerts = monitor.check_all_drift(&baselines);
614 assert_eq!(alerts.len(), 2);
617
618 let inj_alert = alerts.iter().find(|a| a.category == "injection").unwrap();
619 assert!((inj_alert.current_fpr - 1.0).abs() < f64::EPSILON);
620
621 let jb_alert = alerts.iter().find(|a| a.category == "jailbreak").unwrap();
622 assert!(jb_alert.current_fpr.abs() < f64::EPSILON);
623 }
624
625 #[test]
626 fn check_all_drift_only_drifted_categories() {
627 let config = FprMonitorConfig {
628 min_window_samples: 10,
629 drift_threshold: 0.05,
630 ..default_config()
631 };
632 let mut monitor = FprMonitor::new(config);
633
634 for _ in 0..5 {
636 monitor.record_event("injection", true, 0.9);
637 }
638 for _ in 0..5 {
639 monitor.record_event("injection", false, 0.1);
640 }
641
642 monitor.record_event("jailbreak", true, 0.8);
644 for _ in 0..19 {
645 monitor.record_event("jailbreak", false, 0.1);
646 }
647
648 let baselines: HashMap<String, f64> =
649 [("injection".into(), 0.05), ("jailbreak".into(), 0.05)]
650 .into_iter()
651 .collect();
652
653 let alerts = monitor.check_all_drift(&baselines);
654 assert_eq!(alerts.len(), 1);
655 assert_eq!(alerts[0].category, "injection");
656 }
657
658 #[test]
661 fn summary_computes_stats() {
662 let mut monitor = FprMonitor::new(default_config());
663
664 monitor.record_event("injection", true, 0.90);
665 monitor.record_event("injection", false, 0.10);
666 monitor.record_event("injection", true, 0.80);
667 monitor.record_event("injection", false, 0.20);
668
669 let summary = monitor.summary();
670 let inj = summary
671 .categories
672 .iter()
673 .find(|c| c.category == "injection")
674 .unwrap();
675
676 assert_eq!(inj.sample_count, 4);
677 assert_eq!(inj.flagged_count, 2);
678 assert!((inj.current_fpr - 0.5).abs() < f64::EPSILON);
679 assert!((inj.avg_confidence - 0.5).abs() < f64::EPSILON);
680 }
681
682 #[test]
683 fn summary_empty_category_has_zero_stats() {
684 let mut monitor = FprMonitor::new(default_config());
685 let summary = monitor.summary();
686 for cat in &summary.categories {
687 assert_eq!(cat.sample_count, 0);
688 assert_eq!(cat.flagged_count, 0);
689 assert!(cat.current_fpr.abs() < f64::EPSILON);
690 assert!(cat.avg_confidence.abs() < f64::EPSILON);
691 }
692 }
693
694 #[test]
695 fn summary_sorted_by_category() {
696 let config = FprMonitorConfig {
697 categories: vec!["zzz".into(), "aaa".into(), "mmm".into()],
698 ..default_config()
699 };
700 let mut monitor = FprMonitor::new(config);
701 let summary = monitor.summary();
702 let names: Vec<&str> = summary
703 .categories
704 .iter()
705 .map(|c| c.category.as_str())
706 .collect();
707 assert_eq!(names, vec!["aaa", "mmm", "zzz"]);
708 }
709
710 #[test]
713 fn security_finding_from_small_drift() {
714 let alert = FprDriftAlert {
715 category: "injection".into(),
716 baseline_fpr: 0.05,
717 current_fpr: 0.08,
718 deviation: 0.03,
719 window_size: 200,
720 detected_at: Instant::now(),
721 };
722 let findings = FprMonitor::to_security_findings(&[alert]);
723 assert_eq!(findings.len(), 1);
724 let f = &findings[0];
725 assert_eq!(f.finding_type, "fpr_drift");
726 assert_eq!(f.severity, SecuritySeverity::Low);
727 assert!(!f.requires_alert);
728 assert!(f.description.contains("injection"));
729 assert_eq!(f.metadata["category"], "injection");
730 assert_eq!(f.metadata["window_size"], "200");
731 }
732
733 #[test]
734 fn security_finding_from_medium_drift() {
735 let alert = FprDriftAlert {
736 category: "pii".into(),
737 baseline_fpr: 0.01,
738 current_fpr: 0.08,
739 deviation: 0.07,
740 window_size: 500,
741 detected_at: Instant::now(),
742 };
743 let findings = FprMonitor::to_security_findings(&[alert]);
744 assert_eq!(findings[0].severity, SecuritySeverity::Medium);
745 }
746
747 #[test]
748 fn security_finding_from_large_drift() {
749 let alert = FprDriftAlert {
750 category: "toxicity".into(),
751 baseline_fpr: 0.02,
752 current_fpr: 0.15,
753 deviation: 0.13,
754 window_size: 1000,
755 detected_at: Instant::now(),
756 };
757 let findings = FprMonitor::to_security_findings(&[alert]);
758 assert_eq!(findings[0].severity, SecuritySeverity::High);
759 assert!(findings[0].requires_alert);
760 }
761
762 #[test]
763 fn security_finding_metadata_populated() {
764 let alert = FprDriftAlert {
765 category: "injection".into(),
766 baseline_fpr: 0.05,
767 current_fpr: 0.30,
768 deviation: 0.25,
769 window_size: 100,
770 detected_at: Instant::now(),
771 };
772 let findings = FprMonitor::to_security_findings(&[alert]);
773 let f = &findings[0];
774 assert_eq!(f.metadata["category"], "injection");
775 assert!(f.metadata.contains_key("baseline_fpr"));
776 assert!(f.metadata.contains_key("current_fpr"));
777 assert!(f.metadata.contains_key("deviation"));
778 assert!(f.metadata.contains_key("window_size"));
779 }
780
781 #[test]
782 fn empty_alerts_produce_empty_findings() {
783 let findings = FprMonitor::to_security_findings(&[]);
784 assert!(findings.is_empty());
785 }
786
787 #[test]
790 fn single_sample_below_min_window() {
791 let mut monitor = FprMonitor::new(default_config());
792 monitor.record_event("injection", true, 0.95);
793
794 let fpr = monitor.current_fpr("injection").unwrap();
795 assert!((fpr - 1.0).abs() < f64::EPSILON);
796
797 assert!(monitor.check_drift("injection", 0.05).is_none());
799 }
800
801 #[test]
802 fn all_flagged() {
803 let config = FprMonitorConfig {
804 min_window_samples: 5,
805 ..default_config()
806 };
807 let mut monitor = FprMonitor::new(config);
808 for _ in 0..10 {
809 monitor.record_event("injection", true, 0.99);
810 }
811
812 let fpr = monitor.current_fpr("injection").unwrap();
813 assert!((fpr - 1.0).abs() < f64::EPSILON);
814
815 let alert = monitor.check_drift("injection", 0.05);
816 assert!(alert.is_some());
817 let alert = alert.unwrap();
818 assert!((alert.deviation - 0.95).abs() < f64::EPSILON);
819 }
820
821 #[test]
822 fn none_flagged() {
823 let config = FprMonitorConfig {
824 min_window_samples: 5,
825 ..default_config()
826 };
827 let mut monitor = FprMonitor::new(config);
828 for _ in 0..10 {
829 monitor.record_event("injection", false, 0.01);
830 }
831
832 let fpr = monitor.current_fpr("injection").unwrap();
833 assert!(fpr.abs() < f64::EPSILON);
834
835 assert!(monitor.check_drift("injection", 0.0).is_none());
837 }
838
839 #[test]
840 fn dynamic_category_creation() {
841 let mut monitor = FprMonitor::new(default_config());
842 monitor.record_event("pii", true, 0.7);
844 let fpr = monitor.current_fpr("pii").unwrap();
845 assert!((fpr - 1.0).abs() < f64::EPSILON);
846 }
847
848 #[test]
849 fn confidence_clamped_to_unit_range() {
850 let mut monitor = FprMonitor::new(default_config());
851 monitor.record_event("injection", true, 1.5);
852 monitor.record_event("injection", false, -0.5);
853
854 let summary = monitor.summary();
855 let inj = summary
856 .categories
857 .iter()
858 .find(|c| c.category == "injection")
859 .unwrap();
860 assert!((inj.avg_confidence - 0.5).abs() < f64::EPSILON);
861 }
862
863 #[test]
864 fn drift_below_baseline_also_detected() {
865 let config = FprMonitorConfig {
866 min_window_samples: 10,
867 drift_threshold: 0.02,
868 ..default_config()
869 };
870 let mut monitor = FprMonitor::new(config);
871
872 for _ in 0..10 {
874 monitor.record_event("injection", false, 0.1);
875 }
876
877 let alert = monitor.check_drift("injection", 0.50);
878 assert!(alert.is_some());
879 let alert = alert.unwrap();
880 assert!((alert.deviation - 0.50).abs() < f64::EPSILON);
881 }
882
883 #[test]
884 fn multiple_findings_from_multiple_alerts() {
885 let alerts = vec![
886 FprDriftAlert {
887 category: "injection".into(),
888 baseline_fpr: 0.05,
889 current_fpr: 0.30,
890 deviation: 0.25,
891 window_size: 100,
892 detected_at: Instant::now(),
893 },
894 FprDriftAlert {
895 category: "jailbreak".into(),
896 baseline_fpr: 0.03,
897 current_fpr: 0.10,
898 deviation: 0.07,
899 window_size: 200,
900 detected_at: Instant::now(),
901 },
902 ];
903 let findings = FprMonitor::to_security_findings(&alerts);
904 assert_eq!(findings.len(), 2);
905
906 let types: Vec<&str> = findings.iter().map(|f| f.finding_type.as_str()).collect();
907 assert!(types.iter().all(|t| *t == "fpr_drift"));
908 }
909}