1use anyhow::Result;
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::PathBuf;
10use uuid::Uuid;
11
12use crate::{DebugConfig, DebugReport, DebugSession};
13
14#[derive(Debug, thiserror::Error)]
20pub enum NotificationError {
21 #[error(
24 "HTTP notification delivery is not enabled: rebuild trustformers-debug with \
25 `--features http-integrations`"
26 )]
27 HttpFeatureDisabled,
28 #[error("no HTTP email API endpoint configured for this Email notification channel")]
31 EmailNotConfigured,
32 #[error("{0} notification channel has no delivery implementation")]
34 NotImplemented(&'static str),
35 #[error("notification delivery failed: {0}")]
38 Transport(String),
39}
40
41#[cfg(feature = "http-integrations")]
47async fn post_json(
48 url: &str,
49 payload: &serde_json::Value,
50 extra_headers: &HashMap<String, String>,
51) -> Result<()> {
52 let client = reqwest::Client::new();
53 let mut request = client.post(url).json(payload);
54 for (key, value) in extra_headers {
55 request = request.header(key, value);
56 }
57
58 let response = request.send().await.map_err(|e| NotificationError::Transport(e.to_string()))?;
59
60 let status = response.status();
61 if !status.is_success() {
62 let body = response.text().await.unwrap_or_default();
63 return Err(NotificationError::Transport(format!("HTTP {status}: {body}")).into());
64 }
65 Ok(())
66}
67
68#[cfg(not(feature = "http-integrations"))]
71async fn post_json(
72 _url: &str,
73 _payload: &serde_json::Value,
74 _extra_headers: &HashMap<String, String>,
75) -> Result<()> {
76 Err(NotificationError::HttpFeatureDisabled.into())
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub enum CICDPlatform {
82 GitHub,
83 GitLab,
84 Jenkins,
85 CircleCI,
86 AzureDevOps,
87 BitbucketPipelines,
88 TeamCity,
89 Travis,
90 Custom(String),
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct CICDConfig {
96 pub platform: CICDPlatform,
97 pub project_id: String,
98 pub api_token: Option<String>,
99 pub base_url: Option<String>,
100 pub branch_filters: Vec<String>,
101 pub enable_regression_detection: bool,
102 pub enable_performance_tracking: bool,
103 pub enable_quality_gates: bool,
104 pub enable_automated_reports: bool,
105 pub enable_alert_systems: bool,
106 pub report_formats: Vec<ReportFormat>,
107 pub notification_channels: Vec<NotificationChannel>,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112pub enum ReportFormat {
113 JSON,
114 XML,
115 HTML,
116 Markdown,
117 JUnit,
118 SonarQube,
119 Custom(String),
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124pub enum NotificationChannel {
125 Email {
126 recipients: Vec<String>,
127 #[serde(default)]
133 api: Option<EmailApiConfig>,
134 },
135 Slack {
136 webhook_url: String,
137 channel: String,
138 },
139 Teams {
140 webhook_url: String,
141 },
142 Discord {
143 webhook_url: String,
144 },
145 Webhook {
146 url: String,
147 headers: HashMap<String, String>,
148 },
149 Custom(String),
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct EmailApiConfig {
156 pub endpoint: String,
158 #[serde(default)]
160 pub headers: HashMap<String, String>,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
165pub enum PipelineStage {
166 Build,
167 Test,
168 Debug,
169 Analysis,
170 Deploy,
171 Custom(String),
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
176pub enum QualityGateStatus {
177 Passed,
178 Failed,
179 Warning,
180 Skipped,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct QualityGate {
186 pub name: String,
187 pub description: String,
188 pub metric: QualityMetric,
189 pub threshold: f64,
190 pub operator: ComparisonOperator,
191 pub blocking: bool,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196pub enum QualityMetric {
197 TestCoverage,
198 ModelAccuracy,
199 TrainingLoss,
200 GradientNorm,
201 MemoryUsage,
202 TrainingTime,
203 ModelSize,
204 InferenceLatency,
205 Custom(String),
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
210pub enum ComparisonOperator {
211 GreaterThan,
212 LessThan,
213 GreaterThanOrEqual,
214 LessThanOrEqual,
215 Equal,
216 NotEqual,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct RegressionResult {
222 pub detected: bool,
223 pub severity: RegressionSeverity,
224 pub metric: String,
225 pub baseline_value: f64,
226 pub current_value: f64,
227 pub change_percent: f64,
228 pub description: String,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
233pub enum RegressionSeverity {
234 Critical,
235 Major,
236 Minor,
237 Info,
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct PerformanceData {
243 pub timestamp: DateTime<Utc>,
244 pub commit_hash: String,
245 pub branch: String,
246 pub metrics: HashMap<String, f64>,
247 pub benchmark_results: Vec<BenchmarkResult>,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct BenchmarkResult {
253 pub name: String,
254 pub value: f64,
255 pub unit: String,
256 pub baseline: Option<f64>,
257 pub improvement_percent: Option<f64>,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct PipelineResult {
263 pub run_id: Uuid,
264 pub timestamp: DateTime<Utc>,
265 pub commit_hash: String,
266 pub branch: String,
267 pub stage: PipelineStage,
268 pub status: PipelineStatus,
269 pub debug_report: Option<DebugReport>,
270 pub quality_gate_results: Vec<QualityGateResult>,
271 pub regression_results: Vec<RegressionResult>,
272 pub performance_data: Option<PerformanceData>,
273 pub artifacts: Vec<Artifact>,
274 pub duration_ms: u64,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
279pub enum PipelineStatus {
280 Success,
281 Failed,
282 Warning,
283 Cancelled,
284 Timeout,
285}
286
287impl std::fmt::Display for PipelineStatus {
288 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289 match self {
290 PipelineStatus::Success => write!(f, "Success"),
291 PipelineStatus::Failed => write!(f, "Failed"),
292 PipelineStatus::Warning => write!(f, "Warning"),
293 PipelineStatus::Cancelled => write!(f, "Cancelled"),
294 PipelineStatus::Timeout => write!(f, "Timeout"),
295 }
296 }
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct QualityGateResult {
302 pub gate: QualityGate,
303 pub status: QualityGateStatus,
304 pub actual_value: f64,
305 pub message: String,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct Artifact {
311 pub name: String,
312 pub path: PathBuf,
313 pub size_bytes: u64,
314 pub checksum: String,
315 pub artifact_type: ArtifactType,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
320pub enum ArtifactType {
321 DebugReport,
322 TestResults,
323 BenchmarkResults,
324 Model,
325 Dataset,
326 Documentation,
327 Custom(String),
328}
329
330#[derive(Debug)]
332pub struct CICDIntegration {
333 config: CICDConfig,
334 quality_gates: Vec<QualityGate>,
335 baseline_metrics: HashMap<String, f64>,
336 performance_history: Vec<PerformanceData>,
337 pipeline_history: Vec<PipelineResult>,
338}
339
340impl CICDIntegration {
341 pub fn new(config: CICDConfig) -> Self {
343 Self {
344 config,
345 quality_gates: Vec::new(),
346 baseline_metrics: HashMap::new(),
347 performance_history: Vec::new(),
348 pipeline_history: Vec::new(),
349 }
350 }
351
352 pub fn add_quality_gate(&mut self, gate: QualityGate) {
354 self.quality_gates.push(gate);
355 }
356
357 pub fn set_baseline_metrics(&mut self, metrics: HashMap<String, f64>) {
359 self.baseline_metrics = metrics;
360 }
361
362 pub async fn run_debug_analysis(
364 &mut self,
365 commit_hash: String,
366 branch: String,
367 debug_config: DebugConfig,
368 ) -> Result<PipelineResult> {
369 let run_id = Uuid::new_v4();
370 let start_time = Utc::now();
371
372 tracing::info!(
373 "Starting debug analysis for commit {} on branch {}",
374 commit_hash,
375 branch
376 );
377
378 let mut debug_session = DebugSession::new(debug_config);
380 debug_session.start().await?;
381
382 let debug_report = debug_session.stop().await?;
386 let analysis_duration_ms = (Utc::now() - start_time).num_milliseconds().max(0) as u64;
387
388 let metrics = self.extract_metrics_from_report(&debug_report, analysis_duration_ms);
390
391 let quality_gate_results = self.evaluate_quality_gates(&metrics);
393
394 let regression_results = self.detect_regressions(&metrics, &commit_hash);
396
397 let status = self.determine_pipeline_status(&quality_gate_results, ®ression_results);
399
400 let performance_data = PerformanceData {
402 timestamp: start_time,
403 commit_hash: commit_hash.clone(),
404 branch: branch.clone(),
405 metrics: metrics.clone(),
406 benchmark_results: self.generate_benchmark_results(&metrics),
407 };
408
409 let artifacts = self.generate_artifacts(&debug_report, &performance_data)?;
411
412 let duration_ms = (Utc::now() - start_time).num_milliseconds() as u64;
413
414 let result = PipelineResult {
415 run_id,
416 timestamp: start_time,
417 commit_hash,
418 branch,
419 stage: PipelineStage::Debug,
420 status: status.clone(),
421 debug_report: Some(debug_report),
422 quality_gate_results,
423 regression_results,
424 performance_data: Some(performance_data.clone()),
425 artifacts,
426 duration_ms,
427 };
428
429 self.performance_history.push(performance_data);
431 self.pipeline_history.push(result.clone());
432
433 if self.config.enable_alert_systems {
435 self.send_notifications(&result).await?;
436 }
437
438 if self.config.enable_automated_reports {
440 self.generate_reports(&result).await?;
441 }
442
443 tracing::info!("Debug analysis completed with status: {:?}", status);
444
445 Ok(result)
446 }
447
448 fn extract_metrics_from_report(
454 &self,
455 report: &DebugReport,
456 analysis_duration_ms: u64,
457 ) -> HashMap<String, f64> {
458 let mut metrics = HashMap::new();
459
460 if let Some(ref tensor_report) = report.tensor_report {
462 metrics.insert(
463 "tensor_nan_count".to_string(),
464 tensor_report.total_nan_count() as f64,
465 );
466 metrics.insert(
467 "tensor_inf_count".to_string(),
468 tensor_report.total_inf_count() as f64,
469 );
470 }
471
472 if let Some(ref gradient_report) = report.gradient_report {
474 metrics.insert(
475 "gradient_norm".to_string(),
476 gradient_report.average_gradient_norm(),
477 );
478 metrics.insert(
479 "vanishing_gradients".to_string(),
480 gradient_report.vanishing_gradient_layers().len() as f64,
481 );
482 metrics.insert(
483 "exploding_gradients".to_string(),
484 gradient_report.exploding_gradient_layers().len() as f64,
485 );
486 }
487
488 if let Some(ref memory_report) = report.memory_profiler_report {
490 metrics.insert(
491 "peak_memory_mb".to_string(),
492 memory_report.peak_memory_usage() / (1024.0 * 1024.0),
493 );
494 metrics.insert(
495 "memory_efficiency".to_string(),
496 memory_report.memory_efficiency(),
497 );
498 }
499
500 if let Some(total_parameters) = self.count_model_parameters(report) {
504 metrics.insert("total_parameters".to_string(), total_parameters as f64);
505 }
506 metrics.insert("training_time_ms".to_string(), analysis_duration_ms as f64);
507
508 metrics
509 }
510
511 fn evaluate_quality_gates(&self, metrics: &HashMap<String, f64>) -> Vec<QualityGateResult> {
513 let mut results = Vec::new();
514
515 for gate in &self.quality_gates {
516 let metric_name = self.get_metric_name(&gate.metric);
517 let actual_value = metrics.get(&metric_name).copied().unwrap_or(0.0);
518
519 let passed = match gate.operator {
520 ComparisonOperator::GreaterThan => actual_value > gate.threshold,
521 ComparisonOperator::LessThan => actual_value < gate.threshold,
522 ComparisonOperator::GreaterThanOrEqual => actual_value >= gate.threshold,
523 ComparisonOperator::LessThanOrEqual => actual_value <= gate.threshold,
524 ComparisonOperator::Equal => (actual_value - gate.threshold).abs() < f64::EPSILON,
525 ComparisonOperator::NotEqual => {
526 (actual_value - gate.threshold).abs() >= f64::EPSILON
527 },
528 };
529
530 let status = if passed { QualityGateStatus::Passed } else { QualityGateStatus::Failed };
531
532 let message = format!(
533 "Quality gate '{}': {} {} {} (actual: {})",
534 gate.name,
535 metric_name,
536 self.operator_symbol(&gate.operator),
537 gate.threshold,
538 actual_value
539 );
540
541 results.push(QualityGateResult {
542 gate: gate.clone(),
543 status,
544 actual_value,
545 message,
546 });
547 }
548
549 results
550 }
551
552 fn detect_regressions(
554 &self,
555 metrics: &HashMap<String, f64>,
556 _commit_hash: &str,
557 ) -> Vec<RegressionResult> {
558 let mut results = Vec::new();
559
560 if !self.config.enable_regression_detection {
561 return results;
562 }
563
564 for (metric_name, ¤t_value) in metrics {
565 if let Some(&baseline_value) = self.baseline_metrics.get(metric_name) {
566 let change_percent = ((current_value - baseline_value) / baseline_value) * 100.0;
567
568 let (detected, severity) = self.analyze_regression(metric_name, change_percent);
570
571 if detected {
572 results.push(RegressionResult {
573 detected: true,
574 severity,
575 metric: metric_name.clone(),
576 baseline_value,
577 current_value,
578 change_percent,
579 description: format!(
580 "Regression detected in {}: {:.2}% change from baseline (baseline: {:.4}, current: {:.4})",
581 metric_name, change_percent, baseline_value, current_value
582 ),
583 });
584 }
585 }
586 }
587
588 results
589 }
590
591 fn analyze_regression(
593 &self,
594 metric_name: &str,
595 change_percent: f64,
596 ) -> (bool, RegressionSeverity) {
597 let abs_change = change_percent.abs();
598
599 let (minor_threshold, major_threshold, critical_threshold) = match metric_name {
601 name if name.contains("accuracy") => (2.0, 5.0, 10.0),
602 name if name.contains("loss") => (5.0, 15.0, 30.0),
603 name if name.contains("memory") => (10.0, 25.0, 50.0),
604 name if name.contains("time") => (15.0, 30.0, 60.0),
605 _ => (5.0, 15.0, 30.0), };
607
608 if abs_change >= critical_threshold {
609 (true, RegressionSeverity::Critical)
610 } else if abs_change >= major_threshold {
611 (true, RegressionSeverity::Major)
612 } else if abs_change >= minor_threshold {
613 (true, RegressionSeverity::Minor)
614 } else {
615 (false, RegressionSeverity::Info)
616 }
617 }
618
619 fn determine_pipeline_status(
621 &self,
622 quality_gate_results: &[QualityGateResult],
623 regression_results: &[RegressionResult],
624 ) -> PipelineStatus {
625 for result in quality_gate_results {
627 if result.gate.blocking && matches!(result.status, QualityGateStatus::Failed) {
628 return PipelineStatus::Failed;
629 }
630 }
631
632 for regression in regression_results {
634 if matches!(regression.severity, RegressionSeverity::Critical) {
635 return PipelineStatus::Failed;
636 }
637 }
638
639 let has_warnings = quality_gate_results
641 .iter()
642 .any(|r| matches!(r.status, QualityGateStatus::Failed))
643 || regression_results
644 .iter()
645 .any(|r| matches!(r.severity, RegressionSeverity::Major));
646
647 if has_warnings {
648 PipelineStatus::Warning
649 } else {
650 PipelineStatus::Success
651 }
652 }
653
654 fn generate_benchmark_results(&self, metrics: &HashMap<String, f64>) -> Vec<BenchmarkResult> {
656 let mut results = Vec::new();
657
658 for (name, &value) in metrics {
659 let baseline = self.baseline_metrics.get(name).copied();
660 let improvement_percent = baseline.map(|b| ((value - b) / b) * 100.0);
661
662 let unit = match name.as_str() {
663 name if name.contains("time") || name.contains("latency") => "ms",
664 name if name.contains("memory") => "MB",
665 name if name.contains("accuracy") => "%",
666 name if name.contains("loss") => "loss",
667 _ => "units",
668 };
669
670 results.push(BenchmarkResult {
671 name: name.clone(),
672 value,
673 unit: unit.to_string(),
674 baseline,
675 improvement_percent,
676 });
677 }
678
679 results
680 }
681
682 fn generate_artifacts(
684 &self,
685 debug_report: &DebugReport,
686 performance_data: &PerformanceData,
687 ) -> Result<Vec<Artifact>> {
688 let mut artifacts = Vec::new();
689
690 let debug_report_json = serde_json::to_string_pretty(debug_report)?;
692 let debug_report_path = PathBuf::from("debug_report.json");
693 std::fs::write(&debug_report_path, &debug_report_json)?;
694
695 artifacts.push(Artifact {
696 name: "Debug Report".to_string(),
697 path: debug_report_path,
698 size_bytes: debug_report_json.len() as u64,
699 checksum: format!("{:x}", md5::compute(&debug_report_json)),
700 artifact_type: ArtifactType::DebugReport,
701 });
702
703 let performance_json = serde_json::to_string_pretty(performance_data)?;
705 let performance_path = PathBuf::from("performance_data.json");
706 std::fs::write(&performance_path, &performance_json)?;
707
708 artifacts.push(Artifact {
709 name: "Performance Data".to_string(),
710 path: performance_path,
711 size_bytes: performance_json.len() as u64,
712 checksum: format!("{:x}", md5::compute(&performance_json)),
713 artifact_type: ArtifactType::BenchmarkResults,
714 });
715
716 Ok(artifacts)
717 }
718
719 async fn send_notifications(&self, result: &PipelineResult) -> Result<()> {
721 for channel in &self.config.notification_channels {
722 match channel {
723 NotificationChannel::Slack {
724 webhook_url,
725 channel: slack_channel,
726 } => {
727 self.send_slack_notification(webhook_url, slack_channel, result).await?;
728 },
729 NotificationChannel::Email { recipients, api } => {
730 self.send_email_notification(recipients, api.as_ref(), result).await?;
731 },
732 NotificationChannel::Teams { webhook_url } => {
733 self.send_teams_notification(webhook_url, result).await?;
734 },
735 NotificationChannel::Discord { webhook_url } => {
736 self.send_discord_notification(webhook_url, result).await?;
737 },
738 NotificationChannel::Webhook { url, headers } => {
739 self.send_webhook_notification(url, headers, result).await?;
740 },
741 NotificationChannel::Custom(_) => {
742 return Err(NotificationError::NotImplemented("Custom").into());
743 },
744 }
745 }
746
747 Ok(())
748 }
749
750 async fn generate_reports(&self, result: &PipelineResult) -> Result<()> {
752 for format in &self.config.report_formats {
753 match format {
754 ReportFormat::JSON => {
755 let json_report = serde_json::to_string_pretty(result)?;
756 std::fs::write("cicd_report.json", json_report)?;
757 },
758 ReportFormat::HTML => {
759 let html_report = self.generate_html_report(result)?;
760 std::fs::write("cicd_report.html", html_report)?;
761 },
762 ReportFormat::Markdown => {
763 let md_report = self.generate_markdown_report(result)?;
764 std::fs::write("cicd_report.md", md_report)?;
765 },
766 ReportFormat::JUnit => {
767 let junit_report = self.generate_junit_report(result)?;
768 std::fs::write("cicd_report.xml", junit_report)?;
769 },
770 _ => {
771 tracing::info!("Report format {:?} not implemented", format);
772 },
773 }
774 }
775
776 Ok(())
777 }
778
779 async fn send_slack_notification(
789 &self,
790 webhook_url: &str,
791 channel: &str,
792 result: &PipelineResult,
793 ) -> Result<()> {
794 let color = match result.status {
795 PipelineStatus::Success => "good",
796 PipelineStatus::Warning => "warning",
797 PipelineStatus::Failed => "danger",
798 _ => "warning",
799 };
800
801 let message = serde_json::json!({
802 "channel": channel,
803 "attachments": [{
804 "color": color,
805 "title": format!("Debug Analysis - {}", result.commit_hash),
806 "text": format!("Branch: {} | Status: {:?} | Duration: {}ms",
807 result.branch, result.status, result.duration_ms),
808 "fields": [
809 {
810 "title": "Quality Gates",
811 "value": format!("{} passed, {} failed",
812 result.quality_gate_results.iter().filter(|r| matches!(r.status, QualityGateStatus::Passed)).count(),
813 result.quality_gate_results.iter().filter(|r| matches!(r.status, QualityGateStatus::Failed)).count()),
814 "short": true
815 },
816 {
817 "title": "Regressions",
818 "value": format!("{} detected", result.regression_results.len()),
819 "short": true
820 }
821 ]
822 }]
823 });
824
825 post_json(webhook_url, &message, &HashMap::new()).await?;
826 tracing::info!(webhook_url = %webhook_url, "Slack notification delivered");
827 Ok(())
828 }
829
830 async fn send_email_notification(
831 &self,
832 recipients: &[String],
833 api: Option<&EmailApiConfig>,
834 result: &PipelineResult,
835 ) -> Result<()> {
836 let Some(api) = api else {
837 return Err(NotificationError::EmailNotConfigured.into());
838 };
839
840 let subject = format!(
841 "Debug Analysis Report - {} ({})",
842 result.commit_hash, result.status
843 );
844 let body = format!(
845 "Debug analysis completed for commit {} on branch {}.\n\nStatus: {:?}\nDuration: {}ms\n\nQuality Gates: {} passed, {} failed\nRegressions: {} detected",
846 result.commit_hash,
847 result.branch,
848 result.status,
849 result.duration_ms,
850 result.quality_gate_results.iter().filter(|r| matches!(r.status, QualityGateStatus::Passed)).count(),
851 result.quality_gate_results.iter().filter(|r| matches!(r.status, QualityGateStatus::Failed)).count(),
852 result.regression_results.len()
853 );
854
855 let payload = serde_json::json!({
856 "to": recipients,
857 "subject": subject,
858 "body": body,
859 });
860
861 post_json(&api.endpoint, &payload, &api.headers).await?;
862 tracing::info!(recipients = ?recipients, endpoint = %api.endpoint, "Email notification delivered");
863 Ok(())
864 }
865
866 async fn send_teams_notification(
867 &self,
868 webhook_url: &str,
869 result: &PipelineResult,
870 ) -> Result<()> {
871 let theme_color = match result.status {
873 PipelineStatus::Success => "28A745",
874 PipelineStatus::Warning => "FFC107",
875 PipelineStatus::Failed => "DC3545",
876 _ => "6C757D",
877 };
878
879 let message = serde_json::json!({
880 "@type": "MessageCard",
881 "@context": "http://schema.org/extensions",
882 "themeColor": theme_color,
883 "summary": format!("Debug Analysis - {}", result.commit_hash),
884 "sections": [{
885 "activityTitle": format!("Debug Analysis - {}", result.commit_hash),
886 "facts": [
887 {"name": "Branch", "value": result.branch},
888 {"name": "Status", "value": format!("{:?}", result.status)},
889 {"name": "Duration", "value": format!("{}ms", result.duration_ms)},
890 {"name": "Quality Gates", "value": format!("{} passed, {} failed",
891 result.quality_gate_results.iter().filter(|r| matches!(r.status, QualityGateStatus::Passed)).count(),
892 result.quality_gate_results.iter().filter(|r| matches!(r.status, QualityGateStatus::Failed)).count())},
893 {"name": "Regressions", "value": format!("{} detected", result.regression_results.len())},
894 ],
895 }],
896 });
897
898 post_json(webhook_url, &message, &HashMap::new()).await?;
899 tracing::info!(webhook_url = %webhook_url, "Teams notification delivered");
900 Ok(())
901 }
902
903 async fn send_discord_notification(
904 &self,
905 webhook_url: &str,
906 result: &PipelineResult,
907 ) -> Result<()> {
908 let color = match result.status {
910 PipelineStatus::Success => 0x28_A7_45,
911 PipelineStatus::Warning => 0xFF_C1_07,
912 PipelineStatus::Failed => 0xDC_35_45,
913 _ => 0x6C_75_7D,
914 };
915
916 let message = serde_json::json!({
917 "embeds": [{
918 "title": format!("Debug Analysis - {}", result.commit_hash),
919 "description": format!("Branch: {} | Status: {:?} | Duration: {}ms",
920 result.branch, result.status, result.duration_ms),
921 "color": color,
922 "fields": [
923 {
924 "name": "Quality Gates",
925 "value": format!("{} passed, {} failed",
926 result.quality_gate_results.iter().filter(|r| matches!(r.status, QualityGateStatus::Passed)).count(),
927 result.quality_gate_results.iter().filter(|r| matches!(r.status, QualityGateStatus::Failed)).count()),
928 "inline": true
929 },
930 {
931 "name": "Regressions",
932 "value": format!("{} detected", result.regression_results.len()),
933 "inline": true
934 }
935 ]
936 }]
937 });
938
939 post_json(webhook_url, &message, &HashMap::new()).await?;
940 tracing::info!(webhook_url = %webhook_url, "Discord notification delivered");
941 Ok(())
942 }
943
944 async fn send_webhook_notification(
945 &self,
946 url: &str,
947 headers: &HashMap<String, String>,
948 result: &PipelineResult,
949 ) -> Result<()> {
950 let payload = serde_json::to_value(result)?;
951 post_json(url, &payload, headers).await?;
952 tracing::info!(url = %url, "Generic webhook notification delivered");
953 Ok(())
954 }
955
956 fn generate_html_report(&self, result: &PipelineResult) -> Result<String> {
958 let html = format!(r#"
959<!DOCTYPE html>
960<html>
961<head>
962 <title>Debug Analysis Report</title>
963 <style>
964 body {{ font-family: Arial, sans-serif; margin: 20px; }}
965 .status-success {{ color: green; }}
966 .status-warning {{ color: orange; }}
967 .status-failed {{ color: red; }}
968 table {{ border-collapse: collapse; width: 100%; }}
969 th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
970 th {{ background-color: #f2f2f2; }}
971 </style>
972</head>
973<body>
974 <h1>Debug Analysis Report</h1>
975 <h2>Overview</h2>
976 <p><strong>Commit:</strong> {}</p>
977 <p><strong>Branch:</strong> {}</p>
978 <p><strong>Status:</strong> <span class="status-{}">{:?}</span></p>
979 <p><strong>Duration:</strong> {}ms</p>
980 <p><strong>Timestamp:</strong> {}</p>
981
982 <h2>Quality Gates</h2>
983 <table>
984 <tr><th>Gate</th><th>Status</th><th>Actual Value</th><th>Threshold</th><th>Message</th></tr>
985 {}
986 </table>
987
988 <h2>Regression Analysis</h2>
989 <table>
990 <tr><th>Metric</th><th>Severity</th><th>Change %</th><th>Baseline</th><th>Current</th><th>Description</th></tr>
991 {}
992 </table>
993</body>
994</html>
995"#,
996 result.commit_hash,
997 result.branch,
998 format!("{:?}", result.status).to_lowercase(),
999 result.status,
1000 result.duration_ms,
1001 result.timestamp,
1002 result.quality_gate_results.iter().map(|r| format!(
1003 "<tr><td>{}</td><td>{:?}</td><td>{:.4}</td><td>{:.4}</td><td>{}</td></tr>",
1004 r.gate.name, r.status, r.actual_value, r.gate.threshold, r.message
1005 )).collect::<Vec<_>>().join(""),
1006 result.regression_results.iter().map(|r| format!(
1007 "<tr><td>{}</td><td>{:?}</td><td>{:.2}%</td><td>{:.4}</td><td>{:.4}</td><td>{}</td></tr>",
1008 r.metric, r.severity, r.change_percent, r.baseline_value, r.current_value, r.description
1009 )).collect::<Vec<_>>().join("")
1010 );
1011
1012 Ok(html)
1013 }
1014
1015 fn generate_markdown_report(&self, result: &PipelineResult) -> Result<String> {
1017 let status_emoji = match result.status {
1018 PipelineStatus::Success => "✅",
1019 PipelineStatus::Warning => "⚠️",
1020 PipelineStatus::Failed => "❌",
1021 _ => "❓",
1022 };
1023
1024 let markdown = format!(
1025 r#"# Debug Analysis Report
1026
1027## Overview
1028- **Commit:** {}
1029- **Branch:** {}
1030- **Status:** {} {:?}
1031- **Duration:** {}ms
1032- **Timestamp:** {}
1033
1034## Quality Gates
1035| Gate | Status | Actual Value | Threshold | Message |
1036|------|--------|--------------|-----------|---------|
1037{}
1038
1039## Regression Analysis
1040| Metric | Severity | Change % | Baseline | Current | Description |
1041|--------|----------|----------|----------|---------|-------------|
1042{}
1043
1044## Artifacts
1045{}
1046"#,
1047 result.commit_hash,
1048 result.branch,
1049 status_emoji,
1050 result.status,
1051 result.duration_ms,
1052 result.timestamp,
1053 result
1054 .quality_gate_results
1055 .iter()
1056 .map(|r| format!(
1057 "| {} | {:?} | {:.4} | {:.4} | {} |",
1058 r.gate.name, r.status, r.actual_value, r.gate.threshold, r.message
1059 ))
1060 .collect::<Vec<_>>()
1061 .join("\n"),
1062 result
1063 .regression_results
1064 .iter()
1065 .map(|r| format!(
1066 "| {} | {:?} | {:.2}% | {:.4} | {:.4} | {} |",
1067 r.metric,
1068 r.severity,
1069 r.change_percent,
1070 r.baseline_value,
1071 r.current_value,
1072 r.description
1073 ))
1074 .collect::<Vec<_>>()
1075 .join("\n"),
1076 result
1077 .artifacts
1078 .iter()
1079 .map(|a| format!(
1080 "- **{}:** {} ({} bytes)",
1081 a.name,
1082 a.path.display(),
1083 a.size_bytes
1084 ))
1085 .collect::<Vec<_>>()
1086 .join("\n")
1087 );
1088
1089 Ok(markdown)
1090 }
1091
1092 fn generate_junit_report(&self, result: &PipelineResult) -> Result<String> {
1094 let test_cases = result
1095 .quality_gate_results
1096 .iter()
1097 .map(|r| {
1098 let status = match r.status {
1099 QualityGateStatus::Passed => "",
1100 QualityGateStatus::Failed => {
1101 r#"<failure message="Quality gate failed"></failure>"#
1102 },
1103 QualityGateStatus::Warning => {
1104 r#"<error message="Quality gate warning"></error>"#
1105 },
1106 QualityGateStatus::Skipped => r#"<skipped/>"#,
1107 };
1108 format!(
1109 r#"<testcase classname="QualityGates" name="{}" time="0">{}</testcase>"#,
1110 r.gate.name, status
1111 )
1112 })
1113 .collect::<Vec<_>>()
1114 .join("\n ");
1115
1116 let junit = format!(
1117 r#"<?xml version="1.0" encoding="UTF-8"?>
1118<testsuite name="DebugAnalysis" tests="{}" failures="{}" errors="0" time="{:.3}">
1119 {}
1120</testsuite>
1121"#,
1122 result.quality_gate_results.len(),
1123 result
1124 .quality_gate_results
1125 .iter()
1126 .filter(|r| matches!(r.status, QualityGateStatus::Failed))
1127 .count(),
1128 result.duration_ms as f64 / 1000.0,
1129 test_cases
1130 );
1131
1132 Ok(junit)
1133 }
1134
1135 fn get_metric_name(&self, metric: &QualityMetric) -> String {
1137 match metric {
1138 QualityMetric::TestCoverage => "test_coverage".to_string(),
1139 QualityMetric::ModelAccuracy => "model_accuracy".to_string(),
1140 QualityMetric::TrainingLoss => "training_loss".to_string(),
1141 QualityMetric::GradientNorm => "gradient_norm".to_string(),
1142 QualityMetric::MemoryUsage => "peak_memory_mb".to_string(),
1143 QualityMetric::TrainingTime => "training_time_ms".to_string(),
1144 QualityMetric::ModelSize => "total_parameters".to_string(),
1145 QualityMetric::InferenceLatency => "inference_latency_ms".to_string(),
1146 QualityMetric::Custom(name) => name.clone(),
1147 }
1148 }
1149
1150 fn operator_symbol(&self, op: &ComparisonOperator) -> &'static str {
1151 match op {
1152 ComparisonOperator::GreaterThan => ">",
1153 ComparisonOperator::LessThan => "<",
1154 ComparisonOperator::GreaterThanOrEqual => ">=",
1155 ComparisonOperator::LessThanOrEqual => "<=",
1156 ComparisonOperator::Equal => "==",
1157 ComparisonOperator::NotEqual => "!=",
1158 }
1159 }
1160
1161 fn count_model_parameters(&self, _report: &DebugReport) -> Option<u64> {
1168 None
1169 }
1170
1171 pub fn get_pipeline_history(&self) -> &[PipelineResult] {
1173 &self.pipeline_history
1174 }
1175
1176 pub fn get_performance_history(&self) -> &[PerformanceData] {
1178 &self.performance_history
1179 }
1180
1181 pub fn get_quality_gates(&self) -> &[QualityGate] {
1183 &self.quality_gates
1184 }
1185}
1186
1187impl Default for CICDConfig {
1188 fn default() -> Self {
1189 Self {
1190 platform: CICDPlatform::GitHub,
1191 project_id: "default".to_string(),
1192 api_token: None,
1193 base_url: None,
1194 branch_filters: vec!["main".to_string(), "develop".to_string()],
1195 enable_regression_detection: true,
1196 enable_performance_tracking: true,
1197 enable_quality_gates: true,
1198 enable_automated_reports: true,
1199 enable_alert_systems: true,
1200 report_formats: vec![
1201 ReportFormat::JSON,
1202 ReportFormat::HTML,
1203 ReportFormat::Markdown,
1204 ],
1205 notification_channels: Vec::new(),
1206 }
1207 }
1208}
1209
1210impl crate::GradientDebugReport {
1218 pub fn average_gradient_norm(&self) -> f64 {
1221 let norms: Vec<f64> =
1222 self.status.layer_statuses.values().map(|s| s.latest_gradient_norm).collect();
1223 mean_gradient_norm(&norms)
1224 }
1225
1226 pub fn vanishing_gradient_layers(&self) -> Vec<String> {
1229 let norms: HashMap<String, f64> = self
1230 .status
1231 .layer_statuses
1232 .iter()
1233 .map(|(name, status)| (name.clone(), status.latest_gradient_norm))
1234 .collect();
1235 layers_matching(&norms, |norm| norm < 1e-8)
1236 }
1237
1238 pub fn exploding_gradient_layers(&self) -> Vec<String> {
1241 let norms: HashMap<String, f64> = self
1242 .status
1243 .layer_statuses
1244 .iter()
1245 .map(|(name, status)| (name.clone(), status.latest_gradient_norm))
1246 .collect();
1247 layers_matching(&norms, |norm| norm > 100.0)
1248 }
1249}
1250
1251fn mean_gradient_norm(norms: &[f64]) -> f64 {
1254 if norms.is_empty() {
1255 0.0
1256 } else {
1257 norms.iter().sum::<f64>() / norms.len() as f64
1258 }
1259}
1260
1261fn layers_matching(norms: &HashMap<String, f64>, predicate: impl Fn(f64) -> bool) -> Vec<String> {
1264 norms
1265 .iter()
1266 .filter(|(_, &norm)| predicate(norm))
1267 .map(|(name, _)| name.clone())
1268 .collect()
1269}
1270
1271impl crate::MemoryProfilingReport {
1272 pub fn peak_memory_usage(&self) -> f64 {
1275 self.peak_memory_mb * 1024.0 * 1024.0
1276 }
1277
1278 pub fn memory_efficiency(&self) -> f64 {
1281 (1.0 - self.fragmentation_analysis.fragmentation_ratio).clamp(0.0, 1.0)
1282 }
1283}
1284
1285#[cfg(test)]
1286mod tests {
1287 use super::*;
1288
1289 fn sample_result(status: PipelineStatus) -> PipelineResult {
1290 PipelineResult {
1291 run_id: Uuid::new_v4(),
1292 timestamp: Utc::now(),
1293 commit_hash: "abc123".to_string(),
1294 branch: "main".to_string(),
1295 stage: PipelineStage::Debug,
1296 status,
1297 debug_report: None,
1298 quality_gate_results: Vec::new(),
1299 regression_results: Vec::new(),
1300 performance_data: None,
1301 artifacts: Vec::new(),
1302 duration_ms: 42,
1303 }
1304 }
1305
1306 #[test]
1311 fn test_mean_gradient_norm_is_real_not_fixed_one() {
1312 assert_eq!(mean_gradient_norm(&[]), 0.0);
1314 assert_eq!(mean_gradient_norm(&[2.0, 4.0, 6.0]), 4.0);
1315 assert_ne!(mean_gradient_norm(&[2.0, 4.0, 6.0]), 1.0);
1316 }
1317
1318 #[test]
1319 fn test_layers_matching_finds_real_vanishing_and_exploding_layers() {
1320 let mut norms = HashMap::new();
1321 norms.insert("healthy".to_string(), 0.5);
1322 norms.insert("vanished".to_string(), 1e-10);
1323 norms.insert("exploded".to_string(), 500.0);
1324
1325 let vanishing = layers_matching(&norms, |n| n < 1e-8);
1328 assert_eq!(vanishing, vec!["vanished".to_string()]);
1329
1330 let exploding = layers_matching(&norms, |n| n > 100.0);
1331 assert_eq!(exploding, vec!["exploded".to_string()]);
1332 }
1333
1334 #[tokio::test]
1335 async fn test_memory_report_accessors_reflect_real_profiler_fields() {
1336 use crate::memory_profiler::{AllocationType, MemoryProfiler, MemoryProfilingConfig};
1337
1338 let mut profiler = MemoryProfiler::new(MemoryProfilingConfig::default());
1339 profiler.start().await.expect("profiler should start");
1340 let id = profiler
1341 .record_allocation(4096, AllocationType::Tensor, vec!["test".to_string()])
1342 .expect("allocation should record");
1343 profiler.record_deallocation(id).expect("deallocation should record");
1344 let report = profiler.stop().await.expect("profiler should stop");
1345
1346 assert_eq!(
1349 report.peak_memory_usage(),
1350 report.peak_memory_mb * 1024.0 * 1024.0
1351 );
1352 assert_eq!(
1353 report.memory_efficiency(),
1354 (1.0 - report.fragmentation_analysis.fragmentation_ratio).clamp(0.0, 1.0)
1355 );
1356 }
1357
1358 #[tokio::test]
1359 async fn test_count_model_parameters_is_honest_absence_not_a_million() {
1360 let integration = CICDIntegration::new(CICDConfig::default());
1361 let mut session = DebugSession::new(DebugConfig::default());
1362 session.start().await.expect("session should start");
1363 let report = session.stop().await.expect("session should stop");
1364
1365 assert_eq!(integration.count_model_parameters(&report), None);
1368 }
1369
1370 #[cfg(feature = "http-integrations")]
1376 mod http_delivery {
1377 use super::*;
1378 use axum::{extract::State, routing::post, Json, Router};
1379 use std::sync::Arc;
1380 use tokio::net::TcpListener;
1381 use tokio::sync::Mutex as AsyncMutex;
1382
1383 #[derive(Default, Clone)]
1384 struct Captured {
1385 body: Option<serde_json::Value>,
1386 }
1387
1388 async fn capture(
1389 State(state): State<Arc<AsyncMutex<Captured>>>,
1390 Json(body): Json<serde_json::Value>,
1391 ) -> &'static str {
1392 state.lock().await.body = Some(body);
1393 "ok"
1394 }
1395
1396 async fn start_mock_server() -> (String, Arc<AsyncMutex<Captured>>) {
1400 let state = Arc::new(AsyncMutex::new(Captured::default()));
1401 let app = Router::new().route("/hook", post(capture)).with_state(state.clone());
1402 let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind mock server");
1403 let addr = listener.local_addr().expect("mock server local addr");
1404 tokio::spawn(async move {
1405 let _ = axum::serve(listener, app).await;
1406 });
1407 (format!("http://{addr}/hook"), state)
1408 }
1409
1410 #[tokio::test]
1411 async fn test_slack_notification_sends_real_payload() {
1412 let (url, state) = start_mock_server().await;
1413 let integration = CICDIntegration::new(CICDConfig::default());
1414 let result = sample_result(PipelineStatus::Success);
1415
1416 integration
1417 .send_slack_notification(&url, "#ci-alerts", &result)
1418 .await
1419 .expect("mock server should accept the request");
1420
1421 let captured = state.lock().await.body.clone().expect("payload should be captured");
1422 assert_eq!(captured["channel"], "#ci-alerts");
1423 assert_eq!(captured["attachments"][0]["color"], "good");
1424 assert!(captured["attachments"][0]["title"]
1425 .as_str()
1426 .expect("title should be a string")
1427 .contains(&result.commit_hash));
1428 }
1429
1430 #[tokio::test]
1431 async fn test_teams_notification_sends_real_payload() {
1432 let (url, state) = start_mock_server().await;
1433 let integration = CICDIntegration::new(CICDConfig::default());
1434 let result = sample_result(PipelineStatus::Failed);
1435
1436 integration
1437 .send_teams_notification(&url, &result)
1438 .await
1439 .expect("mock server should accept the request");
1440
1441 let captured = state.lock().await.body.clone().expect("payload should be captured");
1442 assert_eq!(captured["@type"], "MessageCard");
1443 assert_eq!(captured["themeColor"], "DC3545");
1444 }
1445
1446 #[tokio::test]
1447 async fn test_discord_notification_sends_real_payload() {
1448 let (url, state) = start_mock_server().await;
1449 let integration = CICDIntegration::new(CICDConfig::default());
1450 let result = sample_result(PipelineStatus::Warning);
1451
1452 integration
1453 .send_discord_notification(&url, &result)
1454 .await
1455 .expect("mock server should accept the request");
1456
1457 let captured = state.lock().await.body.clone().expect("payload should be captured");
1458 assert!(captured["embeds"][0]["title"]
1459 .as_str()
1460 .expect("title should be a string")
1461 .contains(&result.commit_hash));
1462 }
1463
1464 #[tokio::test]
1465 async fn test_generic_webhook_sends_real_pipeline_result() {
1466 let (url, state) = start_mock_server().await;
1467 let integration = CICDIntegration::new(CICDConfig::default());
1468 let result = sample_result(PipelineStatus::Success);
1469
1470 integration
1471 .send_webhook_notification(&url, &HashMap::new(), &result)
1472 .await
1473 .expect("mock server should accept the request");
1474
1475 let captured = state.lock().await.body.clone().expect("payload should be captured");
1476 assert_eq!(captured["commit_hash"], result.commit_hash);
1477 assert_eq!(captured["duration_ms"], result.duration_ms);
1478 }
1479
1480 #[tokio::test]
1481 async fn test_email_notification_sends_real_payload_when_configured() {
1482 let (url, state) = start_mock_server().await;
1483 let integration = CICDIntegration::new(CICDConfig::default());
1484 let result = sample_result(PipelineStatus::Success);
1485 let api = EmailApiConfig {
1486 endpoint: url,
1487 headers: HashMap::new(),
1488 };
1489
1490 integration
1491 .send_email_notification(&["dev@example.com".to_string()], Some(&api), &result)
1492 .await
1493 .expect("mock server should accept the request");
1494
1495 let captured = state.lock().await.body.clone().expect("payload should be captured");
1496 assert_eq!(captured["to"][0], "dev@example.com");
1497 assert!(captured["subject"]
1498 .as_str()
1499 .expect("subject should be a string")
1500 .contains(&result.commit_hash));
1501 }
1502 }
1503
1504 #[cfg(not(feature = "http-integrations"))]
1505 mod http_disabled {
1506 use super::*;
1507
1508 #[tokio::test]
1509 async fn test_slack_notification_fails_honestly_without_feature() {
1510 let integration = CICDIntegration::new(CICDConfig::default());
1511 let result = sample_result(PipelineStatus::Success);
1512
1513 let err = integration
1514 .send_slack_notification("http://127.0.0.1:1/hook", "#ci", &result)
1515 .await
1516 .expect_err("must not silently pretend to have sent anything");
1517 assert!(err.to_string().contains("http-integrations"));
1518 }
1519 }
1520
1521 #[tokio::test]
1522 async fn test_email_notification_is_honestly_not_configured_without_api() {
1523 let integration = CICDIntegration::new(CICDConfig::default());
1524 let result = sample_result(PipelineStatus::Success);
1525
1526 let err = integration
1527 .send_email_notification(&["dev@example.com".to_string()], None, &result)
1528 .await
1529 .expect_err("must not silently pretend to have sent an email");
1530 assert!(err.to_string().contains("endpoint configured"));
1531 }
1532
1533 #[tokio::test]
1534 async fn test_custom_notification_channel_errors_instead_of_silently_succeeding() {
1535 let mut config = CICDConfig::default();
1536 config.enable_alert_systems = true;
1537 config.notification_channels = vec![NotificationChannel::Custom("pagerduty".to_string())];
1538 let integration = CICDIntegration::new(config);
1539 let result = sample_result(PipelineStatus::Success);
1540
1541 let err = integration
1545 .send_notifications(&result)
1546 .await
1547 .expect_err("an unimplemented channel must not report success");
1548 assert!(err.to_string().contains("no delivery implementation"));
1549 }
1550}