1#![allow(dead_code)]
2use crate::stack::quality::{QualityGrade, StackLayer};
16use crate::stack::DependencyGraph;
17use anyhow::Result;
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20
21pub use super::diagnostics_ml::{ErrorForecaster, ForecastMetrics, IsolationForest};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
30pub enum HealthStatus {
31 Green,
33 Yellow,
35 Red,
37 Unknown,
39}
40
41impl HealthStatus {
42 pub fn from_grade(grade: QualityGrade) -> Self {
44 match grade {
45 QualityGrade::APlus | QualityGrade::A => Self::Green,
46 QualityGrade::AMinus | QualityGrade::BPlus => Self::Yellow,
47 _ => Self::Red,
48 }
49 }
50
51 pub fn icon(&self) -> &'static str {
53 match self {
54 Self::Green => "🟢",
55 Self::Yellow => "🟡",
56 Self::Red => "🔴",
57 Self::Unknown => "⚪",
58 }
59 }
60
61 pub fn symbol(&self) -> &'static str {
63 match self {
64 Self::Green => "●",
65 Self::Yellow => "◐",
66 Self::Red => "○",
67 Self::Unknown => "◌",
68 }
69 }
70}
71
72impl std::fmt::Display for HealthStatus {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 write!(f, "{}", self.icon())
75 }
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct ComponentNode {
85 pub name: String,
87 pub version: String,
89 pub layer: StackLayer,
91 pub health: HealthStatus,
93 pub metrics: ComponentMetrics,
95}
96
97impl ComponentNode {
98 pub fn new(name: impl Into<String>, version: impl Into<String>, layer: StackLayer) -> Self {
100 Self {
101 name: name.into(),
102 version: version.into(),
103 layer,
104 health: HealthStatus::Unknown,
105 metrics: ComponentMetrics::default(),
106 }
107 }
108
109 pub fn update_health(&mut self) {
111 self.health = HealthStatus::from_grade(self.metrics.grade);
112 }
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct ComponentMetrics {
122 pub demo_score: f64,
124 pub coverage: f64,
126 pub mutation_score: f64,
128 pub complexity_avg: f64,
130 pub satd_count: u32,
132 pub dead_code_pct: f64,
134 pub grade: QualityGrade,
136}
137
138impl Default for ComponentMetrics {
139 fn default() -> Self {
140 Self {
141 demo_score: 0.0,
142 coverage: 0.0,
143 mutation_score: 0.0,
144 complexity_avg: 0.0,
145 satd_count: 0,
146 dead_code_pct: 0.0,
147 grade: QualityGrade::F, }
149 }
150}
151
152impl ComponentMetrics {
153 pub fn with_demo_score(demo_score: f64) -> Self {
155 let grade = QualityGrade::from_sqi(demo_score);
156 Self {
157 demo_score,
158 grade,
159 ..Default::default()
160 }
161 }
162
163 pub fn meets_threshold(&self) -> bool {
165 self.demo_score >= 85.0
166 }
167}
168
169#[derive(Debug, Clone, Default, Serialize, Deserialize)]
175pub struct GraphMetrics {
176 pub pagerank: HashMap<String, f64>,
178 pub betweenness: HashMap<String, f64>,
180 pub clustering: HashMap<String, f64>,
182 pub communities: HashMap<String, usize>,
184 pub depth_map: HashMap<String, u32>,
186 pub total_nodes: usize,
188 pub total_edges: usize,
190 pub density: f64,
192 pub avg_degree: f64,
194 pub max_depth: u32,
196}
197
198impl GraphMetrics {
199 pub fn top_by_pagerank(&self, n: usize) -> Vec<(&String, f64)> {
201 let mut scores: Vec<_> = self.pagerank.iter().map(|(k, v)| (k, *v)).collect();
202 scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
203 scores.into_iter().take(n).collect()
204 }
205
206 pub fn bottlenecks(&self, threshold: f64) -> Vec<&String> {
208 self.betweenness
209 .iter()
210 .filter(|(_, &v)| v > threshold)
211 .map(|(k, _)| k)
212 .collect()
213 }
214}
215
216#[derive(Debug)]
222pub struct StackDiagnostics {
223 components: HashMap<String, ComponentNode>,
225 graph: Option<DependencyGraph>,
227 metrics: GraphMetrics,
229 anomalies: Vec<Anomaly>,
231}
232
233impl StackDiagnostics {
234 pub fn new() -> Self {
236 Self {
237 components: HashMap::new(),
238 graph: None,
239 metrics: GraphMetrics::default(),
240 anomalies: Vec::new(),
241 }
242 }
243
244 pub fn add_component(&mut self, node: ComponentNode) {
246 self.components.insert(node.name.clone(), node);
247 }
248
249 pub fn get_component(&self, name: &str) -> Option<&ComponentNode> {
251 self.components.get(name)
252 }
253
254 pub fn components(&self) -> impl Iterator<Item = &ComponentNode> {
256 self.components.values()
257 }
258
259 pub fn component_count(&self) -> usize {
261 self.components.len()
262 }
263
264 pub fn set_graph(&mut self, graph: DependencyGraph) {
266 self.graph = Some(graph);
267 }
268
269 pub fn graph(&self) -> Option<&DependencyGraph> {
271 self.graph.as_ref()
272 }
273
274 pub fn compute_metrics(&mut self) -> Result<&GraphMetrics> {
276 let n = self.components.len();
277 if n == 0 {
278 return Ok(&self.metrics);
279 }
280
281 self.metrics.total_nodes = n;
282
283 let adjacency = self.build_adjacency();
285
286 self.compute_pagerank(&adjacency, 0.85, 100);
288
289 self.compute_betweenness(&adjacency);
291
292 self.compute_depth(&adjacency);
294
295 self.metrics.total_edges = adjacency.values().map(|v| v.len()).sum();
297 let max_edges = n * (n.saturating_sub(1));
298 self.metrics.density = if max_edges > 0 {
299 self.metrics.total_edges as f64 / max_edges as f64
300 } else {
301 0.0
302 };
303 self.metrics.avg_degree = if n > 0 {
304 self.metrics.total_edges as f64 / n as f64
305 } else {
306 0.0
307 };
308 self.metrics.max_depth = self.metrics.depth_map.values().copied().max().unwrap_or(0);
309
310 Ok(&self.metrics)
311 }
312
313 fn build_adjacency(&self) -> HashMap<String, Vec<String>> {
315 let mut adjacency: HashMap<String, Vec<String>> = HashMap::new();
316
317 for name in self.components.keys() {
319 adjacency.insert(name.clone(), Vec::new());
320 }
321
322 if let Some(graph) = &self.graph {
324 for crate_info in graph.all_crates() {
325 let from = &crate_info.name;
326 for dep in &crate_info.paiml_dependencies {
327 if self.components.contains_key(&dep.name) {
328 adjacency
329 .entry(from.clone())
330 .or_default()
331 .push(dep.name.clone());
332 }
333 }
334 }
335 }
336
337 adjacency
338 }
339
340 fn compute_pagerank(
342 &mut self,
343 adjacency: &HashMap<String, Vec<String>>,
344 damping: f64,
345 max_iter: usize,
346 ) {
347 let n = self.components.len();
348 if n == 0 {
349 return;
350 }
351
352 let initial = 1.0 / n as f64;
353 let mut scores: HashMap<String, f64> = self
354 .components
355 .keys()
356 .map(|k| (k.clone(), initial))
357 .collect();
358
359 let dangling_nodes: Vec<_> = adjacency
361 .iter()
362 .filter(|(_, targets)| targets.is_empty())
363 .map(|(node, _)| node.clone())
364 .collect();
365
366 for _ in 0..max_iter {
368 let mut new_scores: HashMap<String, f64> = HashMap::new();
369 let teleport = (1.0 - damping) / n as f64;
370
371 let dangling_sum: f64 = dangling_nodes
373 .iter()
374 .map(|node| scores.get(node).unwrap_or(&0.0))
375 .sum();
376 let dangling_contrib = damping * dangling_sum / n as f64;
377
378 for node in self.components.keys() {
379 let mut incoming_score = 0.0;
380
381 for (source, targets) in adjacency {
383 if targets.contains(node) {
384 let out_degree = targets.len();
385 if out_degree > 0 {
386 incoming_score +=
387 scores.get(source).unwrap_or(&0.0) / out_degree as f64;
388 }
389 }
390 }
391
392 new_scores.insert(
393 node.clone(),
394 teleport + damping * incoming_score + dangling_contrib,
395 );
396 }
397
398 let diff: f64 = new_scores
400 .iter()
401 .map(|(k, v)| (v - scores.get(k).unwrap_or(&0.0)).abs())
402 .sum();
403
404 scores = new_scores;
405
406 if diff < 1e-6 {
407 break;
408 }
409 }
410
411 self.metrics.pagerank = scores;
412 }
413
414 fn compute_betweenness(&mut self, adjacency: &HashMap<String, Vec<String>>) {
416 let nodes: Vec<_> = self.components.keys().cloned().collect();
417 let n = nodes.len();
418
419 let mut betweenness: HashMap<String, f64> =
421 nodes.iter().map(|n| (n.clone(), 0.0)).collect();
422
423 for source in &nodes {
425 let mut dist: HashMap<String, i32> = HashMap::new();
427 let mut sigma: HashMap<String, f64> = HashMap::new();
428 let mut predecessors: HashMap<String, Vec<String>> = HashMap::new();
429
430 for n in &nodes {
431 dist.insert(n.clone(), -1);
432 sigma.insert(n.clone(), 0.0);
433 predecessors.insert(n.clone(), Vec::new());
434 }
435
436 dist.insert(source.clone(), 0);
437 sigma.insert(source.clone(), 1.0);
438
439 let mut queue = vec![source.clone()];
440 let mut order = Vec::new();
441
442 while !queue.is_empty() {
443 let v = queue.remove(0);
444 order.push(v.clone());
445
446 if let Some(neighbors) = adjacency.get(&v) {
447 for w in neighbors {
448 let d_v = dist[&v];
449 let d_w = dist.get(w).copied().unwrap_or(-1);
450
451 if d_w < 0 {
452 dist.insert(w.clone(), d_v + 1);
453 queue.push(w.clone());
454 }
455
456 if dist.get(w).copied().unwrap_or(-1) == d_v + 1 {
457 let sigma_v = sigma.get(&v).copied().unwrap_or(0.0);
458 if let Some(s) = sigma.get_mut(w) {
459 *s += sigma_v;
460 }
461 if let Some(p) = predecessors.get_mut(w) {
462 p.push(v.clone());
463 }
464 }
465 }
466 }
467 }
468
469 let mut delta: HashMap<String, f64> = nodes.iter().map(|n| (n.clone(), 0.0)).collect();
471
472 for w in order.iter().rev() {
473 for v in predecessors.get(w).cloned().unwrap_or_default() {
474 let sigma_v = sigma.get(&v).copied().unwrap_or(1.0);
475 let sigma_w = sigma.get(w).copied().unwrap_or(1.0);
476 let delta_w = delta.get(w).copied().unwrap_or(0.0);
477
478 if sigma_w > 0.0 {
479 if let Some(d) = delta.get_mut(&v) {
480 *d += (sigma_v / sigma_w) * (1.0 + delta_w);
481 }
482 }
483 }
484
485 if w != source {
486 if let Some(b) = betweenness.get_mut(w) {
487 *b += delta.get(w).copied().unwrap_or(0.0);
488 }
489 }
490 }
491 }
492
493 let norm = if n > 2 { (n - 1) * (n - 2) } else { 1 };
495 for v in betweenness.values_mut() {
496 *v /= norm as f64;
497 }
498
499 self.metrics.betweenness = betweenness;
500 }
501
502 fn compute_depth(&mut self, adjacency: &HashMap<String, Vec<String>>) {
504 let mut depth: HashMap<String, u32> = HashMap::new();
505 let nodes: Vec<_> = self.components.keys().cloned().collect();
506
507 let mut has_incoming: HashMap<String, bool> =
509 nodes.iter().map(|n| (n.clone(), false)).collect();
510 for targets in adjacency.values() {
511 for t in targets {
512 has_incoming.insert(t.clone(), true);
513 }
514 }
515
516 let roots: Vec<_> = nodes
518 .iter()
519 .filter(|n| !has_incoming.get(*n).unwrap_or(&false))
520 .cloned()
521 .collect();
522
523 let mut queue: Vec<(String, u32)> = roots.into_iter().map(|r| (r, 0)).collect();
525
526 while let Some((node, d)) = queue.pop() {
527 if let std::collections::hash_map::Entry::Vacant(e) = depth.entry(node.clone()) {
528 e.insert(d);
529 if let Some(neighbors) = adjacency.get(&node) {
530 for neighbor in neighbors {
531 if !depth.contains_key(neighbor) {
532 queue.push((neighbor.clone(), d + 1));
533 }
534 }
535 }
536 }
537 }
538
539 for node in &nodes {
541 depth.entry(node.clone()).or_insert(0);
542 }
543
544 self.metrics.depth_map = depth;
545 }
546
547 pub fn metrics(&self) -> &GraphMetrics {
549 &self.metrics
550 }
551
552 pub fn anomalies(&self) -> &[Anomaly] {
554 &self.anomalies
555 }
556
557 pub fn add_anomaly(&mut self, anomaly: Anomaly) {
559 self.anomalies.push(anomaly);
560 }
561
562 pub fn health_summary(&self) -> HealthSummary {
564 let total = self.components.len();
565 let green = self
566 .components
567 .values()
568 .filter(|c| c.health == HealthStatus::Green)
569 .count();
570 let yellow = self
571 .components
572 .values()
573 .filter(|c| c.health == HealthStatus::Yellow)
574 .count();
575 let red = self
576 .components
577 .values()
578 .filter(|c| c.health == HealthStatus::Red)
579 .count();
580
581 let avg_score = if total > 0 {
582 self.components
583 .values()
584 .map(|c| c.metrics.demo_score)
585 .sum::<f64>()
586 / total as f64
587 } else {
588 0.0
589 };
590
591 HealthSummary {
592 total_components: total,
593 green_count: green,
594 yellow_count: yellow,
595 red_count: red,
596 unknown_count: total.saturating_sub(green + yellow + red),
597 avg_demo_score: avg_score,
598 avg_coverage: self.avg_metric(|c| c.metrics.coverage),
599 andon_status: self.compute_andon_status(green, yellow, red, total),
600 }
601 }
602
603 fn avg_metric<F>(&self, f: F) -> f64
604 where
605 F: Fn(&ComponentNode) -> f64,
606 {
607 let total = self.components.len();
608 if total == 0 {
609 return 0.0;
610 }
611 self.components.values().map(f).sum::<f64>() / total as f64
612 }
613
614 fn compute_andon_status(
615 &self,
616 green: usize,
617 yellow: usize,
618 red: usize,
619 total: usize,
620 ) -> AndonStatus {
621 if red > 0 {
622 AndonStatus::Red
623 } else if yellow > 0 {
624 AndonStatus::Yellow
625 } else if green == total && total > 0 {
626 AndonStatus::Green
627 } else {
628 AndonStatus::Unknown
629 }
630 }
631}
632
633impl Default for StackDiagnostics {
634 fn default() -> Self {
635 Self::new()
636 }
637}
638
639#[derive(Debug, Clone, Serialize, Deserialize)]
645pub struct HealthSummary {
646 pub total_components: usize,
648 pub green_count: usize,
650 pub yellow_count: usize,
652 pub red_count: usize,
654 pub unknown_count: usize,
656 pub avg_demo_score: f64,
658 pub avg_coverage: f64,
660 pub andon_status: AndonStatus,
662}
663
664impl HealthSummary {
665 pub fn all_healthy(&self) -> bool {
667 self.red_count == 0 && self.yellow_count == 0 && self.green_count == self.total_components
668 }
669
670 pub fn health_percentage(&self) -> f64 {
672 if self.total_components == 0 {
673 return 0.0;
674 }
675 (self.green_count as f64 / self.total_components as f64) * 100.0
676 }
677}
678
679#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
685pub enum AndonStatus {
686 Green,
688 Yellow,
690 Red,
692 Unknown,
694}
695
696impl AndonStatus {
697 pub fn message(&self) -> &'static str {
699 match self {
700 Self::Green => "All systems healthy",
701 Self::Yellow => "Attention needed",
702 Self::Red => "Stop-the-line",
703 Self::Unknown => "Analysis pending",
704 }
705 }
706}
707
708impl std::fmt::Display for AndonStatus {
709 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
710 let icon = match self {
711 Self::Green => "🟢",
712 Self::Yellow => "🟡",
713 Self::Red => "🔴",
714 Self::Unknown => "⚪",
715 };
716 write!(f, "{} {}", icon, self.message())
717 }
718}
719
720#[derive(Debug, Clone, Serialize, Deserialize)]
726pub struct Anomaly {
727 pub component: String,
729 pub score: f64,
731 pub category: AnomalyCategory,
733 pub description: String,
735 pub evidence: Vec<String>,
737 pub recommendation: Option<String>,
739}
740
741impl Anomaly {
742 pub fn new(
744 component: impl Into<String>,
745 score: f64,
746 category: AnomalyCategory,
747 description: impl Into<String>,
748 ) -> Self {
749 Self {
750 component: component.into(),
751 score,
752 category,
753 description: description.into(),
754 evidence: Vec::new(),
755 recommendation: None,
756 }
757 }
758
759 pub fn with_evidence(mut self, evidence: impl Into<String>) -> Self {
761 self.evidence.push(evidence.into());
762 self
763 }
764
765 pub fn with_recommendation(mut self, rec: impl Into<String>) -> Self {
767 self.recommendation = Some(rec.into());
768 self
769 }
770
771 pub fn is_critical(&self) -> bool {
773 self.score > 0.8
774 }
775}
776
777#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
779pub enum AnomalyCategory {
780 QualityRegression,
782 CoverageDrop,
784 BuildTimeSpike,
786 DependencyRisk,
788 ComplexityIncrease,
790 Other,
792}
793
794impl std::fmt::Display for AnomalyCategory {
795 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
796 match self {
797 Self::QualityRegression => write!(f, "Quality Regression"),
798 Self::CoverageDrop => write!(f, "Coverage Drop"),
799 Self::BuildTimeSpike => write!(f, "Build Time Spike"),
800 Self::DependencyRisk => write!(f, "Dependency Risk"),
801 Self::ComplexityIncrease => write!(f, "Complexity Increase"),
802 Self::Other => write!(f, "Other"),
803 }
804 }
805}
806
807pub fn render_dashboard(diagnostics: &StackDiagnostics) -> String {
813 let mut output = String::new();
814 let summary = diagnostics.health_summary();
815
816 output
818 .push_str("┌─────────────────────────────────────────────────────────────────────────┐\n");
819 output
820 .push_str("│ SOVEREIGN AI STACK HEALTH DASHBOARD │\n");
821 output.push_str(&format!(
822 "│ Timestamp: {:40} │\n",
823 chrono::Utc::now().format("%Y-%m-%d %H:%M:%S")
824 ));
825 output
826 .push_str("├─────────────────────────────────────────────────────────────────────────┤\n");
827
828 output
830 .push_str("│ │\n");
831 output.push_str(&format!(
832 "│ ANDON STATUS: {} {:55}│\n",
833 summary.andon_status, ""
834 ));
835 output
836 .push_str("│ │\n");
837
838 output.push_str("│ ═══════════════════════════════════════════════════════════════════ │\n");
840 output
841 .push_str("│ STACK SUMMARY │\n");
842 output.push_str("│ ═══════════════════════════════════════════════════════════════════ │\n");
843 output
844 .push_str("│ │\n");
845 output.push_str(&format!(
846 "│ Total Components: {:3} │\n",
847 summary.total_components
848 ));
849 output.push_str(&format!(
850 "│ Healthy: {:3} ({:.0}%) │\n",
851 summary.green_count,
852 summary.health_percentage()
853 ));
854 output.push_str(&format!(
855 "│ Warnings: {:3} ({:.0}%) │\n",
856 summary.yellow_count,
857 if summary.total_components > 0 {
858 (summary.yellow_count as f64 / summary.total_components as f64) * 100.0
859 } else {
860 0.0
861 }
862 ));
863 output.push_str(&format!(
864 "│ Critical: {:3} ({:.0}%) │\n",
865 summary.red_count,
866 if summary.total_components > 0 {
867 (summary.red_count as f64 / summary.total_components as f64) * 100.0
868 } else {
869 0.0
870 }
871 ));
872 output.push_str(&format!(
873 "│ Average Demo Score: {:.1}/100 │\n",
874 summary.avg_demo_score
875 ));
876 output.push_str(&format!(
877 "│ Average Coverage: {:.1}% │\n",
878 summary.avg_coverage
879 ));
880 output
881 .push_str("│ │\n");
882
883 let anomalies = diagnostics.anomalies();
885 if !anomalies.is_empty() {
886 output.push_str(
887 "│ ═══════════════════════════════════════════════════════════════════ │\n",
888 );
889 output.push_str(
890 "│ ANOMALIES DETECTED │\n",
891 );
892 output.push_str(
893 "│ ═══════════════════════════════════════════════════════════════════ │\n",
894 );
895 output.push_str(
896 "│ │\n",
897 );
898
899 for anomaly in anomalies.iter().take(5) {
900 let icon = if anomaly.is_critical() {
901 "🔴"
902 } else {
903 "⚠️"
904 };
905 output.push_str(&format!(
906 "│ {} {}: {} │\n",
907 icon, anomaly.component, anomaly.description
908 ));
909 }
910 output.push_str(
911 "│ │\n",
912 );
913 }
914
915 output
916 .push_str("└─────────────────────────────────────────────────────────────────────────┘\n");
917
918 output
919}
920
921#[cfg(test)]
926mod tests {
927 use super::*;
928
929 #[test]
934 fn test_health_status_from_grade_green() {
935 assert_eq!(
936 HealthStatus::from_grade(QualityGrade::APlus),
937 HealthStatus::Green
938 );
939 assert_eq!(
940 HealthStatus::from_grade(QualityGrade::A),
941 HealthStatus::Green
942 );
943 }
944
945 #[test]
946 fn test_health_status_from_grade_yellow() {
947 assert_eq!(
948 HealthStatus::from_grade(QualityGrade::AMinus),
949 HealthStatus::Yellow
950 );
951 assert_eq!(
952 HealthStatus::from_grade(QualityGrade::BPlus),
953 HealthStatus::Yellow
954 );
955 }
956
957 #[test]
958 fn test_health_status_from_grade_red() {
959 assert_eq!(HealthStatus::from_grade(QualityGrade::B), HealthStatus::Red);
960 assert_eq!(HealthStatus::from_grade(QualityGrade::C), HealthStatus::Red);
961 assert_eq!(HealthStatus::from_grade(QualityGrade::F), HealthStatus::Red);
962 }
963
964 #[test]
965 fn test_health_status_icons() {
966 assert_eq!(HealthStatus::Green.icon(), "🟢");
967 assert_eq!(HealthStatus::Yellow.icon(), "🟡");
968 assert_eq!(HealthStatus::Red.icon(), "🔴");
969 assert_eq!(HealthStatus::Unknown.icon(), "⚪");
970 }
971
972 #[test]
973 fn test_health_status_symbols() {
974 assert_eq!(HealthStatus::Green.symbol(), "●");
975 assert_eq!(HealthStatus::Yellow.symbol(), "◐");
976 assert_eq!(HealthStatus::Red.symbol(), "○");
977 assert_eq!(HealthStatus::Unknown.symbol(), "◌");
978 }
979
980 #[test]
985 fn test_component_node_creation() {
986 let node = ComponentNode::new("trueno", "0.7.4", StackLayer::Compute);
987 assert_eq!(node.name, "trueno");
988 assert_eq!(node.version, "0.7.4");
989 assert_eq!(node.layer, StackLayer::Compute);
990 assert_eq!(node.health, HealthStatus::Unknown);
991 }
992
993 #[test]
994 fn test_component_node_update_health() {
995 let mut node = ComponentNode::new("trueno", "0.7.4", StackLayer::Compute);
996 node.metrics = ComponentMetrics::with_demo_score(95.0);
997 node.update_health();
998 assert_eq!(node.health, HealthStatus::Green);
999 }
1000
1001 #[test]
1002 fn test_component_node_update_health_yellow() {
1003 let mut node = ComponentNode::new("test", "1.0.0", StackLayer::Ml);
1004 node.metrics = ComponentMetrics::with_demo_score(85.0);
1005 node.update_health();
1006 assert_eq!(node.health, HealthStatus::Yellow);
1007 }
1008
1009 #[test]
1010 fn test_component_node_update_health_red() {
1011 let mut node = ComponentNode::new("test", "1.0.0", StackLayer::Ml);
1012 node.metrics = ComponentMetrics::with_demo_score(65.0);
1013 node.update_health();
1014 assert_eq!(node.health, HealthStatus::Red);
1015 }
1016
1017 #[test]
1022 fn test_component_metrics_default() {
1023 let metrics = ComponentMetrics::default();
1024 assert_eq!(metrics.demo_score, 0.0);
1025 assert_eq!(metrics.coverage, 0.0);
1026 assert!(!metrics.meets_threshold());
1027 }
1028
1029 #[test]
1030 fn test_component_metrics_with_demo_score() {
1031 let metrics = ComponentMetrics::with_demo_score(90.0);
1032 assert_eq!(metrics.demo_score, 90.0);
1033 assert!(metrics.meets_threshold());
1034 }
1035
1036 #[test]
1037 fn test_component_metrics_threshold() {
1038 assert!(ComponentMetrics::with_demo_score(85.0).meets_threshold());
1039 assert!(ComponentMetrics::with_demo_score(100.0).meets_threshold());
1040 assert!(!ComponentMetrics::with_demo_score(84.9).meets_threshold());
1041 }
1042
1043 #[test]
1048 fn test_graph_metrics_top_by_pagerank() {
1049 let mut metrics = GraphMetrics::default();
1050 metrics.pagerank.insert("trueno".to_string(), 0.25);
1051 metrics.pagerank.insert("aprender".to_string(), 0.15);
1052 metrics.pagerank.insert("batuta".to_string(), 0.10);
1053
1054 let top = metrics.top_by_pagerank(2);
1055 assert_eq!(top.len(), 2);
1056 assert_eq!(top[0].0, "trueno");
1057 assert_eq!(top[1].0, "aprender");
1058 }
1059
1060 #[test]
1061 fn test_graph_metrics_bottlenecks() {
1062 let mut metrics = GraphMetrics::default();
1063 metrics.betweenness.insert("trueno".to_string(), 0.8);
1064 metrics.betweenness.insert("aprender".to_string(), 0.3);
1065 metrics.betweenness.insert("batuta".to_string(), 0.1);
1066
1067 let bottlenecks = metrics.bottlenecks(0.5);
1068 assert_eq!(bottlenecks.len(), 1);
1069 assert!(bottlenecks.contains(&&"trueno".to_string()));
1070 }
1071
1072 #[test]
1077 fn test_stack_diagnostics_new() {
1078 let diag = StackDiagnostics::new();
1079 assert_eq!(diag.component_count(), 0);
1080 assert!(diag.graph().is_none());
1081 assert!(diag.anomalies().is_empty());
1082 }
1083
1084 #[test]
1085 fn test_stack_diagnostics_add_component() {
1086 let mut diag = StackDiagnostics::new();
1087 let node = ComponentNode::new("trueno", "0.7.4", StackLayer::Compute);
1088 diag.add_component(node);
1089
1090 assert_eq!(diag.component_count(), 1);
1091 assert!(diag.get_component("trueno").is_some());
1092 assert!(diag.get_component("missing").is_none());
1093 }
1094
1095 #[test]
1096 fn test_stack_diagnostics_health_summary_empty() {
1097 let diag = StackDiagnostics::new();
1098 let summary = diag.health_summary();
1099
1100 assert_eq!(summary.total_components, 0);
1101 assert_eq!(summary.green_count, 0);
1102 assert_eq!(summary.andon_status, AndonStatus::Unknown);
1103 }
1104
1105 #[test]
1106 fn test_stack_diagnostics_health_summary_all_green() {
1107 let mut diag = StackDiagnostics::new();
1108
1109 let mut node1 = ComponentNode::new("trueno", "0.7.4", StackLayer::Compute);
1110 node1.health = HealthStatus::Green;
1111 node1.metrics = ComponentMetrics::with_demo_score(95.0);
1112 diag.add_component(node1);
1113
1114 let mut node2 = ComponentNode::new("aprender", "0.9.0", StackLayer::Ml);
1115 node2.health = HealthStatus::Green;
1116 node2.metrics = ComponentMetrics::with_demo_score(92.0);
1117 diag.add_component(node2);
1118
1119 let summary = diag.health_summary();
1120
1121 assert_eq!(summary.total_components, 2);
1122 assert_eq!(summary.green_count, 2);
1123 assert_eq!(summary.yellow_count, 0);
1124 assert_eq!(summary.red_count, 0);
1125 assert!(summary.all_healthy());
1126 assert_eq!(summary.andon_status, AndonStatus::Green);
1127 assert!((summary.avg_demo_score - 93.5).abs() < 0.1);
1128 }
1129
1130 #[test]
1131 fn test_stack_diagnostics_health_summary_mixed() {
1132 let mut diag = StackDiagnostics::new();
1133
1134 let mut node1 = ComponentNode::new("trueno", "0.7.4", StackLayer::Compute);
1135 node1.health = HealthStatus::Green;
1136 diag.add_component(node1);
1137
1138 let mut node2 = ComponentNode::new("weak", "1.0.0", StackLayer::Ml);
1139 node2.health = HealthStatus::Red;
1140 diag.add_component(node2);
1141
1142 let summary = diag.health_summary();
1143
1144 assert_eq!(summary.green_count, 1);
1145 assert_eq!(summary.red_count, 1);
1146 assert!(!summary.all_healthy());
1147 assert_eq!(summary.andon_status, AndonStatus::Red);
1148 }
1149
1150 #[test]
1151 fn test_stack_diagnostics_add_anomaly() {
1152 let mut diag = StackDiagnostics::new();
1153
1154 let anomaly = Anomaly::new(
1155 "trueno-graph",
1156 0.75,
1157 AnomalyCategory::CoverageDrop,
1158 "Coverage dropped 5.2%",
1159 )
1160 .with_evidence("lcov.info shows missing tests")
1161 .with_recommendation("Add tests for GPU BFS");
1162
1163 diag.add_anomaly(anomaly);
1164
1165 assert_eq!(diag.anomalies().len(), 1);
1166 assert_eq!(diag.anomalies()[0].component, "trueno-graph");
1167 assert!(!diag.anomalies()[0].is_critical());
1168 }
1169
1170 #[test]
1175 fn test_health_summary_percentage() {
1176 let summary = HealthSummary {
1177 total_components: 20,
1178 green_count: 17,
1179 yellow_count: 3,
1180 red_count: 0,
1181 unknown_count: 0,
1182 avg_demo_score: 85.0,
1183 avg_coverage: 90.0,
1184 andon_status: AndonStatus::Yellow,
1185 };
1186
1187 assert_eq!(summary.health_percentage(), 85.0);
1188 assert!(!summary.all_healthy());
1189 }
1190
1191 #[test]
1192 fn test_health_summary_percentage_empty() {
1193 let summary = HealthSummary {
1194 total_components: 0,
1195 green_count: 0,
1196 yellow_count: 0,
1197 red_count: 0,
1198 unknown_count: 0,
1199 avg_demo_score: 0.0,
1200 avg_coverage: 0.0,
1201 andon_status: AndonStatus::Unknown,
1202 };
1203
1204 assert_eq!(summary.health_percentage(), 0.0);
1205 }
1206
1207 #[test]
1212 fn test_anomaly_creation() {
1213 let anomaly = Anomaly::new(
1214 "test",
1215 0.65,
1216 AnomalyCategory::QualityRegression,
1217 "Score dropped",
1218 );
1219
1220 assert_eq!(anomaly.component, "test");
1221 assert_eq!(anomaly.score, 0.65);
1222 assert!(!anomaly.is_critical());
1223 assert!(anomaly.evidence.is_empty());
1224 assert!(anomaly.recommendation.is_none());
1225 }
1226
1227 #[test]
1228 fn test_anomaly_critical() {
1229 let critical = Anomaly::new("test", 0.85, AnomalyCategory::DependencyRisk, "High risk");
1230 assert!(critical.is_critical());
1231
1232 let non_critical = Anomaly::new("test", 0.79, AnomalyCategory::Other, "Low risk");
1233 assert!(!non_critical.is_critical());
1234 }
1235
1236 #[test]
1237 fn test_anomaly_with_details() {
1238 let anomaly = Anomaly::new("test", 0.7, AnomalyCategory::BuildTimeSpike, "Build slow")
1239 .with_evidence("Time increased 40%")
1240 .with_evidence("New macro expansion")
1241 .with_recommendation("Enable incremental compilation");
1242
1243 assert_eq!(anomaly.evidence.len(), 2);
1244 assert!(anomaly.recommendation.is_some());
1245 }
1246
1247 #[test]
1248 fn test_anomaly_category_display() {
1249 assert_eq!(
1250 format!("{}", AnomalyCategory::QualityRegression),
1251 "Quality Regression"
1252 );
1253 assert_eq!(
1254 format!("{}", AnomalyCategory::CoverageDrop),
1255 "Coverage Drop"
1256 );
1257 assert_eq!(
1258 format!("{}", AnomalyCategory::BuildTimeSpike),
1259 "Build Time Spike"
1260 );
1261 }
1262
1263 #[test]
1268 fn test_andon_status_messages() {
1269 assert_eq!(AndonStatus::Green.message(), "All systems healthy");
1270 assert_eq!(AndonStatus::Yellow.message(), "Attention needed");
1271 assert_eq!(AndonStatus::Red.message(), "Stop-the-line");
1272 assert_eq!(AndonStatus::Unknown.message(), "Analysis pending");
1273 }
1274
1275 #[test]
1276 fn test_andon_status_display() {
1277 let green = format!("{}", AndonStatus::Green);
1278 assert!(green.contains("🟢"));
1279 assert!(green.contains("healthy"));
1280 }
1281
1282 #[test]
1287 fn test_render_dashboard_empty() {
1288 let diag = StackDiagnostics::new();
1289 let output = render_dashboard(&diag);
1290
1291 assert!(output.contains("SOVEREIGN AI STACK"));
1292 assert!(output.contains("ANDON STATUS"));
1293 assert!(output.contains("Total Components"));
1294 }
1295
1296 #[test]
1297 fn test_render_dashboard_with_components() {
1298 let mut diag = StackDiagnostics::new();
1299
1300 let mut node = ComponentNode::new("trueno", "0.7.4", StackLayer::Compute);
1301 node.health = HealthStatus::Green;
1302 node.metrics = ComponentMetrics::with_demo_score(92.0);
1303 diag.add_component(node);
1304
1305 let output = render_dashboard(&diag);
1306
1307 assert!(output.contains("Total Components: 1"));
1308 assert!(output.contains("Healthy: 1"));
1309 }
1310
1311 #[test]
1312 fn test_render_dashboard_with_anomalies() {
1313 let mut diag = StackDiagnostics::new();
1314
1315 diag.add_anomaly(Anomaly::new(
1316 "trueno-graph",
1317 0.75,
1318 AnomalyCategory::CoverageDrop,
1319 "Coverage dropped",
1320 ));
1321
1322 let output = render_dashboard(&diag);
1323 assert!(output.contains("ANOMALIES DETECTED"));
1324 assert!(output.contains("trueno-graph"));
1325 }
1326
1327 #[test]
1332 fn test_compute_metrics_empty() {
1333 let mut diag = StackDiagnostics::new();
1334 let metrics = diag.compute_metrics().unwrap();
1335
1336 assert_eq!(metrics.total_nodes, 0);
1337 assert_eq!(metrics.total_edges, 0);
1338 assert_eq!(metrics.density, 0.0);
1339 }
1340
1341 #[test]
1342 fn test_compute_metrics_single_node() {
1343 let mut diag = StackDiagnostics::new();
1344 diag.add_component(ComponentNode::new("trueno", "0.7.4", StackLayer::Compute));
1345
1346 let metrics = diag.compute_metrics().unwrap();
1347
1348 assert_eq!(metrics.total_nodes, 1);
1349 assert_eq!(metrics.total_edges, 0);
1350 assert_eq!(metrics.density, 0.0);
1351 assert_eq!(metrics.avg_degree, 0.0);
1352
1353 let pagerank = metrics.pagerank.get("trueno").copied().unwrap_or(0.0);
1355 assert!(
1356 (pagerank - 1.0).abs() < 0.01,
1357 "Single node PageRank should be ~1.0"
1358 );
1359
1360 assert_eq!(metrics.depth_map.get("trueno").copied(), Some(0));
1362 }
1363
1364 #[test]
1365 fn test_compute_metrics_pagerank_chain() {
1366 let mut diag = StackDiagnostics::new();
1367
1368 diag.add_component(ComponentNode::new("A", "1.0", StackLayer::Orchestration));
1370 diag.add_component(ComponentNode::new("B", "1.0", StackLayer::Ml));
1371 diag.add_component(ComponentNode::new("C", "1.0", StackLayer::Compute));
1372
1373 let metrics = diag.compute_metrics().unwrap();
1374
1375 assert!(metrics.pagerank.contains_key("A"));
1377 assert!(metrics.pagerank.contains_key("B"));
1378 assert!(metrics.pagerank.contains_key("C"));
1379
1380 let sum: f64 = metrics.pagerank.values().sum();
1382 assert!((sum - 1.0).abs() < 0.01, "PageRank sum should be ~1.0");
1383 }
1384
1385 #[test]
1386 fn test_compute_metrics_betweenness() {
1387 let mut diag = StackDiagnostics::new();
1388
1389 diag.add_component(ComponentNode::new("hub", "1.0", StackLayer::Compute));
1391 diag.add_component(ComponentNode::new("leaf1", "1.0", StackLayer::Ml));
1392 diag.add_component(ComponentNode::new("leaf2", "1.0", StackLayer::DataMlops));
1393 diag.add_component(ComponentNode::new(
1394 "leaf3",
1395 "1.0",
1396 StackLayer::Orchestration,
1397 ));
1398
1399 let metrics = diag.compute_metrics().unwrap();
1400
1401 assert!(metrics.betweenness.contains_key("hub"));
1403 assert!(metrics.betweenness.contains_key("leaf1"));
1404 assert!(metrics.betweenness.contains_key("leaf2"));
1405 assert!(metrics.betweenness.contains_key("leaf3"));
1406
1407 for &v in metrics.betweenness.values() {
1409 assert_eq!(v, 0.0);
1410 }
1411 }
1412
1413 #[test]
1414 fn test_compute_metrics_depth() {
1415 let mut diag = StackDiagnostics::new();
1416
1417 diag.add_component(ComponentNode::new("root1", "1.0", StackLayer::Compute));
1419 diag.add_component(ComponentNode::new("root2", "1.0", StackLayer::Ml));
1420 diag.add_component(ComponentNode::new("root3", "1.0", StackLayer::DataMlops));
1421
1422 let metrics = diag.compute_metrics().unwrap();
1423
1424 assert_eq!(metrics.depth_map.get("root1").copied(), Some(0));
1426 assert_eq!(metrics.depth_map.get("root2").copied(), Some(0));
1427 assert_eq!(metrics.depth_map.get("root3").copied(), Some(0));
1428 assert_eq!(metrics.max_depth, 0);
1429 }
1430
1431 #[test]
1432 fn test_compute_metrics_graph_density() {
1433 let mut diag = StackDiagnostics::new();
1434
1435 diag.add_component(ComponentNode::new("A", "1.0", StackLayer::Compute));
1437 diag.add_component(ComponentNode::new("B", "1.0", StackLayer::Ml));
1438 diag.add_component(ComponentNode::new("C", "1.0", StackLayer::DataMlops));
1439
1440 let metrics = diag.compute_metrics().unwrap();
1441
1442 assert_eq!(metrics.total_nodes, 3);
1444 assert_eq!(metrics.total_edges, 0);
1445 assert_eq!(metrics.density, 0.0);
1446
1447 }
1450
1451 #[test]
1452 fn test_compute_metrics_avg_degree() {
1453 let mut diag = StackDiagnostics::new();
1454
1455 diag.add_component(ComponentNode::new("node1", "1.0", StackLayer::Compute));
1456 diag.add_component(ComponentNode::new("node2", "1.0", StackLayer::Ml));
1457
1458 let metrics = diag.compute_metrics().unwrap();
1459
1460 assert_eq!(metrics.total_nodes, 2);
1461 assert_eq!(metrics.avg_degree, 0.0);
1462 }
1463
1464 #[test]
1465 fn test_build_adjacency_no_graph() {
1466 let mut diag = StackDiagnostics::new();
1467 diag.add_component(ComponentNode::new("A", "1.0", StackLayer::Compute));
1468 diag.add_component(ComponentNode::new("B", "1.0", StackLayer::Ml));
1469
1470 let metrics = diag.compute_metrics().unwrap();
1472
1473 assert_eq!(metrics.total_edges, 0);
1475 }
1476
1477 #[test]
1478 fn test_graph_metrics_top_by_pagerank_empty() {
1479 let metrics = GraphMetrics::default();
1480 let top = metrics.top_by_pagerank(5);
1481 assert!(top.is_empty());
1482 }
1483
1484 #[test]
1485 fn test_graph_metrics_bottlenecks_empty() {
1486 let metrics = GraphMetrics::default();
1487 let bottlenecks = metrics.bottlenecks(0.5);
1488 assert!(bottlenecks.is_empty());
1489 }
1490
1491 #[test]
1492 fn test_compute_metrics_pagerank_convergence() {
1493 let mut diag = StackDiagnostics::new();
1494
1495 for i in 0..10 {
1497 diag.add_component(ComponentNode::new(
1498 format!("node{}", i),
1499 "1.0",
1500 StackLayer::Compute,
1501 ));
1502 }
1503
1504 let metrics = diag.compute_metrics().unwrap();
1505
1506 assert_eq!(metrics.pagerank.len(), 10);
1508
1509 let sum: f64 = metrics.pagerank.values().sum();
1511 assert!(
1512 (sum - 1.0).abs() < 0.01,
1513 "PageRank sum={} should be ~1.0",
1514 sum
1515 );
1516 }
1517
1518 #[test]
1519 fn test_compute_metrics_multiple_calls() {
1520 let mut diag = StackDiagnostics::new();
1521 diag.add_component(ComponentNode::new("X", "1.0", StackLayer::Compute));
1522
1523 let _ = diag.compute_metrics().unwrap();
1525 let metrics = diag.compute_metrics().unwrap();
1526
1527 assert_eq!(metrics.total_nodes, 1);
1529 assert!(metrics.pagerank.contains_key("X"));
1530 }
1531
1532}