1use crate::collector::types::{KeyboardEvent, KeyboardEventType, MouseEvent, MouseEventType};
7use crate::core::windowing::EventWindow;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Default, Serialize, Deserialize)]
17pub struct KeyboardFeatures {
18 pub typing_rate: f64,
20 pub pause_count: u32,
22 pub mean_pause_ms: f64,
24 pub latency_variability: f64,
26 pub hold_time_mean: f64,
28 pub burst_index: f64,
30 pub session_continuity: f64,
32 pub typing_tap_count: u32,
34 pub typing_cadence_stability: f64,
36 pub typing_gap_ratio: f64,
38 pub typing_interaction_intensity: f64,
40 pub keyboard_scroll_rate: f64,
43 pub navigation_key_count: u32,
45}
46
47#[derive(Debug, Clone, Default, Serialize, Deserialize)]
49pub struct MouseFeatures {
50 pub mouse_activity_rate: f64,
52 pub mean_velocity: f64,
54 pub velocity_variability: f64,
56 pub acceleration_spikes: u32,
58 pub click_rate: f64,
60 pub scroll_rate: f64,
62 pub idle_ratio: f64,
64 pub micro_adjustment_ratio: f64,
66 pub idle_time_ms: u64,
68}
69
70#[derive(Debug, Clone, Default, Serialize, Deserialize)]
77pub struct BehavioralSignals {
78 pub interaction_rhythm: f64,
80 pub friction: f64,
82 pub motor_stability: f64,
84 pub focus_continuity_proxy: f64,
86 pub burstiness: f64,
89 pub deep_focus_block: bool,
94}
95
96#[derive(Debug, Clone, Default, Serialize, Deserialize)]
98pub struct WindowFeatures {
99 pub keyboard: KeyboardFeatures,
100 pub mouse: MouseFeatures,
101 pub behavioral: BehavioralSignals,
102}
103
104const PAUSE_THRESHOLD_MS: i64 = 500;
106
107const MICRO_ADJUSTMENT_THRESHOLD: f64 = 5.0;
109
110const ACCELERATION_SPIKE_THRESHOLD: f64 = 50.0;
112
113pub fn compute_features(window: &EventWindow) -> WindowFeatures {
115 let keyboard = compute_keyboard_features(&window.keyboard_events, window.duration_secs());
116 let mouse = compute_mouse_features(&window.mouse_events, window.duration_secs());
117 let behavioral = compute_behavioral_signals(&keyboard, &mouse);
118
119 WindowFeatures {
120 keyboard,
121 mouse,
122 behavioral,
123 }
124}
125
126fn compute_keyboard_features(events: &[KeyboardEvent], window_duration: f64) -> KeyboardFeatures {
132 if events.is_empty() || window_duration <= 0.0 {
133 return KeyboardFeatures::default();
134 }
135
136 let typing_events: Vec<&KeyboardEvent> = events
138 .iter()
139 .filter(|e| e.event_type == KeyboardEventType::TypingTap)
140 .collect();
141
142 let navigation_events: Vec<&KeyboardEvent> = events
143 .iter()
144 .filter(|e| e.event_type == KeyboardEventType::NavigationKey)
145 .collect();
146
147 let navigation_key_presses: Vec<&KeyboardEvent> = navigation_events
149 .iter()
150 .filter(|e| e.is_key_down)
151 .copied()
152 .collect();
153 let navigation_key_count = navigation_key_presses.len() as u32;
154 let keyboard_scroll_rate = navigation_key_count as f64 / window_duration;
155
156 let typing_key_presses: Vec<&KeyboardEvent> = typing_events
158 .iter()
159 .filter(|e| e.is_key_down)
160 .copied()
161 .collect();
162 let typing_tap_count = typing_key_presses.len() as u32;
163
164 let typing_rate = typing_tap_count as f64 / window_duration;
166
167 let intervals: Vec<i64> = typing_key_presses
169 .windows(2)
170 .map(|pair| (pair[1].timestamp - pair[0].timestamp).num_milliseconds())
171 .collect();
172
173 let pauses: Vec<i64> = intervals
175 .iter()
176 .filter(|&&i| i > PAUSE_THRESHOLD_MS)
177 .copied()
178 .collect();
179 let pause_count = pauses.len() as u32;
180 let mean_pause_ms = if pauses.is_empty() {
181 0.0
182 } else {
183 pauses.iter().sum::<i64>() as f64 / pauses.len() as f64
184 };
185
186 let latency_variability = std_dev(&intervals.iter().map(|&i| i as f64).collect::<Vec<_>>());
188
189 let hold_times = compute_hold_times(&typing_events);
192 let hold_time_mean = if hold_times.is_empty() {
193 0.0
194 } else {
195 hold_times.iter().sum::<f64>() / hold_times.len() as f64
196 };
197
198 let short_interval_count = intervals.iter().filter(|&&i| i < 100).count();
201 let burst_index = if intervals.is_empty() {
202 0.0
203 } else {
204 short_interval_count as f64 / intervals.len() as f64
205 };
206
207 let active_intervals: Vec<i64> = intervals
210 .iter()
211 .filter(|&&i| i <= PAUSE_THRESHOLD_MS * 2) .copied()
213 .collect();
214 let active_time_ms: i64 = active_intervals.iter().sum();
215 let session_continuity = (active_time_ms as f64 / 1000.0) / window_duration;
216
217 let typing_cadence_stability = 1.0 / (1.0 + latency_variability / 100.0);
220
221 let typing_gap_ratio = if intervals.is_empty() {
223 0.0
224 } else {
225 pause_count as f64 / intervals.len() as f64
226 };
227
228 let normalized_speed = (typing_rate / 10.0).min(1.0); let typing_interaction_intensity =
232 (normalized_speed * 0.4 + typing_cadence_stability * 0.3 + (1.0 - typing_gap_ratio) * 0.3)
233 .clamp(0.0, 1.0);
234
235 KeyboardFeatures {
236 typing_rate,
237 pause_count,
238 mean_pause_ms,
239 latency_variability,
240 hold_time_mean,
241 burst_index,
242 session_continuity: session_continuity.min(1.0), typing_tap_count,
244 typing_cadence_stability,
245 typing_gap_ratio,
246 typing_interaction_intensity,
247 keyboard_scroll_rate,
248 navigation_key_count,
249 }
250}
251
252fn compute_hold_times(events: &[&KeyboardEvent]) -> Vec<f64> {
254 let mut hold_times = Vec::new();
255 let mut last_down: Option<&KeyboardEvent> = None;
256
257 for event in events {
258 if event.is_key_down {
259 last_down = Some(event);
260 } else if let Some(down) = last_down {
261 let hold_ms = (event.timestamp - down.timestamp).num_milliseconds() as f64;
262 if (20.0..=2000.0).contains(&hold_ms) {
264 hold_times.push(hold_ms);
265 }
266 last_down = None;
267 }
268 }
269
270 hold_times
271}
272
273fn compute_mouse_features(events: &[MouseEvent], window_duration: f64) -> MouseFeatures {
275 if events.is_empty() || window_duration <= 0.0 {
276 return MouseFeatures::default();
277 }
278
279 let move_events: Vec<&MouseEvent> = events
281 .iter()
282 .filter(|e| e.event_type == MouseEventType::Move)
283 .collect();
284
285 let click_events: Vec<&MouseEvent> = events
286 .iter()
287 .filter(|e| {
288 e.event_type == MouseEventType::LeftClick || e.event_type == MouseEventType::RightClick
289 })
290 .collect();
291
292 let scroll_events: Vec<&MouseEvent> = events
293 .iter()
294 .filter(|e| e.event_type == MouseEventType::Scroll)
295 .collect();
296
297 let mouse_activity_rate = move_events.len() as f64 / window_duration;
299
300 let velocities: Vec<f64> = move_events
302 .iter()
303 .filter_map(|e| e.delta_magnitude)
304 .collect();
305
306 let mean_velocity = if velocities.is_empty() {
307 0.0
308 } else {
309 velocities.iter().sum::<f64>() / velocities.len() as f64
310 };
311
312 let velocity_variability = std_dev(&velocities);
313
314 let acceleration_spikes = velocities
316 .windows(2)
317 .filter(|pair| (pair[1] - pair[0]).abs() > ACCELERATION_SPIKE_THRESHOLD)
318 .count() as u32;
319
320 let click_rate = click_events.len() as f64 / window_duration;
322 let scroll_rate = scroll_events.len() as f64 / window_duration;
323
324 let (idle_ratio, idle_time_ms, _has_long_gap) =
326 estimate_idle_metrics(&move_events, window_duration);
327
328 let micro_count = velocities
330 .iter()
331 .filter(|&&v| v < MICRO_ADJUSTMENT_THRESHOLD)
332 .count();
333 let micro_adjustment_ratio = if velocities.is_empty() {
334 0.0
335 } else {
336 micro_count as f64 / velocities.len() as f64
337 };
338
339 MouseFeatures {
340 mouse_activity_rate,
341 mean_velocity,
342 velocity_variability,
343 acceleration_spikes,
344 click_rate,
345 scroll_rate,
346 idle_ratio,
347 micro_adjustment_ratio,
348 idle_time_ms,
349 }
350}
351
352fn estimate_idle_metrics(move_events: &[&MouseEvent], window_duration: f64) -> (f64, u64, bool) {
356 if move_events.len() < 2 {
357 let total_idle = (window_duration * 1000.0) as u64;
359 return (1.0, total_idle, true);
360 }
361
362 const IDLE_THRESHOLD_MS: i64 = 1000;
364 const LONG_GAP_THRESHOLD_MS: i64 = 2000;
366
367 let mut idle_time_ms: i64 = 0;
368 let mut has_long_gap = false;
369
370 for pair in move_events.windows(2) {
371 let gap = (pair[1].timestamp - pair[0].timestamp).num_milliseconds();
372 if gap > IDLE_THRESHOLD_MS {
373 idle_time_ms += gap - IDLE_THRESHOLD_MS; }
375 if gap > LONG_GAP_THRESHOLD_MS {
376 has_long_gap = true;
377 }
378 }
379
380 let idle_secs = idle_time_ms as f64 / 1000.0;
381 let idle_ratio = (idle_secs / window_duration).min(1.0);
382
383 (idle_ratio, idle_time_ms.max(0) as u64, has_long_gap)
384}
385
386fn compute_behavioral_signals(
388 keyboard: &KeyboardFeatures,
389 mouse: &MouseFeatures,
390) -> BehavioralSignals {
391 let typing_rhythm = 1.0 / (1.0 + keyboard.latency_variability / 100.0);
394 let mouse_rhythm = 1.0 / (1.0 + mouse.velocity_variability / 50.0);
395 let interaction_rhythm = (typing_rhythm + mouse_rhythm) / 2.0;
396
397 let friction = (keyboard.pause_count as f64 * 0.1)
400 + (1.0 - keyboard.burst_index) * 0.3
401 + mouse.micro_adjustment_ratio * 0.3;
402
403 let motor_stability = 1.0
406 - (keyboard.latency_variability / 200.0).min(0.5)
407 - (mouse.velocity_variability / 100.0).min(0.5);
408
409 let focus_continuity_proxy = keyboard.session_continuity * 0.5 + (1.0 - mouse.idle_ratio) * 0.5;
412
413 let keyboard_burstiness = keyboard.burst_index;
417 let mouse_burstiness = if mouse.mouse_activity_rate > 0.0 {
419 mouse.idle_ratio * (1.0 - mouse.micro_adjustment_ratio)
421 } else {
422 0.0
423 };
424 let burstiness = (keyboard_burstiness * 0.6 + mouse_burstiness * 0.4).clamp(0.0, 1.0);
425
426 let has_activity = keyboard.typing_tap_count > 0 || mouse.mouse_activity_rate > 0.5;
431 let sustained_typing = keyboard.session_continuity > 0.7;
432 let minimal_idle = mouse.idle_ratio < 0.3;
433 let deep_focus_block = has_activity && sustained_typing && minimal_idle;
434
435 BehavioralSignals {
436 interaction_rhythm: interaction_rhythm.clamp(0.0, 1.0),
437 friction: friction.clamp(0.0, 1.0),
438 motor_stability: motor_stability.clamp(0.0, 1.0),
439 focus_continuity_proxy: focus_continuity_proxy.clamp(0.0, 1.0),
440 burstiness,
441 deep_focus_block,
442 }
443}
444
445fn std_dev(values: &[f64]) -> f64 {
447 if values.len() < 2 {
448 return 0.0;
449 }
450
451 let mean = values.iter().sum::<f64>() / values.len() as f64;
452 let variance = values.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
453 variance.sqrt()
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459 use chrono::{Duration, Utc};
460
461 fn make_keyboard_event(is_down: bool, offset_ms: i64) -> KeyboardEvent {
462 KeyboardEvent {
463 timestamp: Utc::now() + Duration::milliseconds(offset_ms),
464 is_key_down: is_down,
465 event_type: KeyboardEventType::TypingTap,
466 }
467 }
468
469 fn make_navigation_event(is_down: bool, offset_ms: i64) -> KeyboardEvent {
470 KeyboardEvent {
471 timestamp: Utc::now() + Duration::milliseconds(offset_ms),
472 is_key_down: is_down,
473 event_type: KeyboardEventType::NavigationKey,
474 }
475 }
476
477 #[test]
478 fn test_keyboard_features_empty() {
479 let features = compute_keyboard_features(&[], 10.0);
480 assert_eq!(features.typing_rate, 0.0);
481 }
482
483 #[test]
484 fn test_keyboard_features_basic() {
485 let events = vec![
486 make_keyboard_event(true, 0),
487 make_keyboard_event(false, 50),
488 make_keyboard_event(true, 100),
489 make_keyboard_event(false, 150),
490 make_keyboard_event(true, 200),
491 make_keyboard_event(false, 250),
492 ];
493
494 let features = compute_keyboard_features(&events, 1.0);
495 assert_eq!(features.typing_rate, 3.0); }
497
498 #[test]
499 fn test_std_dev() {
500 let values = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
501 let sd = std_dev(&values);
502 assert!((sd - 2.0).abs() < 0.1);
503 }
504
505 #[test]
506 fn test_behavioral_signals_bounds() {
507 let keyboard = KeyboardFeatures::default();
508 let mouse = MouseFeatures::default();
509 let signals = compute_behavioral_signals(&keyboard, &mouse);
510
511 assert!(signals.interaction_rhythm >= 0.0 && signals.interaction_rhythm <= 1.0);
513 assert!(signals.friction >= 0.0 && signals.friction <= 1.0);
514 assert!(signals.motor_stability >= 0.0 && signals.motor_stability <= 1.0);
515 assert!(signals.focus_continuity_proxy >= 0.0 && signals.focus_continuity_proxy <= 1.0);
516 }
517
518 #[test]
519 fn test_typing_tap_count() {
520 let events = vec![
521 make_keyboard_event(true, 0),
522 make_keyboard_event(false, 50),
523 make_keyboard_event(true, 100),
524 make_keyboard_event(false, 150),
525 make_keyboard_event(true, 200),
526 make_keyboard_event(false, 250),
527 ];
528
529 let features = compute_keyboard_features(&events, 1.0);
530 assert_eq!(features.typing_tap_count, 3); }
532
533 #[test]
534 fn test_typing_cadence_stability_bounds() {
535 let features_empty = compute_keyboard_features(&[], 10.0);
537 assert!(
538 features_empty.typing_cadence_stability >= 0.0
539 && features_empty.typing_cadence_stability <= 1.0
540 );
541
542 let events = vec![
544 make_keyboard_event(true, 0),
545 make_keyboard_event(false, 50),
546 make_keyboard_event(true, 100),
547 make_keyboard_event(false, 150),
548 make_keyboard_event(true, 200),
549 make_keyboard_event(false, 250),
550 ];
551 let features = compute_keyboard_features(&events, 1.0);
552 assert!(
553 features.typing_cadence_stability >= 0.0 && features.typing_cadence_stability <= 1.0
554 );
555 assert!(features.typing_cadence_stability > 0.5);
557 }
558
559 #[test]
560 fn test_typing_gap_ratio_bounds() {
561 let features_empty = compute_keyboard_features(&[], 10.0);
562 assert_eq!(features_empty.typing_gap_ratio, 0.0);
563
564 let events = vec![
566 make_keyboard_event(true, 0),
567 make_keyboard_event(false, 50),
568 make_keyboard_event(true, 100),
569 make_keyboard_event(false, 150),
570 ];
571 let features = compute_keyboard_features(&events, 1.0);
572 assert!(features.typing_gap_ratio >= 0.0 && features.typing_gap_ratio <= 1.0);
573 assert_eq!(features.typing_gap_ratio, 0.0); let events_with_gaps = vec![
577 make_keyboard_event(true, 0),
578 make_keyboard_event(false, 50),
579 make_keyboard_event(true, 600), make_keyboard_event(false, 650),
581 ];
582 let features_gaps = compute_keyboard_features(&events_with_gaps, 1.0);
583 assert!(features_gaps.typing_gap_ratio > 0.0); }
585
586 #[test]
587 fn test_typing_interaction_intensity_bounds() {
588 let features_empty = compute_keyboard_features(&[], 10.0);
589 assert!(
590 features_empty.typing_interaction_intensity >= 0.0
591 && features_empty.typing_interaction_intensity <= 1.0
592 );
593
594 let fast_events = vec![
596 make_keyboard_event(true, 0),
597 make_keyboard_event(false, 30),
598 make_keyboard_event(true, 60),
599 make_keyboard_event(false, 90),
600 make_keyboard_event(true, 120),
601 make_keyboard_event(false, 150),
602 make_keyboard_event(true, 180),
603 make_keyboard_event(false, 210),
604 make_keyboard_event(true, 240),
605 make_keyboard_event(false, 270),
606 ];
607 let features = compute_keyboard_features(&fast_events, 1.0);
608 assert!(
609 features.typing_interaction_intensity >= 0.0
610 && features.typing_interaction_intensity <= 1.0
611 );
612 assert!(features.typing_interaction_intensity > 0.3);
614 }
615
616 #[test]
617 fn test_navigation_key_separation() {
618 let events = vec![
620 make_keyboard_event(true, 0), make_keyboard_event(false, 50), make_navigation_event(true, 100), make_navigation_event(false, 150), make_keyboard_event(true, 200), make_keyboard_event(false, 250), make_navigation_event(true, 300), make_navigation_event(false, 350), ];
629
630 let features = compute_keyboard_features(&events, 1.0);
631
632 assert_eq!(features.typing_tap_count, 2);
634 assert_eq!(features.typing_rate, 2.0);
635
636 assert_eq!(features.navigation_key_count, 2);
638 assert_eq!(features.keyboard_scroll_rate, 2.0);
639 }
640
641 #[test]
642 fn test_navigation_keys_dont_inflate_typing_metrics() {
643 let nav_only_events = vec![
645 make_navigation_event(true, 0),
646 make_navigation_event(false, 50),
647 make_navigation_event(true, 100),
648 make_navigation_event(false, 150),
649 make_navigation_event(true, 200),
650 make_navigation_event(false, 250),
651 ];
652
653 let features = compute_keyboard_features(&nav_only_events, 1.0);
654
655 assert_eq!(features.typing_tap_count, 0);
657 assert_eq!(features.typing_rate, 0.0);
658
659 assert_eq!(features.navigation_key_count, 3);
661 assert_eq!(features.keyboard_scroll_rate, 3.0);
662 }
663
664 #[test]
665 fn test_keyboard_scroll_rate_bounds() {
666 let features_empty = compute_keyboard_features(&[], 10.0);
667 assert_eq!(features_empty.keyboard_scroll_rate, 0.0);
668 assert_eq!(features_empty.navigation_key_count, 0);
669
670 let nav_events = vec![
672 make_navigation_event(true, 0),
673 make_navigation_event(false, 30),
674 make_navigation_event(true, 60),
675 make_navigation_event(false, 90),
676 make_navigation_event(true, 120),
677 make_navigation_event(false, 150),
678 ];
679 let features = compute_keyboard_features(&nav_events, 1.0);
680 assert_eq!(features.navigation_key_count, 3);
681 assert!(features.keyboard_scroll_rate > 0.0);
682 }
683
684 #[test]
685 fn test_burstiness_bounds() {
686 let keyboard = KeyboardFeatures::default();
687 let mouse = MouseFeatures::default();
688 let signals = compute_behavioral_signals(&keyboard, &mouse);
689
690 assert!(signals.burstiness >= 0.0 && signals.burstiness <= 1.0);
692 }
693
694 #[test]
695 fn test_burstiness_high_burst_index() {
696 let keyboard = KeyboardFeatures {
698 burst_index: 0.9, ..Default::default()
700 };
701
702 let mouse = MouseFeatures::default();
703 let signals = compute_behavioral_signals(&keyboard, &mouse);
704
705 assert!(signals.burstiness > 0.4);
707 assert!(signals.burstiness <= 1.0);
708 }
709
710 #[test]
711 fn test_deep_focus_block_detection() {
712 let keyboard = KeyboardFeatures::default();
714 let mouse = MouseFeatures::default();
715 let signals = compute_behavioral_signals(&keyboard, &mouse);
716 assert!(!signals.deep_focus_block);
717
718 let keyboard_active = KeyboardFeatures {
720 session_continuity: 0.9, typing_tap_count: 50, ..Default::default()
723 };
724
725 let mouse_active = MouseFeatures {
726 idle_ratio: 0.1, mouse_activity_rate: 2.0,
728 ..Default::default()
729 };
730
731 let signals_active = compute_behavioral_signals(&keyboard_active, &mouse_active);
732 assert!(signals_active.deep_focus_block);
733 }
734
735 #[test]
736 fn test_deep_focus_block_requires_low_idle() {
737 let keyboard = KeyboardFeatures {
739 session_continuity: 0.9,
740 typing_tap_count: 50,
741 ..Default::default()
742 };
743
744 let mouse = MouseFeatures {
745 idle_ratio: 0.5, ..Default::default()
747 };
748
749 let signals = compute_behavioral_signals(&keyboard, &mouse);
750 assert!(!signals.deep_focus_block);
751 }
752
753 #[test]
754 fn test_idle_time_ms_computation() {
755 use crate::collector::types::MouseEvent;
756
757 let base_time = chrono::Utc::now();
759 let events = vec![
760 MouseEvent {
761 timestamp: base_time,
762 event_type: MouseEventType::Move,
763 delta_magnitude: Some(10.0),
764 scroll_direction: None,
765 scroll_magnitude: None,
766 },
767 MouseEvent {
768 timestamp: base_time + chrono::Duration::milliseconds(500),
769 event_type: MouseEventType::Move,
770 delta_magnitude: Some(10.0),
771 scroll_direction: None,
772 scroll_magnitude: None,
773 },
774 MouseEvent {
775 timestamp: base_time + chrono::Duration::milliseconds(2000), event_type: MouseEventType::Move,
777 delta_magnitude: Some(10.0),
778 scroll_direction: None,
779 scroll_magnitude: None,
780 },
781 ];
782
783 let features = compute_mouse_features(&events, 2.0);
784
785 assert!(features.idle_time_ms > 0);
787 assert!(features.idle_ratio > 0.0);
788 }
789
790 #[test]
791 fn test_behavioral_signals_new_fields_bounds() {
792 let keyboard = KeyboardFeatures {
794 burst_index: 0.5,
795 session_continuity: 0.5,
796 typing_tap_count: 10,
797 ..Default::default()
798 };
799
800 let mouse = MouseFeatures {
801 idle_ratio: 0.5,
802 mouse_activity_rate: 1.0,
803 ..Default::default()
804 };
805
806 let signals = compute_behavioral_signals(&keyboard, &mouse);
807
808 assert!(signals.interaction_rhythm >= 0.0 && signals.interaction_rhythm <= 1.0);
810 assert!(signals.friction >= 0.0 && signals.friction <= 1.0);
811 assert!(signals.motor_stability >= 0.0 && signals.motor_stability <= 1.0);
812 assert!(signals.focus_continuity_proxy >= 0.0 && signals.focus_continuity_proxy <= 1.0);
813 assert!(signals.burstiness >= 0.0 && signals.burstiness <= 1.0);
814 }
816}