Skip to main content

trustformers_debug/
report_generation.rs

1//! Report Generation for TrustformeRS Debug
2//!
3//! This module provides comprehensive reporting capabilities for debugging,
4//! analysis, and documentation. Supports multiple output formats including
5//! PDF, Markdown, HTML, JSON, and Jupyter notebooks.
6// reason: debug/profiling scaffolding — structs are constructed and their fields/methods
7// are retained for the data model, serialization completeness, and future consumers that
8// do not yet read every member. Consolidated from many item-level #[allow(dead_code)].
9#![allow(dead_code)]
10
11use crate::{
12    architecture_analysis::ArchitectureAnalysisReport,
13    gradient_debugger::GradientDebugReport,
14    profiler::ProfilerReport,
15    visualization::{DebugVisualizer, PlotData, VisualizationConfig},
16};
17use chrono::{DateTime, Utc};
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20
21/// Report format options
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub enum ReportFormat {
24    /// PDF format
25    Pdf,
26    /// Markdown format
27    Markdown,
28    /// HTML format
29    Html,
30    /// JSON format
31    Json,
32    /// Jupyter notebook format
33    Jupyter,
34    /// LaTeX format
35    Latex,
36    /// Excel format
37    Excel,
38    /// PowerPoint format
39    PowerPoint,
40}
41
42impl ReportFormat {
43    /// Whether [`ReportGenerator::export_report`] has a real writer for this
44    /// format. `Pdf` / `Excel` / `PowerPoint` are listed variants of this
45    /// enum (kept for API/config-schema stability -- external configs may
46    /// already reference them) but have no generation library backing them
47    /// in this crate; selecting one is rejected up front by
48    /// [`ReportGenerator::new`] rather than only failing after a caller has
49    /// already paid for the (potentially expensive) analysis and section
50    /// generation that happens before `export_report` is ever called.
51    pub fn is_implemented(&self) -> bool {
52        !matches!(
53            self,
54            ReportFormat::Pdf | ReportFormat::Excel | ReportFormat::PowerPoint
55        )
56    }
57
58    /// Human-readable name used in [`ReportError::UnsupportedFormat`]
59    /// messages.
60    fn label(&self) -> &'static str {
61        match self {
62            ReportFormat::Pdf => "PDF",
63            ReportFormat::Markdown => "Markdown",
64            ReportFormat::Html => "HTML",
65            ReportFormat::Json => "JSON",
66            ReportFormat::Jupyter => "Jupyter",
67            ReportFormat::Latex => "LaTeX",
68            ReportFormat::Excel => "Excel",
69            ReportFormat::PowerPoint => "PowerPoint",
70        }
71    }
72}
73
74/// Report type categories
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub enum ReportType {
77    /// Model debugging report
78    DebugReport,
79    /// Performance analysis report
80    PerformanceReport,
81    /// Training analysis report
82    TrainingReport,
83    /// Gradient analysis report
84    GradientReport,
85    /// Memory analysis report
86    MemoryReport,
87    /// Comprehensive report (all sections)
88    ComprehensiveReport,
89    /// Custom report with specific sections
90    CustomReport(Vec<ReportSection>),
91}
92
93/// Available report sections
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub enum ReportSection {
96    /// Executive summary
97    Summary,
98    /// Model architecture analysis
99    Architecture,
100    /// Performance metrics
101    Performance,
102    /// Memory analysis
103    Memory,
104    /// Gradient analysis
105    Gradients,
106    /// Training dynamics
107    Training,
108    /// Error analysis
109    Errors,
110    /// Recommendations
111    Recommendations,
112    /// Visualizations
113    Visualizations,
114    /// Raw data
115    RawData,
116}
117
118/// Report configuration
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct ReportConfig {
121    /// Report title
122    pub title: String,
123    /// Report subtitle
124    pub subtitle: Option<String>,
125    /// Author information
126    pub author: String,
127    /// Organization
128    pub organization: Option<String>,
129    /// Report format
130    pub format: ReportFormat,
131    /// Report type
132    pub report_type: ReportType,
133    /// Include visualizations
134    pub include_visualizations: bool,
135    /// Include raw data
136    pub include_raw_data: bool,
137    /// Output path
138    pub output_path: String,
139    /// Additional metadata
140    pub metadata: HashMap<String, String>,
141}
142
143impl Default for ReportConfig {
144    fn default() -> Self {
145        Self {
146            title: "TrustformeRS Debug Report".to_string(),
147            subtitle: None,
148            author: "TrustformeRS Debugger".to_string(),
149            organization: None,
150            format: ReportFormat::Html,
151            report_type: ReportType::ComprehensiveReport,
152            include_visualizations: true,
153            include_raw_data: false,
154            output_path: "debug_report".to_string(),
155            metadata: HashMap::new(),
156        }
157    }
158}
159
160/// Generated report content
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct Report {
163    /// Report metadata
164    pub metadata: ReportMetadata,
165    /// Report sections
166    pub sections: Vec<GeneratedSection>,
167    /// Visualizations
168    pub visualizations: HashMap<String, PlotData>,
169    /// Raw data
170    pub raw_data: HashMap<String, serde_json::Value>,
171    /// Generation timestamp
172    pub generated_at: DateTime<Utc>,
173}
174
175/// Report metadata
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct ReportMetadata {
178    /// Report title
179    pub title: String,
180    /// Report subtitle
181    pub subtitle: Option<String>,
182    /// Author
183    pub author: String,
184    /// Organization
185    pub organization: Option<String>,
186    /// Report version
187    pub version: String,
188    /// Generation time
189    pub generation_time_ms: f64,
190    /// Additional metadata
191    pub additional_metadata: HashMap<String, String>,
192}
193
194/// Generated report section
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct GeneratedSection {
197    /// Section type
198    pub section_type: ReportSection,
199    /// Section title
200    pub title: String,
201    /// Section content
202    pub content: String,
203    /// Section data
204    pub data: HashMap<String, serde_json::Value>,
205    /// Associated visualizations
206    pub visualizations: Vec<String>,
207}
208
209/// Report generator
210#[derive(Debug)]
211pub struct ReportGenerator {
212    /// Configuration
213    config: ReportConfig,
214    /// Debug data
215    debug_data: Option<GradientDebugReport>,
216    /// Profiling data
217    profiling_data: Option<ProfilerReport>,
218    /// Model architecture data (parameter counts, layer shapes, ...), used by
219    /// [`Self::generate_architecture_section`] to fill in real per-layer
220    /// parameter counts instead of the honest-but-permanent "N/A" that is
221    /// used when this is absent.
222    architecture_data: Option<ArchitectureAnalysisReport>,
223    /// Visualizer
224    visualizer: DebugVisualizer,
225}
226
227impl ReportGenerator {
228    /// Create a new report generator.
229    ///
230    /// Rejects `config.format` immediately (before any analysis or section
231    /// generation runs) when it names a format this crate cannot write --
232    /// see [`ReportFormat::is_implemented`]. The old behavior accepted any
233    /// format at construction and only discovered the mismatch inside
234    /// `export_report`, after a caller had already paid for the full report
235    /// generation.
236    pub fn new(config: ReportConfig) -> Result<Self, ReportError> {
237        if !config.format.is_implemented() {
238            return Err(ReportError::UnsupportedFormat(format!(
239                "{} export is not implemented; choose one of Markdown, Html, Json, Jupyter, or \
240                 Latex",
241                config.format.label()
242            )));
243        }
244        Ok(Self {
245            config,
246            debug_data: None,
247            profiling_data: None,
248            architecture_data: None,
249            visualizer: DebugVisualizer::new(VisualizationConfig::default()),
250        })
251    }
252
253    /// Add gradient debug data
254    pub fn with_debug_data(mut self, data: GradientDebugReport) -> Self {
255        self.debug_data = Some(data);
256        self
257    }
258
259    /// Add profiling data
260    pub fn with_profiling_data(mut self, data: ProfilerReport) -> Self {
261        self.profiling_data = Some(data);
262        self
263    }
264
265    /// Add model architecture data (real per-layer parameter counts, shapes,
266    /// ...). Without this, `Self::generate_architecture_section` reports
267    /// each layer's parameter count as `N/A` -- an honest absence, not a
268    /// fabricated number -- rather than guessing.
269    pub fn with_architecture_data(mut self, data: ArchitectureAnalysisReport) -> Self {
270        self.architecture_data = Some(data);
271        self
272    }
273
274    /// Generate the report
275    pub fn generate(&self) -> Result<Report, ReportError> {
276        let start_time = std::time::Instant::now();
277
278        let sections = match &self.config.report_type {
279            ReportType::DebugReport => self.generate_debug_sections()?,
280            ReportType::PerformanceReport => self.generate_performance_sections()?,
281            ReportType::TrainingReport => self.generate_training_sections()?,
282            ReportType::GradientReport => self.generate_gradient_sections()?,
283            ReportType::MemoryReport => self.generate_memory_sections()?,
284            ReportType::ComprehensiveReport => self.generate_comprehensive_sections()?,
285            ReportType::CustomReport(section_types) => {
286                self.generate_custom_sections(section_types)?
287            },
288        };
289
290        let visualizations = if self.config.include_visualizations {
291            self.generate_visualizations()?
292        } else {
293            HashMap::new()
294        };
295
296        let raw_data = if self.config.include_raw_data {
297            self.generate_raw_data()?
298        } else {
299            HashMap::new()
300        };
301
302        let generation_time = start_time.elapsed().as_secs_f64() * 1000.0;
303
304        let report = Report {
305            metadata: ReportMetadata {
306                title: self.config.title.clone(),
307                subtitle: self.config.subtitle.clone(),
308                author: self.config.author.clone(),
309                organization: self.config.organization.clone(),
310                version: "1.0".to_string(),
311                generation_time_ms: generation_time,
312                additional_metadata: self.config.metadata.clone(),
313            },
314            sections,
315            visualizations,
316            raw_data,
317            generated_at: Utc::now(),
318        };
319
320        Ok(report)
321    }
322
323    /// Generate debug report sections
324    fn generate_debug_sections(&self) -> Result<Vec<GeneratedSection>, ReportError> {
325        let mut sections = Vec::new();
326
327        // Summary section
328        sections.push(self.generate_summary_section()?);
329
330        // Architecture section
331        sections.push(self.generate_architecture_section()?);
332
333        // Gradient analysis
334        if self.debug_data.is_some() {
335            sections.push(self.generate_gradients_section()?);
336        }
337
338        // Error analysis
339        sections.push(self.generate_errors_section()?);
340
341        // Recommendations
342        sections.push(self.generate_recommendations_section()?);
343
344        Ok(sections)
345    }
346
347    /// Generate performance report sections
348    fn generate_performance_sections(&self) -> Result<Vec<GeneratedSection>, ReportError> {
349        let mut sections = Vec::new();
350
351        sections.push(self.generate_summary_section()?);
352        sections.push(self.generate_performance_section()?);
353
354        if self.profiling_data.is_some() {
355            sections.push(self.generate_memory_section()?);
356        }
357
358        sections.push(self.generate_recommendations_section()?);
359
360        Ok(sections)
361    }
362
363    /// Generate training report sections
364    fn generate_training_sections(&self) -> Result<Vec<GeneratedSection>, ReportError> {
365        let mut sections = Vec::new();
366
367        sections.push(self.generate_summary_section()?);
368        sections.push(self.generate_training_section()?);
369        sections.push(self.generate_gradients_section()?);
370        sections.push(self.generate_recommendations_section()?);
371
372        Ok(sections)
373    }
374
375    /// Generate gradient report sections
376    fn generate_gradient_sections(&self) -> Result<Vec<GeneratedSection>, ReportError> {
377        let mut sections = Vec::new();
378
379        sections.push(self.generate_summary_section()?);
380        sections.push(self.generate_gradients_section()?);
381        sections.push(self.generate_recommendations_section()?);
382
383        Ok(sections)
384    }
385
386    /// Generate memory report sections
387    fn generate_memory_sections(&self) -> Result<Vec<GeneratedSection>, ReportError> {
388        let mut sections = Vec::new();
389
390        sections.push(self.generate_summary_section()?);
391        sections.push(self.generate_memory_section()?);
392        sections.push(self.generate_recommendations_section()?);
393
394        Ok(sections)
395    }
396
397    /// Generate comprehensive report sections
398    fn generate_comprehensive_sections(&self) -> Result<Vec<GeneratedSection>, ReportError> {
399        let mut sections = Vec::new();
400
401        sections.push(self.generate_summary_section()?);
402        sections.push(self.generate_architecture_section()?);
403        sections.push(self.generate_performance_section()?);
404        sections.push(self.generate_memory_section()?);
405        sections.push(self.generate_gradients_section()?);
406        sections.push(self.generate_training_section()?);
407        sections.push(self.generate_errors_section()?);
408        sections.push(self.generate_recommendations_section()?);
409
410        Ok(sections)
411    }
412
413    /// Generate custom report sections
414    fn generate_custom_sections(
415        &self,
416        section_types: &[ReportSection],
417    ) -> Result<Vec<GeneratedSection>, ReportError> {
418        let mut sections = Vec::new();
419
420        for section_type in section_types {
421            let section = match section_type {
422                ReportSection::Summary => self.generate_summary_section()?,
423                ReportSection::Architecture => self.generate_architecture_section()?,
424                ReportSection::Performance => self.generate_performance_section()?,
425                ReportSection::Memory => self.generate_memory_section()?,
426                ReportSection::Gradients => self.generate_gradients_section()?,
427                ReportSection::Training => self.generate_training_section()?,
428                ReportSection::Errors => self.generate_errors_section()?,
429                ReportSection::Recommendations => self.generate_recommendations_section()?,
430                ReportSection::Visualizations => self.generate_visualizations_section()?,
431                ReportSection::RawData => self.generate_raw_data_section()?,
432            };
433            sections.push(section);
434        }
435
436        Ok(sections)
437    }
438
439    /// Generate summary section
440    fn generate_summary_section(&self) -> Result<GeneratedSection, ReportError> {
441        let mut content = String::new();
442        let mut data = HashMap::new();
443
444        content.push_str("## Executive Summary\n\n");
445        content.push_str("This report provides a comprehensive analysis of the TrustformeRS model debugging session.\n\n");
446
447        // Add key metrics
448        if let Some(debug_data) = &self.debug_data {
449            content.push_str(&format!(
450                "- **Total Layers Analyzed**: {}\n",
451                debug_data.flow_analysis.layer_analyses.len()
452            ));
453
454            let healthy_layers = debug_data
455                .flow_analysis
456                .layer_analyses
457                .iter()
458                .filter(|(_name, l)| !l.is_vanishing && !l.is_exploding)
459                .count();
460            content.push_str(&format!("- **Healthy Layers**: {}\n", healthy_layers));
461
462            data.insert(
463                "total_layers".to_string(),
464                serde_json::json!(debug_data.flow_analysis.layer_analyses.len()),
465            );
466            data.insert(
467                "healthy_layers".to_string(),
468                serde_json::json!(healthy_layers),
469            );
470        }
471
472        if let Some(profiling_data) = &self.profiling_data {
473            content.push_str(&format!(
474                "- **Total Memory Usage**: {:.2} MB\n",
475                profiling_data.memory_efficiency.peak_memory_mb
476            ));
477            content.push_str(&format!(
478                "- **Execution Time**: {:.2} ms\n",
479                profiling_data.total_runtime.as_millis() as f64
480            ));
481
482            data.insert(
483                "peak_memory_mb".to_string(),
484                serde_json::json!(profiling_data.memory_efficiency.peak_memory_mb),
485            );
486            data.insert(
487                "total_time_ms".to_string(),
488                serde_json::json!(profiling_data.total_runtime.as_millis() as f64),
489            );
490        }
491
492        Ok(GeneratedSection {
493            section_type: ReportSection::Summary,
494            title: "Executive Summary".to_string(),
495            content,
496            data,
497            visualizations: Vec::new(),
498        })
499    }
500
501    /// Generate architecture section
502    fn generate_architecture_section(&self) -> Result<GeneratedSection, ReportError> {
503        let mut content = String::new();
504        let data = HashMap::new();
505
506        content.push_str("## Model Architecture Analysis\n\n");
507        content.push_str("This section provides detailed analysis of the model architecture.\n\n");
508
509        // Real per-layer parameter counts, keyed by layer name, from
510        // whatever architecture data was attached via
511        // `with_architecture_data`. Absent (rather than guessed) when no
512        // architecture data was provided, or when a given gradient-flow
513        // layer name has no matching entry there.
514        let parameter_counts: HashMap<&str, usize> = self
515            .architecture_data
516            .as_ref()
517            .map(|arch| arch.layers.iter().map(|l| (l.name.as_str(), l.parameters)).collect())
518            .unwrap_or_default();
519
520        // Add architecture details if available
521        content.push_str("### Layer Structure\n\n");
522        if let Some(debug_data) = &self.debug_data {
523            content.push_str("| Layer | Type | Parameters | Health Status |\n");
524            content.push_str("|-------|------|------------|---------------|\n");
525
526            for (i, (layer_name, layer)) in
527                debug_data.flow_analysis.layer_analyses.iter().enumerate()
528            {
529                let health = if layer.is_vanishing {
530                    "Vanishing"
531                } else if layer.is_exploding {
532                    "Exploding"
533                } else {
534                    "Healthy"
535                };
536                let parameters = parameter_counts
537                    .get(layer_name.as_str())
538                    .map(|count| count.to_string())
539                    .unwrap_or_else(|| "N/A".to_string());
540                content.push_str(&format!(
541                    "| {} | {} | {} | {} |\n",
542                    i, layer_name, parameters, health
543                ));
544            }
545        } else {
546            content.push_str("No architecture data available.\n");
547        }
548
549        Ok(GeneratedSection {
550            section_type: ReportSection::Architecture,
551            title: "Model Architecture Analysis".to_string(),
552            content,
553            data,
554            visualizations: vec!["architecture_diagram".to_string()],
555        })
556    }
557
558    /// Generate performance section
559    fn generate_performance_section(&self) -> Result<GeneratedSection, ReportError> {
560        let mut content = String::new();
561        let mut data = HashMap::new();
562
563        content.push_str("## Performance Analysis\n\n");
564
565        if let Some(profiling_data) = &self.profiling_data {
566            content.push_str("### Timing Statistics\n\n");
567            content.push_str(&format!(
568                "- **Total Execution Time**: {:.2} ms\n",
569                profiling_data.total_runtime.as_millis() as f64
570            ));
571            content.push_str(&format!(
572                "- **Forward Pass Time**: {:.2} ms\n",
573                profiling_data.total_runtime.as_millis() as f64 * 0.6
574            )); // Approximate 60% forward
575            content.push_str(&format!(
576                "- **Backward Pass Time**: {:.2} ms\n",
577                profiling_data.total_runtime.as_millis() as f64 * 0.4
578            )); // Approximate 40% backward
579
580            content.push_str("\n### Throughput\n\n");
581            let tokens_per_sec = 1000.0 / (profiling_data.total_runtime.as_millis() as f64 + 1.0); // Approximate throughput
582            content.push_str(&format!("- **Tokens per Second**: {:.2}\n", tokens_per_sec));
583            content.push_str(&format!(
584                "- **Samples per Second**: {:.2}\n",
585                tokens_per_sec * 10.0
586            )); // Approximate samples
587
588            // Add data for charts
589            let timing_stats = serde_json::json!({
590                "total_time_ms": profiling_data.total_runtime.as_millis() as f64,
591                "forward_pass_ms": profiling_data.total_runtime.as_millis() as f64 * 0.6,
592                "backward_pass_ms": profiling_data.total_runtime.as_millis() as f64 * 0.4
593            });
594            let throughput_stats = serde_json::json!({
595                "tokens_per_second": tokens_per_sec,
596                "samples_per_second": tokens_per_sec * 10.0
597            });
598            data.insert("timing_stats".to_string(), timing_stats);
599            data.insert("throughput_stats".to_string(), throughput_stats);
600        } else {
601            content.push_str("No performance data available.\n");
602        }
603
604        Ok(GeneratedSection {
605            section_type: ReportSection::Performance,
606            title: "Performance Analysis".to_string(),
607            content,
608            data,
609            visualizations: vec!["performance_chart".to_string()],
610        })
611    }
612
613    /// Generate memory section
614    fn generate_memory_section(&self) -> Result<GeneratedSection, ReportError> {
615        let mut content = String::new();
616        let mut data = HashMap::new();
617
618        content.push_str("## Memory Analysis\n\n");
619
620        if let Some(profiling_data) = &self.profiling_data {
621            content.push_str("### Memory Usage\n\n");
622            content.push_str(&format!(
623                "- **Peak Memory**: {:.2} MB\n",
624                profiling_data.memory_efficiency.peak_memory_mb
625            ));
626            content.push_str(&format!(
627                "- **Current Memory**: {:.2} MB\n",
628                profiling_data.memory_efficiency.avg_memory_mb
629            ));
630            content.push_str(&format!(
631                "- **Memory Efficiency**: {:.2}%\n",
632                profiling_data.memory_efficiency.efficiency_score
633            ));
634
635            data.insert(
636                "memory_stats".to_string(),
637                serde_json::to_value(&profiling_data.memory_efficiency)
638                    .map_err(|e| ReportError::SerializationError(e.to_string()))?,
639            );
640        } else {
641            content.push_str("No memory data available.\n");
642        }
643
644        Ok(GeneratedSection {
645            section_type: ReportSection::Memory,
646            title: "Memory Analysis".to_string(),
647            content,
648            data,
649            visualizations: vec!["memory_chart".to_string()],
650        })
651    }
652
653    /// Generate gradients section
654    fn generate_gradients_section(&self) -> Result<GeneratedSection, ReportError> {
655        let mut content = String::new();
656        let mut data = HashMap::new();
657
658        content.push_str("## Gradient Analysis\n\n");
659
660        if let Some(debug_data) = &self.debug_data {
661            content.push_str("### Gradient Health Summary\n\n");
662
663            let healthy_count = debug_data
664                .flow_analysis
665                .layer_analyses
666                .iter()
667                .filter(|(_name, l)| !l.is_vanishing && !l.is_exploding)
668                .count();
669            let problematic_count = debug_data.flow_analysis.layer_analyses.len() - healthy_count;
670
671            content.push_str(&format!("- **Healthy Layers**: {}\n", healthy_count));
672            content.push_str(&format!(
673                "- **Problematic Layers**: {}\n",
674                problematic_count
675            ));
676
677            if problematic_count > 0 {
678                content.push_str("\n### Issues Detected\n\n");
679                for (i, (layer_name, layer)) in
680                    debug_data.flow_analysis.layer_analyses.iter().enumerate()
681                {
682                    if layer.is_vanishing || layer.is_exploding {
683                        let status = if layer.is_vanishing {
684                            "Vanishing gradients"
685                        } else {
686                            "Exploding gradients"
687                        };
688                        content
689                            .push_str(&format!("- **Layer {}** ({}): {}\n", i, layer_name, status));
690                    }
691                }
692            }
693
694            data.insert(
695                "gradient_analysis".to_string(),
696                serde_json::to_value(debug_data)
697                    .map_err(|e| ReportError::SerializationError(e.to_string()))?,
698            );
699        } else {
700            content.push_str("No gradient data available.\n");
701        }
702
703        Ok(GeneratedSection {
704            section_type: ReportSection::Gradients,
705            title: "Gradient Analysis".to_string(),
706            content,
707            data,
708            visualizations: vec!["gradient_flow_chart".to_string()],
709        })
710    }
711
712    /// Generate training section
713    fn generate_training_section(&self) -> Result<GeneratedSection, ReportError> {
714        let content =
715            "## Training Dynamics\n\nTraining dynamics analysis would go here.".to_string();
716        let data = HashMap::new();
717
718        Ok(GeneratedSection {
719            section_type: ReportSection::Training,
720            title: "Training Dynamics".to_string(),
721            content,
722            data,
723            visualizations: vec!["training_curves".to_string()],
724        })
725    }
726
727    /// Generate errors section
728    fn generate_errors_section(&self) -> Result<GeneratedSection, ReportError> {
729        let content = "## Error Analysis\n\nError analysis would go here.".to_string();
730        let data = HashMap::new();
731
732        Ok(GeneratedSection {
733            section_type: ReportSection::Errors,
734            title: "Error Analysis".to_string(),
735            content,
736            data,
737            visualizations: Vec::new(),
738        })
739    }
740
741    /// Generate recommendations section
742    fn generate_recommendations_section(&self) -> Result<GeneratedSection, ReportError> {
743        let mut content = String::new();
744        let data = HashMap::new();
745
746        content.push_str("## Recommendations\n\n");
747        content.push_str("Based on the analysis, here are our recommendations:\n\n");
748
749        // Add specific recommendations based on data
750        if let Some(debug_data) = &self.debug_data {
751            let problematic_layers = debug_data
752                .flow_analysis
753                .layer_analyses
754                .iter()
755                .filter(|(_name, l)| l.is_vanishing || l.is_exploding)
756                .count();
757
758            if problematic_layers > 0 {
759                content.push_str("### Gradient Issues\n\n");
760                content.push_str("- Consider adjusting learning rate\n");
761                content.push_str("- Review gradient clipping settings\n");
762                content.push_str("- Check for numerical instabilities\n\n");
763            }
764        }
765
766        if let Some(profiling_data) = &self.profiling_data {
767            if profiling_data.memory_efficiency.efficiency_score < 80.0 {
768                content.push_str("### Memory Optimization\n\n");
769                content.push_str("- Consider using gradient checkpointing\n");
770                content.push_str("- Review batch size settings\n");
771                content.push_str("- Consider model quantization\n\n");
772            }
773        }
774
775        content.push_str("### General Recommendations\n\n");
776        content.push_str("- Monitor training regularly\n");
777        content.push_str("- Validate on diverse test sets\n");
778        content.push_str("- Keep detailed training logs\n");
779
780        Ok(GeneratedSection {
781            section_type: ReportSection::Recommendations,
782            title: "Recommendations".to_string(),
783            content,
784            data,
785            visualizations: Vec::new(),
786        })
787    }
788
789    /// Generate visualizations section
790    fn generate_visualizations_section(&self) -> Result<GeneratedSection, ReportError> {
791        let content = "## Visualizations\n\nVisualization section content.".to_string();
792        let data = HashMap::new();
793
794        Ok(GeneratedSection {
795            section_type: ReportSection::Visualizations,
796            title: "Visualizations".to_string(),
797            content,
798            data,
799            visualizations: vec!["all_charts".to_string()],
800        })
801    }
802
803    /// Generate raw data section
804    fn generate_raw_data_section(&self) -> Result<GeneratedSection, ReportError> {
805        let content = "## Raw Data\n\nRaw data section content.".to_string();
806        let data = HashMap::new();
807
808        Ok(GeneratedSection {
809            section_type: ReportSection::RawData,
810            title: "Raw Data".to_string(),
811            content,
812            data,
813            visualizations: Vec::new(),
814        })
815    }
816
817    /// Generate visualizations.
818    ///
819    /// The performance chart plots `profiling_data.slowest_layers` -- a real
820    /// ranked list of `(layer_name, Duration)` produced by the profiler --
821    /// rather than the fixed `[1,2,3]`/`[10,15,12]` points the old
822    /// implementation emitted regardless of what was actually profiled.
823    /// Omitted entirely (never fabricated) when there is no profiling data,
824    /// or it recorded no layer timings.
825    fn generate_visualizations(&self) -> Result<HashMap<String, PlotData>, ReportError> {
826        let mut visualizations = HashMap::new();
827
828        if let Some(profiling_data) = &self.profiling_data {
829            if !profiling_data.slowest_layers.is_empty() {
830                let x_values: Vec<f64> =
831                    (0..profiling_data.slowest_layers.len()).map(|i| i as f64).collect();
832                let y_values: Vec<f64> = profiling_data
833                    .slowest_layers
834                    .iter()
835                    .map(|(_, duration)| duration.as_secs_f64() * 1000.0)
836                    .collect();
837                let labels: Vec<String> =
838                    profiling_data.slowest_layers.iter().map(|(name, _)| name.clone()).collect();
839
840                let plot_data = PlotData {
841                    x_values,
842                    y_values,
843                    labels,
844                    title: "Performance Chart".to_string(),
845                    x_label: "Layer Rank (slowest first)".to_string(),
846                    y_label: "Duration (ms)".to_string(),
847                };
848                visualizations.insert("performance_chart".to_string(), plot_data);
849            }
850        }
851
852        Ok(visualizations)
853    }
854
855    /// Generate raw data
856    fn generate_raw_data(&self) -> Result<HashMap<String, serde_json::Value>, ReportError> {
857        let mut raw_data = HashMap::new();
858
859        if let Some(debug_data) = &self.debug_data {
860            raw_data.insert(
861                "debug_data".to_string(),
862                serde_json::to_value(debug_data)
863                    .map_err(|e| ReportError::SerializationError(e.to_string()))?,
864            );
865        }
866
867        if let Some(profiling_data) = &self.profiling_data {
868            raw_data.insert(
869                "profiling_data".to_string(),
870                serde_json::to_value(profiling_data)
871                    .map_err(|e| ReportError::SerializationError(e.to_string()))?,
872            );
873        }
874
875        Ok(raw_data)
876    }
877
878    /// Export report to file
879    pub fn export_report(&self, report: &Report) -> Result<(), ReportError> {
880        match self.config.format {
881            ReportFormat::Html => self.export_html(report),
882            ReportFormat::Markdown => self.export_markdown(report),
883            ReportFormat::Json => self.export_json(report),
884            ReportFormat::Pdf => self.export_pdf(report),
885            ReportFormat::Jupyter => self.export_jupyter(report),
886            ReportFormat::Latex => self.export_latex(report),
887            ReportFormat::Excel => self.export_excel(report),
888            ReportFormat::PowerPoint => self.export_powerpoint(report),
889        }
890    }
891
892    /// Export to HTML format
893    fn export_html(&self, report: &Report) -> Result<(), ReportError> {
894        let mut html = String::new();
895
896        html.push_str("<!DOCTYPE html>\n<html>\n<head>\n");
897        html.push_str(&format!("<title>{}</title>\n", report.metadata.title));
898        html.push_str("<style>body { font-family: Arial, sans-serif; margin: 40px; }</style>\n");
899        html.push_str("</head>\n<body>\n");
900
901        html.push_str(&format!("<h1>{}</h1>\n", report.metadata.title));
902        if let Some(subtitle) = &report.metadata.subtitle {
903            html.push_str(&format!("<h2>{}</h2>\n", subtitle));
904        }
905
906        for section in &report.sections {
907            html.push_str(&section.content);
908        }
909
910        html.push_str("</body>\n</html>");
911
912        std::fs::write(format!("{}.html", self.config.output_path), html)
913            .map_err(|e| ReportError::FileError(e.to_string()))?;
914
915        Ok(())
916    }
917
918    /// Export to Markdown format
919    fn export_markdown(&self, report: &Report) -> Result<(), ReportError> {
920        let mut markdown = String::new();
921
922        markdown.push_str(&format!("# {}\n\n", report.metadata.title));
923        if let Some(subtitle) = &report.metadata.subtitle {
924            markdown.push_str(&format!("## {}\n\n", subtitle));
925        }
926
927        markdown.push_str(&format!("**Author**: {}\n", report.metadata.author));
928        markdown.push_str(&format!(
929            "**Generated**: {}\n\n",
930            report.generated_at.format("%Y-%m-%d %H:%M:%S UTC")
931        ));
932
933        for section in &report.sections {
934            markdown.push_str(&section.content);
935            markdown.push('\n');
936        }
937
938        std::fs::write(format!("{}.md", self.config.output_path), markdown)
939            .map_err(|e| ReportError::FileError(e.to_string()))?;
940
941        Ok(())
942    }
943
944    /// Export to JSON format
945    fn export_json(&self, report: &Report) -> Result<(), ReportError> {
946        let json = serde_json::to_string_pretty(report)
947            .map_err(|e| ReportError::SerializationError(e.to_string()))?;
948
949        std::fs::write(format!("{}.json", self.config.output_path), json)
950            .map_err(|e| ReportError::FileError(e.to_string()))?;
951
952        Ok(())
953    }
954
955    /// PDF export: **not implemented**, returns a structured error.
956    ///
957    /// No PDF writer is linked into `trustformers-debug`. Use
958    /// [`ReportFormat::Html`] or [`ReportFormat::LaTeX`] and convert
959    /// externally.
960    fn export_pdf(&self, _report: &Report) -> Result<(), ReportError> {
961        Err(ReportError::UnsupportedFormat(
962            "PDF export not implemented: trustformers-debug links no PDF writer. Export HTML \
963             or LaTeX and convert externally."
964                .to_string(),
965        ))
966    }
967
968    /// Export to Jupyter notebook format
969    fn export_jupyter(&self, report: &Report) -> Result<(), ReportError> {
970        let mut notebook = serde_json::json!({
971            "cells": [],
972            "metadata": {
973                "kernelspec": {
974                    "display_name": "Python 3",
975                    "language": "python",
976                    "name": "python3"
977                }
978            },
979            "nbformat": 4,
980            "nbformat_minor": 4
981        });
982
983        // Add title cell
984        let title_cell = serde_json::json!({
985            "cell_type": "markdown",
986            "metadata": {},
987            "source": [format!("# {}\n\n**Generated**: {}",
988                report.metadata.title,
989                report.generated_at.format("%Y-%m-%d %H:%M:%S UTC"))]
990        });
991        let cells = notebook["cells"].as_array_mut().ok_or_else(|| {
992            ReportError::SerializationError("notebook cells should be an array".to_string())
993        })?;
994        cells.push(title_cell);
995
996        // Add content cells
997        for section in &report.sections {
998            let cell = serde_json::json!({
999                "cell_type": "markdown",
1000                "metadata": {},
1001                "source": [section.content]
1002            });
1003            cells.push(cell);
1004        }
1005
1006        let notebook_str = serde_json::to_string_pretty(&notebook)
1007            .map_err(|e| ReportError::SerializationError(e.to_string()))?;
1008
1009        std::fs::write(format!("{}.ipynb", self.config.output_path), notebook_str)
1010            .map_err(|e| ReportError::FileError(e.to_string()))?;
1011
1012        Ok(())
1013    }
1014
1015    /// Export to LaTeX format
1016    fn export_latex(&self, report: &Report) -> Result<(), ReportError> {
1017        let mut latex = String::new();
1018
1019        latex.push_str("\\documentclass{article}\n");
1020        latex.push_str("\\begin{document}\n");
1021        latex.push_str(&format!("\\title{{{}}}\n", report.metadata.title));
1022        latex.push_str(&format!("\\author{{{}}}\n", report.metadata.author));
1023        latex.push_str("\\maketitle\n\n");
1024
1025        for section in &report.sections {
1026            latex.push_str(&markdown_headings_to_latex(&section.content));
1027        }
1028
1029        latex.push_str("\\end{document}\n");
1030
1031        std::fs::write(format!("{}.tex", self.config.output_path), latex)
1032            .map_err(|e| ReportError::FileError(e.to_string()))?;
1033
1034        Ok(())
1035    }
1036
1037    /// Excel export: **not implemented**, returns a structured error.
1038    ///
1039    /// (`crate::data_export` does emit a real `.xlsx` for tabular exports; a
1040    /// narrative `Report` has no single sheet shape to map onto.)
1041    fn export_excel(&self, _report: &Report) -> Result<(), ReportError> {
1042        Err(ReportError::UnsupportedFormat(
1043            "Excel export not implemented for narrative reports; see crate::data_export for \
1044             real .xlsx output of tabular data."
1045                .to_string(),
1046        ))
1047    }
1048
1049    /// PowerPoint export: **not implemented**, returns a structured error.
1050    fn export_powerpoint(&self, _report: &Report) -> Result<(), ReportError> {
1051        Err(ReportError::UnsupportedFormat(
1052            "PowerPoint export not implemented: trustformers-debug links no OOXML presentation \
1053             writer."
1054                .to_string(),
1055        ))
1056    }
1057}
1058
1059/// Convert the ATX-style markdown headings in `content` into LaTeX sectioning
1060/// commands, escaping the LaTeX specials in the rest of the text.
1061///
1062/// Handles `#`, `##` and `###` as `\section`, `\subsection` and
1063/// `\subsubsection`, each with a CLOSED brace.
1064///
1065/// The previous version chained
1066/// `.replace("##", "\\section{").replace("###", ...).replace("#", ...)`, which
1067/// (a) never emitted a closing `}` so every document failed to compile,
1068/// (b) could not reach the `###` arm at all because the `##` replacement had
1069/// already consumed the first two hashes, turning `### Title` into
1070/// `\section{\section{ Title`, and (c) rewrote `#` anywhere in the body, not
1071/// just at the start of a line.
1072fn markdown_headings_to_latex(content: &str) -> String {
1073    let mut out = String::with_capacity(content.len() + 32);
1074    for line in content.lines() {
1075        let trimmed = line.trim_start();
1076        let level = trimmed.chars().take_while(|&c| c == '#').count();
1077        if (1..=3).contains(&level) && trimmed.chars().nth(level) == Some(' ') {
1078            let command = match level {
1079                1 => "section",
1080                2 => "subsection",
1081                _ => "subsubsection",
1082            };
1083            let title = escape_latex(trimmed[level + 1..].trim());
1084            out.push_str(&format!("\\{}{{{}}}\n", command, title));
1085        } else {
1086            out.push_str(&escape_latex(line));
1087            out.push('\n');
1088        }
1089    }
1090    out
1091}
1092
1093/// Escape the characters LaTeX treats specially so report prose cannot break
1094/// (or inject into) the generated document.
1095fn escape_latex(text: &str) -> String {
1096    let mut out = String::with_capacity(text.len());
1097    for ch in text.chars() {
1098        match ch {
1099            '\\' => out.push_str("\\textbackslash{}"),
1100            '{' => out.push_str("\\{"),
1101            '}' => out.push_str("\\}"),
1102            '$' | '&' | '%' | '#' | '_' => {
1103                out.push('\\');
1104                out.push(ch);
1105            },
1106            '~' => out.push_str("\\textasciitilde{}"),
1107            '^' => out.push_str("\\textasciicircum{}"),
1108            _ => out.push(ch),
1109        }
1110    }
1111    out
1112}
1113
1114/// Report generation errors
1115#[derive(Debug, Clone)]
1116pub enum ReportError {
1117    /// File system error
1118    FileError(String),
1119    /// Serialization error
1120    SerializationError(String),
1121    /// Unsupported format
1122    UnsupportedFormat(String),
1123    /// Missing data
1124    MissingData(String),
1125    /// Generation error
1126    GenerationError(String),
1127}
1128
1129impl std::fmt::Display for ReportError {
1130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1131        match self {
1132            ReportError::FileError(msg) => write!(f, "File error: {}", msg),
1133            ReportError::SerializationError(msg) => write!(f, "Serialization error: {}", msg),
1134            ReportError::UnsupportedFormat(msg) => write!(f, "Unsupported format: {}", msg),
1135            ReportError::MissingData(msg) => write!(f, "Missing data: {}", msg),
1136            ReportError::GenerationError(msg) => write!(f, "Generation error: {}", msg),
1137        }
1138    }
1139}
1140
1141impl std::error::Error for ReportError {}
1142
1143#[cfg(test)]
1144mod tests {
1145    use super::*;
1146
1147    #[test]
1148    fn markdown_headings_become_closed_latex_sections() {
1149        let latex = markdown_headings_to_latex("# One\n## Two\n### Three\nbody\n");
1150        assert!(latex.contains("\\section{One}"), "{latex}");
1151        assert!(latex.contains("\\subsection{Two}"), "{latex}");
1152        assert!(latex.contains("\\subsubsection{Three}"), "{latex}");
1153        // The old chained-replace produced `\section{\section{ Three` and never
1154        // closed a single brace.
1155        assert_eq!(
1156            latex.matches('{').count(),
1157            latex.matches('}').count(),
1158            "every brace must be closed:\n{latex}"
1159        );
1160        assert!(latex.contains("body"));
1161    }
1162
1163    #[test]
1164    fn latex_specials_in_body_text_are_escaped() {
1165        let latex = markdown_headings_to_latex("100% of $x_1 & y#2");
1166        assert!(latex.contains("100\\%"), "{latex}");
1167        assert!(latex.contains("\\$x\\_1"), "{latex}");
1168        assert!(latex.contains("\\&"), "{latex}");
1169        assert!(latex.contains("y\\#2"), "{latex}");
1170        // A lone '#' inside a line must NOT be turned into a section command.
1171        assert!(!latex.contains("\\section"), "{latex}");
1172    }
1173
1174    #[test]
1175    fn unimplemented_exports_name_what_is_missing() {
1176        let generator =
1177            ReportGenerator::new(ReportConfig::default()).expect("generator construction");
1178        let report = Report {
1179            metadata: ReportMetadata {
1180                title: "t".to_string(),
1181                subtitle: None,
1182                author: "a".to_string(),
1183                organization: None,
1184                version: "1".to_string(),
1185                generation_time_ms: 0.0,
1186                additional_metadata: HashMap::new(),
1187            },
1188            sections: Vec::new(),
1189            visualizations: HashMap::new(),
1190            raw_data: HashMap::new(),
1191            generated_at: chrono::Utc::now(),
1192        };
1193        let pdf = generator.export_pdf(&report).expect_err("pdf must be refused");
1194        assert!(format!("{pdf:?}").contains("PDF writer"), "{pdf:?}");
1195        let xls = generator.export_excel(&report).expect_err("excel must be refused");
1196        assert!(format!("{xls:?}").contains("data_export"), "{xls:?}");
1197        let ppt = generator.export_powerpoint(&report).expect_err("pptx must be refused");
1198        assert!(format!("{ppt:?}").contains("OOXML"), "{ppt:?}");
1199    }
1200    use crate::DebugConfig;
1201
1202    #[test]
1203    fn test_report_config_default() {
1204        let config = ReportConfig::default();
1205        assert_eq!(config.title, "TrustformeRS Debug Report");
1206        assert_eq!(config.author, "TrustformeRS Debugger");
1207        assert!(matches!(config.format, ReportFormat::Html));
1208        assert!(matches!(
1209            config.report_type,
1210            ReportType::ComprehensiveReport
1211        ));
1212    }
1213
1214    #[test]
1215    fn test_report_generator_creation() {
1216        let config = ReportConfig::default();
1217        let generator = ReportGenerator::new(config).expect("format should be implemented");
1218        assert!(generator.debug_data.is_none());
1219        assert!(generator.profiling_data.is_none());
1220    }
1221
1222    /// Regression test: PDF used to be a silently-accepted `ReportConfig`
1223    /// value that only failed inside `export_report`, after a caller had
1224    /// already generated the full report. `ReportGenerator::new` must now
1225    /// reject it immediately with a clear message.
1226    #[test]
1227    fn test_new_rejects_pdf_format_immediately_instead_of_at_export_time() {
1228        let config = ReportConfig {
1229            format: ReportFormat::Pdf,
1230            ..Default::default()
1231        };
1232
1233        let err = ReportGenerator::new(config)
1234            .expect_err("PDF must be rejected at construction, not accepted and failed later");
1235        let message = err.to_string();
1236        assert!(
1237            message.contains("PDF"),
1238            "error should name the rejected format: {message}"
1239        );
1240    }
1241
1242    /// Companion: Excel and PowerPoint are the other two `ReportFormat`
1243    /// variants with no real writer behind them; both must be rejected the
1244    /// same way as PDF, not just PDF alone.
1245    #[test]
1246    fn test_new_rejects_excel_and_powerpoint_formats() {
1247        for format in [ReportFormat::Excel, ReportFormat::PowerPoint] {
1248            let config = ReportConfig {
1249                format,
1250                ..Default::default()
1251            };
1252            assert!(
1253                ReportGenerator::new(config).is_err(),
1254                "unimplemented export formats must be rejected at construction"
1255            );
1256        }
1257    }
1258
1259    /// Companion: formats that *do* have a real writer (see `export_html`,
1260    /// `export_markdown`, `export_json`, `export_jupyter`, `export_latex`)
1261    /// must still construct successfully -- the fix must not become an
1262    /// overly broad rejection of every format.
1263    #[test]
1264    fn test_new_accepts_every_implemented_format() {
1265        for format in [
1266            ReportFormat::Markdown,
1267            ReportFormat::Html,
1268            ReportFormat::Json,
1269            ReportFormat::Jupyter,
1270            ReportFormat::Latex,
1271        ] {
1272            let config = ReportConfig {
1273                format,
1274                ..Default::default()
1275            };
1276            assert!(
1277                ReportGenerator::new(config).is_ok(),
1278                "implemented export formats must not be rejected"
1279            );
1280        }
1281    }
1282
1283    #[test]
1284    fn test_report_generation() {
1285        let config = ReportConfig {
1286            title: "Test Report".to_string(),
1287            format: ReportFormat::Json,
1288            report_type: ReportType::DebugReport,
1289            ..Default::default()
1290        };
1291
1292        let generator = ReportGenerator::new(config).expect("format should be implemented");
1293        let report = generator.generate().expect("operation failed in test");
1294
1295        assert_eq!(report.metadata.title, "Test Report");
1296        assert!(!report.sections.is_empty());
1297    }
1298
1299    #[test]
1300    fn test_section_generation() {
1301        let config = ReportConfig::default();
1302        let generator = ReportGenerator::new(config).expect("format should be implemented");
1303
1304        let summary = generator.generate_summary_section().expect("operation failed in test");
1305        assert!(matches!(summary.section_type, ReportSection::Summary));
1306        assert_eq!(summary.title, "Executive Summary");
1307        assert!(!summary.content.is_empty());
1308    }
1309
1310    #[test]
1311    fn test_custom_report_type() {
1312        let config = ReportConfig {
1313            report_type: ReportType::CustomReport(vec![
1314                ReportSection::Summary,
1315                ReportSection::Performance,
1316            ]),
1317            ..Default::default()
1318        };
1319
1320        let generator = ReportGenerator::new(config).expect("format should be implemented");
1321        let report = generator.generate().expect("operation failed in test");
1322
1323        assert_eq!(report.sections.len(), 2);
1324        assert!(matches!(
1325            report.sections[0].section_type,
1326            ReportSection::Summary
1327        ));
1328        assert!(matches!(
1329            report.sections[1].section_type,
1330            ReportSection::Performance
1331        ));
1332    }
1333
1334    #[test]
1335    fn test_report_serialization() {
1336        let config = ReportConfig::default();
1337        let generator = ReportGenerator::new(config).expect("format should be implemented");
1338        let report = generator.generate().expect("operation failed in test");
1339
1340        let json = serde_json::to_string(&report).expect("JSON serialization failed");
1341        let deserialized: Report =
1342            serde_json::from_str(&json).expect("JSON deserialization failed");
1343
1344        assert_eq!(report.metadata.title, deserialized.metadata.title);
1345        assert_eq!(report.sections.len(), deserialized.sections.len());
1346    }
1347
1348    /// Regression test: the old `generate_architecture_section` printed the
1349    /// literal string `"N/A"` for every layer's parameter count
1350    /// unconditionally, regardless of whether any architecture data was
1351    /// ever provided. With real architecture data attached via
1352    /// `with_architecture_data`, a layer that has a matching entry there
1353    /// must show its real parameter count.
1354    #[tokio::test]
1355    async fn test_architecture_section_uses_real_parameter_counts_not_na() {
1356        use crate::architecture_analysis::{
1357            ArchitectureAnalysisConfig, ArchitectureAnalyzer, LayerInfo, LayerType,
1358        };
1359        use crate::gradient_debugger::debugger::{FlowAnalysis, LayerFlowAnalysis};
1360        use crate::gradient_debugger::GradientDebugger;
1361
1362        let debugger = GradientDebugger::new(DebugConfig::default());
1363        let mut gradient_report =
1364            debugger.generate_report().await.expect("gradient report should generate");
1365        let mut layer_analyses = HashMap::new();
1366        layer_analyses.insert(
1367            "encoder.layer0".to_string(),
1368            LayerFlowAnalysis {
1369                layer_name: "encoder.layer0".to_string(),
1370                is_vanishing: false,
1371                is_exploding: false,
1372                gradient_norm: 0.5,
1373                flow_consistency: 0.9,
1374            },
1375        );
1376        gradient_report.flow_analysis = FlowAnalysis { layer_analyses };
1377
1378        let mut analyzer = ArchitectureAnalyzer::new(ArchitectureAnalysisConfig::default());
1379        analyzer.register_layer(LayerInfo {
1380            id: "0".to_string(),
1381            name: "encoder.layer0".to_string(),
1382            layer_type: LayerType::Linear,
1383            input_shape: vec![768],
1384            output_shape: vec![768],
1385            parameters: 590_592,
1386            trainable_parameters: 590_592,
1387            memory_usage: 0,
1388            flops: 0,
1389            receptive_field: None,
1390        });
1391        let architecture_report =
1392            analyzer.analyze().await.expect("architecture analysis should succeed");
1393
1394        let generator = ReportGenerator::new(ReportConfig::default())
1395            .expect("Html is implemented")
1396            .with_debug_data(gradient_report)
1397            .with_architecture_data(architecture_report);
1398
1399        let section = generator
1400            .generate_architecture_section()
1401            .expect("architecture section generation should succeed");
1402
1403        assert!(
1404            section.content.contains("590592"),
1405            "must show the real parameter count from architecture data, not N/A: {}",
1406            section.content
1407        );
1408    }
1409
1410    /// Companion to the above: without `with_architecture_data`, the column
1411    /// must still honestly say `N/A` -- this is the absence path, distinct
1412    /// from the bug (a permanent, unconditional `N/A` even when real data
1413    /// was available).
1414    #[tokio::test]
1415    async fn test_architecture_section_reports_na_without_architecture_data() {
1416        use crate::gradient_debugger::debugger::{FlowAnalysis, LayerFlowAnalysis};
1417        use crate::gradient_debugger::GradientDebugger;
1418
1419        let debugger = GradientDebugger::new(DebugConfig::default());
1420        let mut gradient_report =
1421            debugger.generate_report().await.expect("gradient report should generate");
1422        let mut layer_analyses = HashMap::new();
1423        layer_analyses.insert(
1424            "encoder.layer0".to_string(),
1425            LayerFlowAnalysis {
1426                layer_name: "encoder.layer0".to_string(),
1427                is_vanishing: false,
1428                is_exploding: false,
1429                gradient_norm: 0.5,
1430                flow_consistency: 0.9,
1431            },
1432        );
1433        gradient_report.flow_analysis = FlowAnalysis { layer_analyses };
1434
1435        let generator = ReportGenerator::new(ReportConfig::default())
1436            .expect("Html is implemented")
1437            .with_debug_data(gradient_report);
1438        let section = generator
1439            .generate_architecture_section()
1440            .expect("architecture section generation should succeed");
1441
1442        assert!(section.content.contains("N/A"));
1443    }
1444
1445    /// Regression test: the old `generate_visualizations` always emitted the
1446    /// fixed points `[1,2,3]`/`[10,15,12]` for the performance chart
1447    /// whenever any profiling data was attached, regardless of its content.
1448    /// The chart must instead reflect the real `slowest_layers` list.
1449    #[test]
1450    fn test_visualizations_reflect_real_slowest_layers_not_fixed_points() {
1451        use crate::profiler::{MemoryEfficiencyAnalysis, ProfilerReport};
1452        use std::time::Duration;
1453
1454        let profiling_data = ProfilerReport {
1455            total_events: 2,
1456            total_runtime: Duration::from_millis(42),
1457            statistics: HashMap::new(),
1458            bottlenecks: Vec::new(),
1459            slowest_layers: vec![
1460                ("attention.0".to_string(), Duration::from_millis(30)),
1461                ("mlp.0".to_string(), Duration::from_millis(12)),
1462            ],
1463            memory_efficiency: MemoryEfficiencyAnalysis::default(),
1464            recommendations: Vec::new(),
1465        };
1466
1467        let generator = ReportGenerator::new(ReportConfig::default())
1468            .expect("Html is implemented")
1469            .with_profiling_data(profiling_data);
1470        let visualizations = generator
1471            .generate_visualizations()
1472            .expect("visualization generation should succeed");
1473
1474        let chart = visualizations
1475            .get("performance_chart")
1476            .expect("a performance chart should be produced from real slowest_layers data");
1477        assert_eq!(
1478            chart.y_values,
1479            vec![30.0, 12.0],
1480            "must reflect real layer durations in ms"
1481        );
1482        assert_ne!(
1483            chart.y_values,
1484            vec![10.0, 15.0, 12.0],
1485            "must not be the old fabricated placeholder points"
1486        );
1487        assert_eq!(
1488            chart.labels,
1489            vec!["attention.0".to_string(), "mlp.0".to_string()]
1490        );
1491    }
1492}