ricecoder_orchestration/managers/
status_reporter.rs

1//! Status reporting for workspace metrics and health indicators
2
3use crate::models::{HealthStatus, ProjectStatus, Workspace, WorkspaceMetrics};
4use crate::OrchestrationError;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// Collects and reports workspace metrics and health indicators
9#[derive(Debug, Clone)]
10pub struct StatusReporter {
11    /// Workspace being reported on
12    workspace: Workspace,
13}
14
15/// Detailed status report for a workspace
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct StatusReport {
18    /// Overall workspace health status
19    pub health_status: HealthStatus,
20
21    /// Compliance score (0.0 to 1.0)
22    pub compliance_score: f64,
23
24    /// Total number of projects
25    pub total_projects: usize,
26
27    /// Total number of dependencies
28    pub total_dependencies: usize,
29
30    /// Number of healthy projects
31    pub healthy_projects: usize,
32
33    /// Number of projects with warnings
34    pub warning_projects: usize,
35
36    /// Number of projects with critical issues
37    pub critical_projects: usize,
38
39    /// Number of unknown status projects
40    pub unknown_projects: usize,
41
42    /// Project status breakdown
43    pub project_statuses: HashMap<String, ProjectStatus>,
44
45    /// Aggregated metrics
46    pub metrics: WorkspaceMetrics,
47
48    /// Timestamp of the report (ISO 8601 format)
49    pub timestamp: String,
50}
51
52/// Aggregated metrics for the workspace
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct AggregatedMetrics {
55    /// Average project health (0.0 to 1.0)
56    pub average_health: f64,
57
58    /// Percentage of healthy projects
59    pub healthy_percentage: f64,
60
61    /// Percentage of projects with warnings
62    pub warning_percentage: f64,
63
64    /// Percentage of projects with critical issues
65    pub critical_percentage: f64,
66
67    /// Average number of dependencies per project
68    pub avg_dependencies_per_project: f64,
69
70    /// Maximum dependencies for any single project
71    pub max_dependencies: usize,
72
73    /// Minimum dependencies for any single project
74    pub min_dependencies: usize,
75
76    /// Total number of rules
77    pub total_rules: usize,
78
79    /// Number of enabled rules
80    pub enabled_rules: usize,
81
82    /// Number of disabled rules
83    pub disabled_rules: usize,
84}
85
86/// Health indicator for a specific project
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct ProjectHealthIndicator {
89    /// Project name
90    pub name: String,
91
92    /// Current status
93    pub status: ProjectStatus,
94
95    /// Number of dependencies this project has
96    pub dependency_count: usize,
97
98    /// Number of projects depending on this project
99    pub dependent_count: usize,
100
101    /// Compliance with workspace rules
102    pub rule_compliance: f64,
103}
104
105impl StatusReporter {
106    /// Creates a new status reporter for a workspace
107    pub fn new(workspace: Workspace) -> Self {
108        Self { workspace }
109    }
110
111    /// Generates a comprehensive status report
112    pub fn generate_report(&self) -> Result<StatusReport, OrchestrationError> {
113        let timestamp = chrono::Utc::now().to_rfc3339();
114
115        // Count projects by status
116        let mut healthy_count = 0;
117        let mut warning_count = 0;
118        let mut critical_count = 0;
119        let mut unknown_count = 0;
120        let mut project_statuses = HashMap::new();
121
122        for project in &self.workspace.projects {
123            project_statuses.insert(project.name.clone(), project.status);
124
125            match project.status {
126                ProjectStatus::Healthy => healthy_count += 1,
127                ProjectStatus::Warning => warning_count += 1,
128                ProjectStatus::Critical => critical_count += 1,
129                ProjectStatus::Unknown => unknown_count += 1,
130            }
131        }
132
133        let report = StatusReport {
134            health_status: self.workspace.metrics.health_status,
135            compliance_score: self.workspace.metrics.compliance_score,
136            total_projects: self.workspace.projects.len(),
137            total_dependencies: self.workspace.dependencies.len(),
138            healthy_projects: healthy_count,
139            warning_projects: warning_count,
140            critical_projects: critical_count,
141            unknown_projects: unknown_count,
142            project_statuses,
143            metrics: self.workspace.metrics.clone(),
144            timestamp,
145        };
146
147        Ok(report)
148    }
149
150    /// Collects aggregated metrics for the workspace
151    pub fn collect_metrics(&self) -> Result<AggregatedMetrics, OrchestrationError> {
152        let total_projects = self.workspace.projects.len() as f64;
153
154        if total_projects == 0.0 {
155            return Ok(AggregatedMetrics {
156                average_health: 1.0,
157                healthy_percentage: 100.0,
158                warning_percentage: 0.0,
159                critical_percentage: 0.0,
160                avg_dependencies_per_project: 0.0,
161                max_dependencies: 0,
162                min_dependencies: 0,
163                total_rules: self.workspace.config.rules.len(),
164                enabled_rules: self.workspace.config.rules.iter().filter(|r| r.enabled).count(),
165                disabled_rules: self.workspace.config.rules.iter().filter(|r| !r.enabled).count(),
166            });
167        }
168
169        // Count projects by status
170        let mut healthy_count = 0.0;
171        let mut warning_count = 0.0;
172        let mut critical_count = 0.0;
173
174        for project in &self.workspace.projects {
175            match project.status {
176                ProjectStatus::Healthy => healthy_count += 1.0,
177                ProjectStatus::Warning => warning_count += 1.0,
178                ProjectStatus::Critical => critical_count += 1.0,
179                ProjectStatus::Unknown => {}
180            }
181        }
182
183        // Calculate dependencies per project
184        let mut project_dep_counts: HashMap<String, usize> = HashMap::new();
185        for dep in &self.workspace.dependencies {
186            *project_dep_counts.entry(dep.from.clone()).or_insert(0) += 1;
187        }
188
189        let (max_deps, min_deps) = if project_dep_counts.is_empty() {
190            (0, 0)
191        } else {
192            let max = *project_dep_counts.values().max().unwrap_or(&0);
193            let min = *project_dep_counts.values().min().unwrap_or(&0);
194            (max, min)
195        };
196
197        let avg_deps = if self.workspace.projects.is_empty() {
198            0.0
199        } else {
200            self.workspace.dependencies.len() as f64 / self.workspace.projects.len() as f64
201        };
202
203        Ok(AggregatedMetrics {
204            average_health: self.workspace.metrics.compliance_score,
205            healthy_percentage: (healthy_count / total_projects) * 100.0,
206            warning_percentage: (warning_count / total_projects) * 100.0,
207            critical_percentage: (critical_count / total_projects) * 100.0,
208            avg_dependencies_per_project: avg_deps,
209            max_dependencies: max_deps,
210            min_dependencies: min_deps,
211            total_rules: self.workspace.config.rules.len(),
212            enabled_rules: self.workspace.config.rules.iter().filter(|r| r.enabled).count(),
213            disabled_rules: self.workspace.config.rules.iter().filter(|r| !r.enabled).count(),
214        })
215    }
216
217    /// Gets health indicators for all projects
218    pub fn get_project_health_indicators(&self) -> Result<Vec<ProjectHealthIndicator>, OrchestrationError> {
219        let mut indicators = Vec::new();
220
221        for project in &self.workspace.projects {
222            // Count dependencies for this project
223            let dependency_count = self
224                .workspace
225                .dependencies
226                .iter()
227                .filter(|d| d.from == project.name)
228                .count();
229
230            // Count projects depending on this project
231            let dependent_count = self
232                .workspace
233                .dependencies
234                .iter()
235                .filter(|d| d.to == project.name)
236                .count();
237
238            // Calculate rule compliance (simplified: 1.0 if no violations)
239            let rule_compliance = self.workspace.metrics.compliance_score;
240
241            indicators.push(ProjectHealthIndicator {
242                name: project.name.clone(),
243                status: project.status,
244                dependency_count,
245                dependent_count,
246                rule_compliance,
247            });
248        }
249
250        Ok(indicators)
251    }
252
253    /// Tracks project health over time (returns current snapshot)
254    pub fn track_project_health(&self, project_name: &str) -> Result<ProjectHealthIndicator, OrchestrationError> {
255        let project = self
256            .workspace
257            .projects
258            .iter()
259            .find(|p| p.name == project_name)
260            .ok_or_else(|| OrchestrationError::ProjectNotFound(project_name.to_string()))?;
261
262        let dependency_count = self
263            .workspace
264            .dependencies
265            .iter()
266            .filter(|d| d.from == project.name)
267            .count();
268
269        let dependent_count = self
270            .workspace
271            .dependencies
272            .iter()
273            .filter(|d| d.to == project.name)
274            .count();
275
276        Ok(ProjectHealthIndicator {
277            name: project.name.clone(),
278            status: project.status,
279            dependency_count,
280            dependent_count,
281            rule_compliance: self.workspace.metrics.compliance_score,
282        })
283    }
284
285    /// Generates a summary of workspace compliance
286    pub fn generate_compliance_summary(&self) -> Result<ComplianceSummary, OrchestrationError> {
287        let total_rules = self.workspace.config.rules.len();
288        let enabled_rules = self.workspace.config.rules.iter().filter(|r| r.enabled).count();
289
290        let mut violations = Vec::new();
291
292        // Check for critical projects
293        for project in &self.workspace.projects {
294            if project.status == ProjectStatus::Critical {
295                violations.push(format!("Project '{}' has critical status", project.name));
296            }
297        }
298
299        let compliance_score = self.workspace.metrics.compliance_score;
300        let is_compliant = compliance_score >= 0.8 && violations.is_empty();
301
302        Ok(ComplianceSummary {
303            total_rules,
304            enabled_rules,
305            compliance_score,
306            is_compliant,
307            violations,
308        })
309    }
310}
311
312/// Summary of workspace compliance
313#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct ComplianceSummary {
315    /// Total number of rules
316    pub total_rules: usize,
317
318    /// Number of enabled rules
319    pub enabled_rules: usize,
320
321    /// Overall compliance score
322    pub compliance_score: f64,
323
324    /// Whether the workspace is compliant
325    pub is_compliant: bool,
326
327    /// List of compliance violations
328    pub violations: Vec<String>,
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334    use crate::models::{DependencyType, Project, ProjectDependency, WorkspaceConfig, WorkspaceRule, RuleType};
335
336    fn create_test_workspace() -> Workspace {
337        let mut workspace = Workspace::default();
338
339        workspace.projects = vec![
340            Project {
341                path: "/path/to/project1".into(),
342                name: "project1".to_string(),
343                project_type: "rust".to_string(),
344                version: "0.1.0".to_string(),
345                status: ProjectStatus::Healthy,
346            },
347            Project {
348                path: "/path/to/project2".into(),
349                name: "project2".to_string(),
350                project_type: "rust".to_string(),
351                version: "0.1.0".to_string(),
352                status: ProjectStatus::Warning,
353            },
354            Project {
355                path: "/path/to/project3".into(),
356                name: "project3".to_string(),
357                project_type: "rust".to_string(),
358                version: "0.1.0".to_string(),
359                status: ProjectStatus::Healthy,
360            },
361        ];
362
363        workspace.dependencies = vec![
364            ProjectDependency {
365                from: "project1".to_string(),
366                to: "project2".to_string(),
367                dependency_type: DependencyType::Direct,
368                version_constraint: "^0.1.0".to_string(),
369            },
370            ProjectDependency {
371                from: "project2".to_string(),
372                to: "project3".to_string(),
373                dependency_type: DependencyType::Direct,
374                version_constraint: "^0.1.0".to_string(),
375            },
376        ];
377
378        workspace.config = WorkspaceConfig {
379            rules: vec![
380                WorkspaceRule {
381                    name: "no-circular-deps".to_string(),
382                    rule_type: RuleType::DependencyConstraint,
383                    enabled: true,
384                },
385                WorkspaceRule {
386                    name: "naming-convention".to_string(),
387                    rule_type: RuleType::NamingConvention,
388                    enabled: true,
389                },
390            ],
391            settings: serde_json::json!({}),
392        };
393
394        workspace.metrics = WorkspaceMetrics {
395            total_projects: 3,
396            total_dependencies: 2,
397            compliance_score: 0.95,
398            health_status: HealthStatus::Healthy,
399        };
400
401        workspace
402    }
403
404    #[test]
405    fn test_status_reporter_creation() {
406        let workspace = create_test_workspace();
407        let reporter = StatusReporter::new(workspace);
408
409        assert_eq!(reporter.workspace.projects.len(), 3);
410    }
411
412    #[test]
413    fn test_generate_report() {
414        let workspace = create_test_workspace();
415        let reporter = StatusReporter::new(workspace);
416
417        let report = reporter.generate_report().expect("report generation failed");
418
419        assert_eq!(report.total_projects, 3);
420        assert_eq!(report.total_dependencies, 2);
421        assert_eq!(report.healthy_projects, 2);
422        assert_eq!(report.warning_projects, 1);
423        assert_eq!(report.critical_projects, 0);
424        assert_eq!(report.compliance_score, 0.95);
425    }
426
427    #[test]
428    fn test_collect_metrics() {
429        let workspace = create_test_workspace();
430        let reporter = StatusReporter::new(workspace);
431
432        let metrics = reporter.collect_metrics().expect("metrics collection failed");
433
434        assert_eq!(metrics.total_rules, 2);
435        assert_eq!(metrics.enabled_rules, 2);
436        assert_eq!(metrics.disabled_rules, 0);
437        assert!(metrics.healthy_percentage > 0.0);
438        assert!(metrics.warning_percentage > 0.0);
439    }
440
441    #[test]
442    fn test_get_project_health_indicators() {
443        let workspace = create_test_workspace();
444        let reporter = StatusReporter::new(workspace);
445
446        let indicators = reporter
447            .get_project_health_indicators()
448            .expect("health indicators failed");
449
450        assert_eq!(indicators.len(), 3);
451        assert_eq!(indicators[0].name, "project1");
452        assert_eq!(indicators[0].status, ProjectStatus::Healthy);
453    }
454
455    #[test]
456    fn test_track_project_health() {
457        let workspace = create_test_workspace();
458        let reporter = StatusReporter::new(workspace);
459
460        let health = reporter
461            .track_project_health("project1")
462            .expect("health tracking failed");
463
464        assert_eq!(health.name, "project1");
465        assert_eq!(health.status, ProjectStatus::Healthy);
466    }
467
468    #[test]
469    fn test_track_project_health_not_found() {
470        let workspace = create_test_workspace();
471        let reporter = StatusReporter::new(workspace);
472
473        let result = reporter.track_project_health("nonexistent");
474
475        assert!(result.is_err());
476    }
477
478    #[test]
479    fn test_generate_compliance_summary() {
480        let workspace = create_test_workspace();
481        let reporter = StatusReporter::new(workspace);
482
483        let summary = reporter
484            .generate_compliance_summary()
485            .expect("compliance summary failed");
486
487        assert_eq!(summary.total_rules, 2);
488        assert_eq!(summary.enabled_rules, 2);
489        assert!(summary.is_compliant);
490    }
491
492    #[test]
493    fn test_empty_workspace() {
494        let workspace = Workspace::default();
495        let reporter = StatusReporter::new(workspace);
496
497        let report = reporter.generate_report().expect("report generation failed");
498
499        assert_eq!(report.total_projects, 0);
500        assert_eq!(report.total_dependencies, 0);
501    }
502
503    #[test]
504    fn test_metrics_with_empty_workspace() {
505        let workspace = Workspace::default();
506        let reporter = StatusReporter::new(workspace);
507
508        let metrics = reporter.collect_metrics().expect("metrics collection failed");
509
510        assert_eq!(metrics.avg_dependencies_per_project, 0.0);
511        assert_eq!(metrics.max_dependencies, 0);
512    }
513
514    #[test]
515    fn test_report_timestamp() {
516        let workspace = create_test_workspace();
517        let reporter = StatusReporter::new(workspace);
518
519        let report = reporter.generate_report().expect("report generation failed");
520
521        // Verify timestamp is in ISO 8601 format
522        assert!(!report.timestamp.is_empty());
523        assert!(report.timestamp.contains('T'));
524    }
525
526    #[test]
527    fn test_project_status_breakdown() {
528        let workspace = create_test_workspace();
529        let reporter = StatusReporter::new(workspace);
530
531        let report = reporter.generate_report().expect("report generation failed");
532
533        assert_eq!(report.project_statuses.len(), 3);
534        assert_eq!(
535            report.project_statuses.get("project1"),
536            Some(&ProjectStatus::Healthy)
537        );
538        assert_eq!(
539            report.project_statuses.get("project2"),
540            Some(&ProjectStatus::Warning)
541        );
542    }
543
544    #[test]
545    fn test_dependency_counting() {
546        let workspace = create_test_workspace();
547        let reporter = StatusReporter::new(workspace);
548
549        let indicators = reporter
550            .get_project_health_indicators()
551            .expect("health indicators failed");
552
553        // project1 has 1 dependency (to project2)
554        assert_eq!(indicators[0].dependency_count, 1);
555        // project2 has 1 dependency (to project3)
556        assert_eq!(indicators[1].dependency_count, 1);
557        // project3 has 0 dependencies
558        assert_eq!(indicators[2].dependency_count, 0);
559    }
560
561    #[test]
562    fn test_dependent_counting() {
563        let workspace = create_test_workspace();
564        let reporter = StatusReporter::new(workspace);
565
566        let indicators = reporter
567            .get_project_health_indicators()
568            .expect("health indicators failed");
569
570        // project1 has 0 dependents
571        assert_eq!(indicators[0].dependent_count, 0);
572        // project2 has 1 dependent (project1)
573        assert_eq!(indicators[1].dependent_count, 1);
574        // project3 has 1 dependent (project2)
575        assert_eq!(indicators[2].dependent_count, 1);
576    }
577
578    #[test]
579    fn test_compliance_summary_with_critical_project() {
580        let mut workspace = create_test_workspace();
581        workspace.projects[0].status = ProjectStatus::Critical;
582
583        let reporter = StatusReporter::new(workspace);
584        let summary = reporter
585            .generate_compliance_summary()
586            .expect("compliance summary failed");
587
588        assert!(!summary.violations.is_empty());
589    }
590
591    #[test]
592    fn test_aggregated_metrics_percentages() {
593        let workspace = create_test_workspace();
594        let reporter = StatusReporter::new(workspace);
595
596        let metrics = reporter.collect_metrics().expect("metrics collection failed");
597
598        let total = metrics.healthy_percentage + metrics.warning_percentage + metrics.critical_percentage;
599        // Should be approximately 100 (allowing for floating point precision)
600        assert!((total - 100.0).abs() < 0.1);
601    }
602}