1#![forbid(unsafe_code)]
14
15pub mod anomaly;
16pub mod homeostatic;
17pub mod sensorimotor;
18pub mod write_budget;
19
20use std::collections::VecDeque;
21use std::fs;
22use std::sync::RwLock;
23
24use chrono::{DateTime, Utc};
25use serde::{Deserialize, Serialize};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum ThermalState {
33 Normal,
35 Warm,
37 Hot,
39 Critical,
41 Unknown,
43}
44
45impl ThermalState {
46 #[must_use]
48 pub const fn health_factor(self) -> f32 {
49 match self {
50 Self::Normal => 1.0,
51 Self::Warm => 0.8,
52 Self::Hot => 0.5,
53 Self::Critical => 0.2,
54 Self::Unknown => 0.9, }
56 }
57
58 fn from_celsius(temp: f32) -> Self {
60 if temp < 60.0 {
61 Self::Normal
62 } else if temp < 75.0 {
63 Self::Warm
64 } else if temp < 90.0 {
65 Self::Hot
66 } else {
67 Self::Critical
68 }
69 }
70
71 #[must_use]
73 pub const fn as_str(self) -> &'static str {
74 match self {
75 Self::Normal => "normal",
76 Self::Warm => "warm",
77 Self::Hot => "hot",
78 Self::Critical => "critical",
79 Self::Unknown => "unknown",
80 }
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(rename_all = "snake_case")]
89pub enum BatteryState {
90 Charging,
92 Discharging,
94 Full,
96 NotCharging,
98 Unknown,
100}
101
102impl BatteryState {
103 #[must_use]
105 pub const fn as_str(self) -> &'static str {
106 match self {
107 Self::Charging => "charging",
108 Self::Discharging => "discharging",
109 Self::Full => "full",
110 Self::NotCharging => "not_charging",
111 Self::Unknown => "unknown",
112 }
113 }
114
115 fn from_status(s: &str) -> Self {
116 match s.trim() {
117 "Charging" => Self::Charging,
118 "Discharging" => Self::Discharging,
119 "Full" => Self::Full,
120 "Not charging" => Self::NotCharging,
121 _ => Self::Unknown,
122 }
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
136#[serde(rename_all = "snake_case")]
137pub enum GunaTag {
138 Sattvic,
140 Rajasic,
142 Tamasic,
144}
145
146impl GunaTag {
147 #[must_use]
149 pub const fn as_str(self) -> &'static str {
150 match self {
151 Self::Sattvic => "sattvic",
152 Self::Rajasic => "rajasic",
153 Self::Tamasic => "tamasic",
154 }
155 }
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct HarmonyVector {
171 pub cpu_load: f32,
173 pub memory_pressure: f32,
175 pub swap_usage: f32,
177 pub thermal_state: ThermalState,
179 pub temperature_c: Option<f32>,
181 pub battery_state: BatteryState,
183 pub battery_percent: f32,
185 pub disk_io_rate: f32,
187 pub active: bool,
189 pub guna: GunaTag,
191 pub timestamp: DateTime<Utc>,
193}
194
195impl HarmonyVector {
196 #[must_use]
201 pub fn health_score(&self) -> f32 {
202 if self.cpu_load == 0.0
205 && self.memory_pressure == 0.0
206 && self.swap_usage == 0.0
207 && self.temperature_c.unwrap_or(0.0) == 0.0
208 {
209 return 0.5;
210 }
211 let cpu_health = 1.0 - self.cpu_load.min(1.0);
212 let mem_health = 1.0 - self.memory_pressure.min(1.0);
213 let swap_health = 1.0 - self.swap_usage.min(1.0);
214 let thermal_health = self.thermal_state.health_factor();
215 let base = cpu_health.mul_add(
216 0.3,
217 mem_health.mul_add(0.3, swap_health.mul_add(0.2, thermal_health * 0.2)),
218 );
219 base.clamp(0.0, 1.0)
220 }
221
222 #[must_use]
224 pub fn is_stressed(&self) -> bool {
225 self.health_score() < 0.3
226 }
227
228 fn classify_guna(cpu_load: f32, memory_pressure: f32, active: bool) -> GunaTag {
230 if !active && cpu_load < 0.05 {
231 GunaTag::Tamasic
232 } else if cpu_load > 0.7 || memory_pressure > 0.7 {
233 GunaTag::Rajasic
234 } else {
235 GunaTag::Sattvic
236 }
237 }
238
239 #[must_use]
241 pub fn to_json(&self) -> serde_json::Value {
242 serde_json::json!({
243 "cpu_load": self.cpu_load,
244 "memory_pressure": self.memory_pressure,
245 "swap_usage": self.swap_usage,
246 "thermal_state": self.thermal_state.as_str(),
247 "temperature_c": self.temperature_c,
248 "battery_state": self.battery_state.as_str(),
249 "battery_percent": self.battery_percent,
250 "disk_io_rate": self.disk_io_rate,
251 "active": self.active,
252 "guna": self.guna.as_str(),
253 "health_score": self.health_score(),
254 "stressed": self.is_stressed(),
255 "timestamp": self.timestamp.to_rfc3339(),
256 })
257 }
258
259 #[must_use]
264 pub fn sanitized(mut self) -> Self {
265 const fn clamp01(v: f32) -> f32 {
266 if v.is_nan() || v.is_infinite() {
267 0.0
268 } else {
269 v.clamp(0.0, 1.0)
270 }
271 }
272 self.cpu_load = clamp01(self.cpu_load);
273 self.memory_pressure = clamp01(self.memory_pressure);
274 self.swap_usage = clamp01(self.swap_usage);
275 self.battery_percent = clamp01(self.battery_percent);
276 self.disk_io_rate = clamp01(self.disk_io_rate);
277 if let Some(temp) = self.temperature_c {
278 if temp.is_nan() || temp.is_infinite() || !(-100.0..=200.0).contains(&temp) {
279 self.temperature_c = None;
280 }
281 }
282 self
283 }
284}
285
286impl Default for HarmonyVector {
287 fn default() -> Self {
288 Self {
289 cpu_load: 0.0,
290 memory_pressure: 0.0,
291 swap_usage: 0.0,
292 thermal_state: ThermalState::Unknown,
293 temperature_c: None,
294 battery_state: BatteryState::Unknown,
295 battery_percent: 1.0,
296 disk_io_rate: 0.0,
297 active: false,
298 guna: GunaTag::Tamasic,
299 timestamp: Utc::now(),
300 }
301 }
302}
303
304pub struct SubstrateMonitor {
317 history: RwLock<VecDeque<HarmonyVector>>,
318 max_history: usize,
319 cpu_count: f32,
320 sensors_available: bool,
322}
323
324impl SubstrateMonitor {
325 #[must_use]
327 pub fn new(max_history: usize) -> Self {
328 let cpu_count = std::thread::available_parallelism().map_or(1.0, |n| n.get() as f32);
329 let sensors_available =
330 cfg!(target_os = "linux") && std::path::Path::new("/proc/loadavg").exists();
331 Self {
332 history: RwLock::new(VecDeque::with_capacity(max_history)),
333 max_history,
334 cpu_count,
335 sensors_available,
336 }
337 }
338
339 #[must_use]
344 pub const fn sensors_available(&self) -> bool {
345 self.sensors_available
346 }
347
348 #[must_use]
350 #[allow(clippy::should_implement_trait)]
351 pub fn default() -> Self {
352 Self::new(100)
353 }
354
355 pub fn sample(&self) -> HarmonyVector {
360 let cpu_load = self.read_cpu_load();
361 let (memory_pressure, swap_usage) = self.read_memory();
362 let (thermal_state, temperature_c) = self.read_thermal();
363 let (battery_state, battery_percent) = self.read_battery();
364 let active = cpu_load > 0.15;
365 let guna = HarmonyVector::classify_guna(cpu_load, memory_pressure, active);
366
367 let hv = HarmonyVector {
368 cpu_load,
369 memory_pressure,
370 swap_usage,
371 thermal_state,
372 temperature_c,
373 battery_state,
374 battery_percent,
375 disk_io_rate: 0.0,
376 active,
377 guna,
378 timestamp: Utc::now(),
379 };
380
381 if let Ok(mut hist) = self.history.write() {
382 if hist.len() >= self.max_history {
383 hist.pop_front();
384 }
385 hist.push_back(hv.clone());
386 }
387
388 hv
389 }
390
391 #[must_use]
393 pub fn history(&self, limit: usize) -> Vec<HarmonyVector> {
394 self.history
395 .read()
396 .map(|h| h.iter().rev().take(limit).cloned().collect())
397 .unwrap_or_default()
398 }
399
400 #[must_use]
402 pub fn last_sample(&self) -> Option<HarmonyVector> {
403 self.history.read().ok().and_then(|h| h.back().cloned())
404 }
405
406 #[must_use]
408 pub fn history_len(&self) -> usize {
409 self.history.read().map_or(0, |h| h.len())
410 }
411
412 fn read_cpu_load(&self) -> f32 {
415 fs::read_to_string("/proc/loadavg")
416 .ok()
417 .and_then(|s| {
418 let parts: Vec<&str> = s.split_whitespace().collect();
419 parts.first().and_then(|p| p.parse::<f32>().ok())
420 })
421 .map_or(0.0, |load| (load / self.cpu_count).min(1.0))
422 }
423
424 fn read_memory(&self) -> (f32, f32) {
425 let meminfo = match fs::read_to_string("/proc/meminfo") {
426 Ok(s) => s,
427 Err(_) => return (0.0, 0.0),
428 };
429
430 let mut mem_total = None;
431 let mut mem_available = None;
432 let mut swap_total = None;
433 let mut swap_free = None;
434
435 for line in meminfo.lines() {
436 if line.starts_with("MemTotal:") {
437 mem_total = parse_kb(line);
438 } else if line.starts_with("MemAvailable:") {
439 mem_available = parse_kb(line);
440 } else if line.starts_with("SwapTotal:") {
441 swap_total = parse_kb(line);
442 } else if line.starts_with("SwapFree:") {
443 swap_free = parse_kb(line);
444 }
445 }
446
447 let memory_pressure = match (mem_total, mem_available) {
448 (Some(total), Some(avail)) if total > 0 => 1.0 - (avail as f32 / total as f32).min(1.0),
449 _ => 0.0,
450 };
451
452 let swap_usage = match (swap_total, swap_free) {
453 (Some(total), Some(free)) if total > 0 => 1.0 - (free as f32 / total as f32).min(1.0),
454 _ => 0.0,
455 };
456
457 (memory_pressure, swap_usage)
458 }
459
460 fn read_thermal(&self) -> (ThermalState, Option<f32>) {
461 for i in 0..10 {
462 let path = format!("/sys/class/thermal/thermal_zone{i}/temp");
463 if let Ok(s) = fs::read_to_string(&path) {
464 if let Ok(millideg) = s.trim().parse::<f32>() {
465 let temp = millideg / 1000.0;
466 return (ThermalState::from_celsius(temp), Some(temp));
467 }
468 }
469 }
470 (ThermalState::Unknown, None)
471 }
472
473 fn read_battery(&self) -> (BatteryState, f32) {
474 for name in ["BAT0", "BAT1", "BAT2"] {
475 let base = format!("/sys/class/power_supply/{name}");
476 let capacity = fs::read_to_string(format!("{base}/capacity"))
477 .ok()
478 .and_then(|s| s.trim().parse::<f32>().ok())
479 .map(|v| (v / 100.0).clamp(0.0, 1.0));
480 let status = fs::read_to_string(format!("{base}/status"))
481 .ok()
482 .map_or(BatteryState::Unknown, |s| BatteryState::from_status(&s));
483
484 if capacity.is_some() {
485 return (status, capacity.unwrap_or(1.0));
486 }
487 }
488 (BatteryState::Unknown, 1.0)
489 }
490}
491
492fn parse_kb(line: &str) -> Option<u64> {
494 line.split(':')
495 .nth(1)
496 .and_then(|s| s.split_whitespace().next())
497 .and_then(|s| s.parse::<u64>().ok())
498}
499
500#[cfg(test)]
503mod tests {
504 use super::*;
505
506 #[test]
507 fn sensors_available_matches_platform() {
508 let monitor = SubstrateMonitor::default();
509 let expect = cfg!(target_os = "linux") && std::path::Path::new("/proc/loadavg").exists();
512 assert_eq!(monitor.sensors_available(), expect);
513 }
514
515 #[test]
516 fn sample_never_panics_regardless_of_platform() {
517 let monitor = SubstrateMonitor::default();
518 let hv = monitor.sample();
519 assert!((0.0..=1.0).contains(&hv.cpu_load));
520 assert!((0.0..=1.0).contains(&hv.memory_pressure));
521 }
522
523 #[test]
524 fn thermal_state_classification() {
525 assert_eq!(ThermalState::from_celsius(45.0), ThermalState::Normal);
526 assert_eq!(ThermalState::from_celsius(65.0), ThermalState::Warm);
527 assert_eq!(ThermalState::from_celsius(80.0), ThermalState::Hot);
528 assert_eq!(ThermalState::from_celsius(95.0), ThermalState::Critical);
529 }
530
531 #[test]
532 fn thermal_health_factor() {
533 assert_eq!(ThermalState::Normal.health_factor(), 1.0);
534 assert_eq!(ThermalState::Warm.health_factor(), 0.8);
535 assert_eq!(ThermalState::Hot.health_factor(), 0.5);
536 assert_eq!(ThermalState::Critical.health_factor(), 0.2);
537 assert_eq!(ThermalState::Unknown.health_factor(), 0.9);
538 }
539
540 #[test]
541 fn thermal_as_str() {
542 assert_eq!(ThermalState::Normal.as_str(), "normal");
543 assert_eq!(ThermalState::Warm.as_str(), "warm");
544 assert_eq!(ThermalState::Hot.as_str(), "hot");
545 assert_eq!(ThermalState::Critical.as_str(), "critical");
546 assert_eq!(ThermalState::Unknown.as_str(), "unknown");
547 }
548
549 #[test]
550 fn battery_state_from_status() {
551 assert_eq!(
552 BatteryState::from_status("Charging"),
553 BatteryState::Charging
554 );
555 assert_eq!(
556 BatteryState::from_status("Discharging"),
557 BatteryState::Discharging
558 );
559 assert_eq!(BatteryState::from_status("Full"), BatteryState::Full);
560 assert_eq!(
561 BatteryState::from_status("Not charging"),
562 BatteryState::NotCharging
563 );
564 assert_eq!(BatteryState::from_status("Unknown"), BatteryState::Unknown);
565 }
566
567 #[test]
568 fn battery_as_str() {
569 assert_eq!(BatteryState::Charging.as_str(), "charging");
570 assert_eq!(BatteryState::Discharging.as_str(), "discharging");
571 assert_eq!(BatteryState::Full.as_str(), "full");
572 assert_eq!(BatteryState::NotCharging.as_str(), "not_charging");
573 assert_eq!(BatteryState::Unknown.as_str(), "unknown");
574 }
575
576 #[test]
577 fn guna_classification() {
578 assert_eq!(
579 HarmonyVector::classify_guna(0.02, 0.1, false),
580 GunaTag::Tamasic
581 );
582 assert_eq!(
583 HarmonyVector::classify_guna(0.8, 0.3, true),
584 GunaTag::Rajasic
585 );
586 assert_eq!(
587 HarmonyVector::classify_guna(0.3, 0.8, true),
588 GunaTag::Rajasic
589 );
590 assert_eq!(
591 HarmonyVector::classify_guna(0.3, 0.3, true),
592 GunaTag::Sattvic
593 );
594 }
595
596 #[test]
597 fn guna_as_str() {
598 assert_eq!(GunaTag::Sattvic.as_str(), "sattvic");
599 assert_eq!(GunaTag::Rajasic.as_str(), "rajasic");
600 assert_eq!(GunaTag::Tamasic.as_str(), "tamasic");
601 }
602
603 #[test]
604 fn harmony_vector_health_score_healthy() {
605 let hv = HarmonyVector {
606 cpu_load: 0.1,
607 memory_pressure: 0.2,
608 swap_usage: 0.1,
609 thermal_state: ThermalState::Normal,
610 temperature_c: Some(45.0),
611 battery_state: BatteryState::Full,
612 battery_percent: 1.0,
613 disk_io_rate: 0.0,
614 active: true,
615 guna: GunaTag::Sattvic,
616 timestamp: Utc::now(),
617 };
618 let score = hv.health_score();
619 assert!(score > 0.8, "Health score should be high: {score}");
620 assert!(!hv.is_stressed());
621 }
622
623 #[test]
624 fn harmony_vector_health_score_stressed() {
625 let hv = HarmonyVector {
626 cpu_load: 0.9,
627 memory_pressure: 0.9,
628 swap_usage: 0.8,
629 thermal_state: ThermalState::Critical,
630 temperature_c: Some(95.0),
631 battery_state: BatteryState::Discharging,
632 battery_percent: 0.1,
633 disk_io_rate: 0.0,
634 active: true,
635 guna: GunaTag::Rajasic,
636 timestamp: Utc::now(),
637 };
638 let score = hv.health_score();
639 assert!(score < 0.3, "Health score should be critical: {score}");
640 assert!(hv.is_stressed());
641 }
642
643 #[test]
644 fn harmony_vector_health_score_moderate() {
645 let hv = HarmonyVector {
646 cpu_load: 0.5,
647 memory_pressure: 0.4,
648 swap_usage: 0.3,
649 thermal_state: ThermalState::Warm,
650 temperature_c: Some(65.0),
651 battery_state: BatteryState::Discharging,
652 battery_percent: 0.5,
653 disk_io_rate: 0.0,
654 active: true,
655 guna: GunaTag::Sattvic,
656 timestamp: Utc::now(),
657 };
658 let score = hv.health_score();
659 assert!(score > 0.4 && score < 0.7, "Moderate health: {score}");
660 assert!(!hv.is_stressed());
661 }
662
663 #[test]
664 fn harmony_vector_default_is_idle() {
665 let hv = HarmonyVector::default();
666 assert_eq!(hv.guna, GunaTag::Tamasic);
667 assert!(!hv.active);
668 assert_eq!(hv.thermal_state, ThermalState::Unknown);
669 assert_eq!(hv.battery_state, BatteryState::Unknown);
670 }
671
672 #[test]
673 fn harmony_vector_serialization() {
674 let hv = HarmonyVector::default();
675 let json = serde_json::to_string(&hv).unwrap();
676 let back: HarmonyVector = serde_json::from_str(&json).unwrap();
677 assert_eq!(back.cpu_load, hv.cpu_load);
678 assert_eq!(back.thermal_state, hv.thermal_state);
679 }
680
681 #[test]
682 fn harmony_vector_to_json() {
683 let hv = HarmonyVector {
684 cpu_load: 0.3,
685 memory_pressure: 0.2,
686 swap_usage: 0.1,
687 thermal_state: ThermalState::Normal,
688 temperature_c: Some(45.0),
689 battery_state: BatteryState::Full,
690 battery_percent: 1.0,
691 disk_io_rate: 0.0,
692 active: true,
693 guna: GunaTag::Sattvic,
694 timestamp: Utc::now(),
695 };
696 let json = hv.to_json();
697 assert_eq!(json["thermal_state"], "normal");
698 assert_eq!(json["battery_state"], "full");
699 assert_eq!(json["guna"], "sattvic");
700 assert!((json["cpu_load"].as_f64().unwrap() - 0.3).abs() < 0.001);
701 assert!(json["health_score"].as_f64().unwrap() > 0.8);
702 }
703
704 #[test]
705 fn substrate_monitor_sample() {
706 let monitor = SubstrateMonitor::new(10);
707 let hv = monitor.sample();
708 assert!(hv.cpu_load >= 0.0 && hv.cpu_load <= 1.0);
709 assert!(hv.memory_pressure >= 0.0 && hv.memory_pressure <= 1.0);
710 assert_eq!(monitor.history_len(), 1);
711 }
712
713 #[test]
714 fn substrate_monitor_history_grows() {
715 let monitor = SubstrateMonitor::new(5);
716 for _ in 0..3 {
717 let _ = monitor.sample();
718 }
719 assert_eq!(monitor.history_len(), 3);
720 let hist = monitor.history(10);
721 assert_eq!(hist.len(), 3);
722 assert!(hist[0].timestamp >= hist[1].timestamp);
724 }
725
726 #[test]
727 fn substrate_monitor_history_capped() {
728 let monitor = SubstrateMonitor::new(3);
729 for _ in 0..5 {
730 let _ = monitor.sample();
731 }
732 assert_eq!(monitor.history_len(), 3);
733 }
734
735 #[test]
736 fn substrate_monitor_last_sample() {
737 let monitor = SubstrateMonitor::new(10);
738 assert!(monitor.last_sample().is_none());
739 let hv = monitor.sample();
740 let last = monitor.last_sample().unwrap();
741 assert!((last.cpu_load - hv.cpu_load).abs() < 0.01);
742 }
743
744 #[test]
745 fn substrate_monitor_history_limit() {
746 let monitor = SubstrateMonitor::new(100);
747 for _ in 0..10 {
748 let _ = monitor.sample();
749 }
750 let hist = monitor.history(3);
751 assert_eq!(hist.len(), 3);
752 }
753
754 #[test]
755 fn parse_kb_extracts_value() {
756 assert_eq!(parse_kb("MemTotal: 16384000 kB"), Some(16_384_000));
757 assert_eq!(parse_kb("MemAvailable: 8192000 kB"), Some(8_192_000));
758 assert_eq!(parse_kb("garbage"), None);
759 }
760
761 #[test]
762 fn harmony_vector_sanitized_clamps_nan() {
763 let hv = HarmonyVector {
764 cpu_load: f32::NAN,
765 memory_pressure: f32::INFINITY,
766 swap_usage: -0.5,
767 battery_percent: 2.0,
768 disk_io_rate: f32::NEG_INFINITY,
769 temperature_c: Some(500.0),
770 ..Default::default()
771 };
772 let s = hv.sanitized();
773 assert_eq!(s.cpu_load, 0.0, "NaN should become 0.0");
774 assert_eq!(s.memory_pressure, 0.0, "Infinity should become 0.0");
775 assert_eq!(s.swap_usage, 0.0, "Negative should become 0.0");
776 assert_eq!(s.battery_percent, 1.0, "2.0 should be clamped to 1.0");
777 assert_eq!(s.disk_io_rate, 0.0, "NegInfinity should become 0.0");
778 assert!(s.temperature_c.is_none(), "Impossible temp should be None");
779 }
780
781 #[test]
782 fn harmony_vector_sanitized_preserves_valid() {
783 let hv = HarmonyVector {
784 cpu_load: 0.5,
785 memory_pressure: 0.3,
786 swap_usage: 0.1,
787 battery_percent: 0.8,
788 disk_io_rate: 0.2,
789 temperature_c: Some(45.0),
790 ..Default::default()
791 };
792 let s = hv.sanitized();
793 assert_eq!(s.cpu_load, 0.5);
794 assert_eq!(s.memory_pressure, 0.3);
795 assert_eq!(s.temperature_c, Some(45.0));
796 }
797}