sklears_compose/
developer_experience.rs

1//! Developer Experience Enhancements
2//!
3//! This module provides improved developer experience through better error messages,
4//! debugging utilities, and pipeline inspection tools. It focuses on making
5//! sklearn-compose more ergonomic and easier to debug.
6
7use crate::enhanced_errors::PipelineError;
8use serde::{Deserialize, Serialize};
9use sklears_core::error::Result as SklResult;
10use std::collections::HashMap;
11use std::fmt;
12
13/// Enhanced error messages with actionable suggestions and context
14#[derive(Debug, Clone)]
15pub struct DeveloperFriendlyError {
16    /// Original error
17    pub original_error: PipelineError,
18    /// Detailed explanation for developers
19    pub explanation: String,
20    /// Step-by-step suggestions to fix the issue
21    pub fix_suggestions: Vec<FixSuggestion>,
22    /// Related documentation links
23    pub documentation_links: Vec<String>,
24    /// Code examples that might help
25    pub code_examples: Vec<CodeExample>,
26}
27
28/// A specific suggestion to fix an issue
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct FixSuggestion {
31    /// Priority level of this suggestion
32    pub priority: SuggestionPriority,
33    /// Short description of the fix
34    pub title: String,
35    /// Detailed explanation of how to apply the fix
36    pub description: String,
37    /// Code snippet demonstrating the fix
38    pub code_snippet: Option<String>,
39    /// Estimated effort to implement (in minutes)
40    pub estimated_effort_minutes: u32,
41}
42
43/// Priority levels for fix suggestions
44#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
45pub enum SuggestionPriority {
46    /// Must fix - will cause failures
47    Critical,
48    /// Should fix - may cause issues
49    High,
50    /// Good to fix - improves performance or reliability
51    Medium,
52    /// Nice to have - cosmetic improvements
53    Low,
54}
55
56/// Code example to help with debugging
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct CodeExample {
59    pub title: String,
60    pub description: String,
61    pub code: String,
62    pub expected_output: String,
63}
64
65/// Debug utilities for pipeline inspection
66#[derive(Debug)]
67pub struct PipelineDebugger {
68    /// Debug session ID
69    pub session_id: String,
70    /// Current debug state
71    pub state: DebugState,
72    /// Breakpoints set by the developer
73    pub breakpoints: Vec<Breakpoint>,
74    /// Execution trace
75    pub execution_trace: Vec<TraceEntry>,
76    /// Watch expressions
77    pub watch_expressions: HashMap<String, WatchExpression>,
78}
79
80/// Current debugging state
81#[derive(Debug, Clone)]
82pub enum DebugState {
83    /// Not debugging
84    Idle,
85    /// Running with debug enabled
86    Running,
87    /// Paused at a breakpoint
88    Paused {
89        breakpoint_id: String,
90        context: ExecutionContext,
91    },
92    /// Stepping through execution
93    Stepping { step_type: StepType },
94    /// Error occurred during debugging
95    Error { error: String },
96}
97
98/// Types of stepping through execution
99#[derive(Debug, Clone)]
100pub enum StepType {
101    /// Step to next instruction
102    StepNext,
103    /// Step into function/component
104    StepInto,
105    /// Step out of current function/component
106    StepOut,
107    /// Continue until next breakpoint
108    Continue,
109}
110
111/// A debugging breakpoint
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct Breakpoint {
114    /// Unique ID for this breakpoint
115    pub id: String,
116    /// Component or stage where breakpoint is set
117    pub location: String,
118    /// Condition that must be met to trigger breakpoint
119    pub condition: Option<String>,
120    /// Whether this breakpoint is currently enabled
121    pub enabled: bool,
122    /// Number of times this breakpoint has been hit
123    pub hit_count: u32,
124}
125
126/// Execution context when paused at breakpoint
127#[derive(Debug, Clone)]
128pub struct ExecutionContext {
129    /// Current component being executed
130    pub current_component: String,
131    /// Pipeline stage
132    pub stage: String,
133    /// Local variables and their values
134    pub variables: HashMap<String, String>,
135    /// Input data shape at this point
136    pub input_shape: Option<(usize, usize)>,
137    /// Memory usage at this point
138    pub memory_usage_mb: f64,
139    /// Execution time so far (milliseconds)
140    pub execution_time_ms: f64,
141}
142
143/// Entry in the execution trace
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct TraceEntry {
146    /// Timestamp when this entry was recorded
147    pub timestamp: u64,
148    /// Component that was executed
149    pub component: String,
150    /// Operation that was performed
151    pub operation: String,
152    /// Duration of the operation (milliseconds)
153    pub duration_ms: f64,
154    /// Input data shape
155    pub input_shape: Option<(usize, usize)>,
156    /// Output data shape
157    pub output_shape: Option<(usize, usize)>,
158    /// Memory usage before operation
159    pub memory_before_mb: f64,
160    /// Memory usage after operation
161    pub memory_after_mb: f64,
162    /// Any warnings or notes
163    pub notes: Vec<String>,
164}
165
166/// Watch expression for monitoring values during debugging
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct WatchExpression {
169    /// Expression to evaluate
170    pub expression: String,
171    /// Current value of the expression
172    pub current_value: Option<String>,
173    /// History of values
174    pub value_history: Vec<(u64, String)>, // timestamp, value
175    /// Whether this watch is currently active
176    pub active: bool,
177}
178
179impl PipelineDebugger {
180    /// Create a new debugging session
181    #[must_use]
182    pub fn new() -> Self {
183        Self {
184            session_id: format!("debug-{}", chrono::Utc::now().timestamp()),
185            state: DebugState::Idle,
186            breakpoints: Vec::new(),
187            execution_trace: Vec::new(),
188            watch_expressions: HashMap::new(),
189        }
190    }
191
192    /// Start a debugging session
193    pub fn start_debug_session(&mut self) -> SklResult<()> {
194        self.state = DebugState::Running;
195        self.execution_trace.clear();
196        println!("πŸ› Debug session started: {}", self.session_id);
197        Ok(())
198    }
199
200    /// Add a breakpoint
201    pub fn add_breakpoint(&mut self, location: String, condition: Option<String>) -> String {
202        let id = format!("bp-{}", self.breakpoints.len());
203        let breakpoint = Breakpoint {
204            id: id.clone(),
205            location,
206            condition,
207            enabled: true,
208            hit_count: 0,
209        };
210        self.breakpoints.push(breakpoint);
211        println!(
212            "πŸ”΄ Breakpoint added: {} at {}",
213            id,
214            self.breakpoints.last().unwrap().location
215        );
216        id
217    }
218
219    /// Add a watch expression
220    pub fn add_watch(&mut self, name: String, expression: String) {
221        let watch = WatchExpression {
222            expression: expression.clone(),
223            current_value: None,
224            value_history: Vec::new(),
225            active: true,
226        };
227        self.watch_expressions.insert(name.clone(), watch);
228        println!("πŸ‘οΈ  Watch added: {name} -> {expression}");
229    }
230
231    /// Record a trace entry
232    pub fn record_trace(&mut self, entry: TraceEntry) {
233        self.execution_trace.push(entry);
234
235        // Check if we hit any breakpoints
236        let current_component = self.execution_trace.last().unwrap().component.clone();
237        if let Some(breakpoint) = self.check_breakpoints(&current_component) {
238            self.pause_at_breakpoint(breakpoint);
239        }
240    }
241
242    /// Check if execution should pause at any breakpoints
243    fn check_breakpoints(&mut self, current_component: &str) -> Option<String> {
244        for breakpoint in &mut self.breakpoints {
245            if breakpoint.enabled && breakpoint.location == current_component {
246                breakpoint.hit_count += 1;
247                return Some(breakpoint.id.clone());
248            }
249        }
250        None
251    }
252
253    /// Pause execution at a breakpoint
254    fn pause_at_breakpoint(&mut self, breakpoint_id: String) {
255        let context = ExecutionContext {
256            current_component: "example_component".to_string(), // This would be filled with actual context
257            stage: "example_stage".to_string(),
258            variables: HashMap::new(),
259            input_shape: Some((100, 10)),
260            memory_usage_mb: 128.5,
261            execution_time_ms: 1500.0,
262        };
263
264        self.state = DebugState::Paused {
265            breakpoint_id: breakpoint_id.clone(),
266            context,
267        };
268        println!("⏸️  Paused at breakpoint: {breakpoint_id}");
269    }
270
271    /// Get debugging summary
272    pub fn get_debug_summary(&self) -> DebugSummary {
273        DebugSummary {
274            session_id: self.session_id.clone(),
275            total_trace_entries: self.execution_trace.len(),
276            active_breakpoints: self.breakpoints.iter().filter(|bp| bp.enabled).count(),
277            active_watches: self
278                .watch_expressions
279                .iter()
280                .filter(|(_, w)| w.active)
281                .count(),
282            current_state: format!("{:?}", self.state),
283            total_execution_time_ms: self.execution_trace.iter().map(|e| e.duration_ms).sum(),
284            peak_memory_usage_mb: self
285                .execution_trace
286                .iter()
287                .map(|e| e.memory_after_mb)
288                .fold(0.0, f64::max),
289        }
290    }
291}
292
293/// Summary of debugging session
294#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct DebugSummary {
296    pub session_id: String,
297    pub total_trace_entries: usize,
298    pub active_breakpoints: usize,
299    pub active_watches: usize,
300    pub current_state: String,
301    pub total_execution_time_ms: f64,
302    pub peak_memory_usage_mb: f64,
303}
304
305impl Default for PipelineDebugger {
306    fn default() -> Self {
307        Self::new()
308    }
309}
310
311/// Error message enhancer that provides developer-friendly error messages
312pub struct ErrorMessageEnhancer;
313
314impl ErrorMessageEnhancer {
315    /// Enhance a pipeline error with developer-friendly information
316    #[must_use]
317    pub fn enhance_error(error: PipelineError) -> DeveloperFriendlyError {
318        match &error {
319            PipelineError::ConfigurationError { message, .. } => {
320                Self::enhance_configuration_error(error.clone(), message)
321            }
322            PipelineError::DataCompatibilityError {
323                expected,
324                actual,
325                stage,
326                ..
327            } => Self::enhance_data_compatibility_error(error.clone(), expected, actual, stage),
328            PipelineError::StructureError {
329                error_type,
330                affected_components,
331                ..
332            } => Self::enhance_structure_error(error.clone(), error_type, affected_components),
333            PipelineError::PerformanceWarning {
334                warning_type,
335                impact_level,
336                ..
337            } => Self::enhance_performance_warning(error.clone(), warning_type, impact_level),
338            PipelineError::ResourceError {
339                resource_type,
340                limit,
341                current,
342                component,
343                ..
344            } => Self::enhance_resource_error(
345                error.clone(),
346                resource_type,
347                *limit,
348                *current,
349                component,
350            ),
351            PipelineError::TypeSafetyError {
352                violation_type,
353                expected_type,
354                actual_type,
355                ..
356            } => Self::enhance_type_safety_error(
357                error.clone(),
358                violation_type,
359                expected_type,
360                actual_type,
361            ),
362        }
363    }
364
365    fn enhance_configuration_error(error: PipelineError, message: &str) -> DeveloperFriendlyError {
366        DeveloperFriendlyError {
367            original_error: error,
368            explanation: format!(
369                "Configuration Error: {message}\n\n\
370                This error occurs when pipeline parameters are incorrectly configured. \
371                Common causes include invalid parameter values, missing required parameters, \
372                or incompatible parameter combinations."
373            ),
374            fix_suggestions: vec![
375                FixSuggestion {
376                    priority: SuggestionPriority::Critical,
377                    title: "Check parameter documentation".to_string(),
378                    description: "Review the documentation for valid parameter ranges and types".to_string(),
379                    code_snippet: Some("pipeline.builder().with_parameter(\"valid_value\").build()".to_string()),
380                    estimated_effort_minutes: 5,
381                },
382                FixSuggestion {
383                    priority: SuggestionPriority::High,
384                    title: "Use configuration validation".to_string(),
385                    description: "Enable configuration validation to catch errors early".to_string(),
386                    code_snippet: Some("pipeline.validate_config().expect(\"Valid configuration\")".to_string()),
387                    estimated_effort_minutes: 2,
388                },
389            ],
390            documentation_links: vec![
391                "https://docs.sklears.com/pipeline-configuration".to_string(),
392                "https://docs.sklears.com/parameter-validation".to_string(),
393            ],
394            code_examples: vec![
395                CodeExample {
396                    title: "Basic Pipeline Configuration".to_string(),
397                    description: "Example of properly configuring a pipeline".to_string(),
398                    code: "use sklears_compose::Pipeline;\n\nlet pipeline = Pipeline::builder()\n    .add_step(\"scaler\", StandardScaler::new())\n    .add_step(\"model\", LinearRegression::new())\n    .build()?;".to_string(),
399                    expected_output: "Successfully configured pipeline with 2 steps".to_string(),
400                },
401            ],
402        }
403    }
404
405    fn enhance_data_compatibility_error(
406        error: PipelineError,
407        expected: &crate::enhanced_errors::DataShape,
408        actual: &crate::enhanced_errors::DataShape,
409        stage: &str,
410    ) -> DeveloperFriendlyError {
411        DeveloperFriendlyError {
412            original_error: error,
413            explanation: format!(
414                "Data Compatibility Error at stage '{}'\n\n\
415                Expected: {} samples Γ— {} features ({})\n\
416                Actual: {} samples Γ— {} features ({})\n\n\
417                This error occurs when data shapes don't match between pipeline stages. \
418                This is often caused by incorrect data preprocessing or feature selection.",
419                stage,
420                expected.samples, expected.features, expected.data_type,
421                actual.samples, actual.features, actual.data_type
422            ),
423            fix_suggestions: vec![
424                FixSuggestion {
425                    priority: SuggestionPriority::Critical,
426                    title: "Check data preprocessing".to_string(),
427                    description: "Verify that preprocessing steps maintain expected data shapes".to_string(),
428                    code_snippet: Some("print(\"Data shape after preprocessing: {}\", X.shape)".to_string()),
429                    estimated_effort_minutes: 10,
430                },
431                FixSuggestion {
432                    priority: SuggestionPriority::High,
433                    title: "Add shape validation".to_string(),
434                    description: "Add explicit shape validation between pipeline stages".to_string(),
435                    code_snippet: Some("pipeline.add_validation_step(ShapeValidator::new(expected_shape))".to_string()),
436                    estimated_effort_minutes: 5,
437                },
438            ],
439            documentation_links: vec![
440                "https://docs.sklears.com/data-shapes".to_string(),
441                "https://docs.sklears.com/pipeline-validation".to_string(),
442            ],
443            code_examples: vec![
444                CodeExample {
445                    title: "Data Shape Debugging".to_string(),
446                    description: "How to debug data shape issues".to_string(),
447                    code: "// Check data shapes at each step\nlet mut pipeline = Pipeline::new();\npipeline.debug_mode(true);\nlet result = pipeline.fit_transform(&X, &y)?;\nprintln!(\"Final shape: {:?}\", result.shape());".to_string(),
448                    expected_output: "Detailed shape information at each step".to_string(),
449                },
450            ],
451        }
452    }
453
454    fn enhance_structure_error(
455        error: PipelineError,
456        error_type: &crate::enhanced_errors::StructureErrorType,
457        affected_components: &[String],
458    ) -> DeveloperFriendlyError {
459        DeveloperFriendlyError {
460            original_error: error,
461            explanation: format!(
462                "Pipeline Structure Error: {:?}\n\
463                Affected components: {}\n\n\
464                This error indicates a problem with the pipeline's structure or component relationships.",
465                error_type,
466                affected_components.join(", ")
467            ),
468            fix_suggestions: vec![
469                FixSuggestion {
470                    priority: SuggestionPriority::Critical,
471                    title: "Review pipeline structure".to_string(),
472                    description: "Check the logical flow and dependencies between components".to_string(),
473                    code_snippet: Some("pipeline.visualize_structure()".to_string()),
474                    estimated_effort_minutes: 15,
475                },
476            ],
477            documentation_links: vec![
478                "https://docs.sklears.com/pipeline-structure".to_string(),
479            ],
480            code_examples: vec![],
481        }
482    }
483
484    fn enhance_performance_warning(
485        error: PipelineError,
486        warning_type: &crate::enhanced_errors::PerformanceWarningType,
487        impact_level: &crate::enhanced_errors::ImpactLevel,
488    ) -> DeveloperFriendlyError {
489        DeveloperFriendlyError {
490            original_error: error,
491            explanation: format!(
492                "Performance Warning: {warning_type:?} (Impact: {impact_level:?})\n\n\
493                This warning indicates potential performance issues that may affect execution."
494            ),
495            fix_suggestions: vec![FixSuggestion {
496                priority: SuggestionPriority::Medium,
497                title: "Profile pipeline performance".to_string(),
498                description: "Use the built-in profiler to identify bottlenecks".to_string(),
499                code_snippet: Some("pipeline.enable_profiling().run_with_profiling()".to_string()),
500                estimated_effort_minutes: 20,
501            }],
502            documentation_links: vec![
503                "https://docs.sklears.com/performance-optimization".to_string()
504            ],
505            code_examples: vec![],
506        }
507    }
508
509    fn enhance_resource_error(
510        error: PipelineError,
511        resource_type: &crate::enhanced_errors::ResourceType,
512        limit: f64,
513        current: f64,
514        component: &str,
515    ) -> DeveloperFriendlyError {
516        DeveloperFriendlyError {
517            original_error: error,
518            explanation: format!(
519                "Resource Constraint Error in component '{component}'\n\
520                Resource: {resource_type:?}\n\
521                Limit: {limit:.2}\n\
522                Current usage: {current:.2}\n\n\
523                The component is exceeding available resources."
524            ),
525            fix_suggestions: vec![FixSuggestion {
526                priority: SuggestionPriority::High,
527                title: "Optimize memory usage".to_string(),
528                description:
529                    "Consider using streaming or batch processing to reduce memory footprint"
530                        .to_string(),
531                code_snippet: Some("pipeline.set_batch_size(1000).enable_streaming()".to_string()),
532                estimated_effort_minutes: 30,
533            }],
534            documentation_links: vec!["https://docs.sklears.com/memory-optimization".to_string()],
535            code_examples: vec![],
536        }
537    }
538
539    fn enhance_type_safety_error(
540        error: PipelineError,
541        violation_type: &crate::enhanced_errors::TypeViolationType,
542        expected_type: &str,
543        actual_type: &str,
544    ) -> DeveloperFriendlyError {
545        DeveloperFriendlyError {
546            original_error: error,
547            explanation: format!(
548                "Type Safety Error: {violation_type:?}\n\
549                Expected type: {expected_type}\n\
550                Actual type: {actual_type}\n\n\
551                This error occurs when there's a type mismatch between pipeline components."
552            ),
553            fix_suggestions: vec![FixSuggestion {
554                priority: SuggestionPriority::Critical,
555                title: "Check type compatibility".to_string(),
556                description: "Ensure all pipeline components have compatible input/output types"
557                    .to_string(),
558                code_snippet: Some("pipeline.validate_type_safety()".to_string()),
559                estimated_effort_minutes: 10,
560            }],
561            documentation_links: vec!["https://docs.sklears.com/type-safety".to_string()],
562            code_examples: vec![],
563        }
564    }
565}
566
567/// Display implementations for better developer experience
568impl fmt::Display for DeveloperFriendlyError {
569    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
570        writeln!(f, "🚨 {}", self.explanation)?;
571
572        if !self.fix_suggestions.is_empty() {
573            writeln!(f, "\nπŸ’‘ Suggested fixes:")?;
574            for (i, suggestion) in self.fix_suggestions.iter().enumerate() {
575                writeln!(
576                    f,
577                    "  {}. [{}] {} (β‰ˆ{}min)",
578                    i + 1,
579                    match suggestion.priority {
580                        SuggestionPriority::Critical => "πŸ”΄ CRITICAL",
581                        SuggestionPriority::High => "🟑 HIGH",
582                        SuggestionPriority::Medium => "πŸ”΅ MEDIUM",
583                        SuggestionPriority::Low => "βšͺ LOW",
584                    },
585                    suggestion.title,
586                    suggestion.estimated_effort_minutes
587                )?;
588                writeln!(f, "     {}", suggestion.description)?;
589                if let Some(code) = &suggestion.code_snippet {
590                    writeln!(f, "     Example: {code}")?;
591                }
592            }
593        }
594
595        if !self.documentation_links.is_empty() {
596            writeln!(f, "\nπŸ“š Helpful documentation:")?;
597            for link in &self.documentation_links {
598                writeln!(f, "  β€’ {link}")?;
599            }
600        }
601
602        Ok(())
603    }
604}
605
606impl fmt::Display for SuggestionPriority {
607    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
608        match self {
609            SuggestionPriority::Critical => write!(f, "Critical"),
610            SuggestionPriority::High => write!(f, "High"),
611            SuggestionPriority::Medium => write!(f, "Medium"),
612            SuggestionPriority::Low => write!(f, "Low"),
613        }
614    }
615}
616
617#[allow(non_snake_case)]
618#[cfg(test)]
619mod tests {
620    use super::*;
621    use crate::enhanced_errors::*;
622
623    #[test]
624    fn test_debugger_creation() {
625        let debugger = PipelineDebugger::new();
626        assert!(matches!(debugger.state, DebugState::Idle));
627        assert_eq!(debugger.breakpoints.len(), 0);
628        assert_eq!(debugger.execution_trace.len(), 0);
629    }
630
631    #[test]
632    fn test_add_breakpoint() {
633        let mut debugger = PipelineDebugger::new();
634        let bp_id = debugger.add_breakpoint("test_component".to_string(), None);
635
636        assert_eq!(debugger.breakpoints.len(), 1);
637        assert_eq!(debugger.breakpoints[0].id, bp_id);
638        assert_eq!(debugger.breakpoints[0].location, "test_component");
639        assert!(debugger.breakpoints[0].enabled);
640    }
641
642    #[test]
643    fn test_add_watch() {
644        let mut debugger = PipelineDebugger::new();
645        debugger.add_watch("test_var".to_string(), "x.shape".to_string());
646
647        assert_eq!(debugger.watch_expressions.len(), 1);
648        assert!(debugger.watch_expressions.contains_key("test_var"));
649        assert_eq!(debugger.watch_expressions["test_var"].expression, "x.shape");
650    }
651
652    #[test]
653    fn test_error_enhancement() {
654        let error = PipelineError::ConfigurationError {
655            message: "Invalid parameter value".to_string(),
656            suggestions: vec!["Check documentation".to_string()],
657            context: ErrorContext {
658                pipeline_stage: "test".to_string(),
659                component_name: "test_component".to_string(),
660                input_shape: Some((100, 10)),
661                parameters: std::collections::HashMap::new(),
662                stack_trace: vec![],
663            },
664        };
665
666        let enhanced = ErrorMessageEnhancer::enhance_error(error);
667        assert!(enhanced.explanation.contains("Configuration Error"));
668        assert!(!enhanced.fix_suggestions.is_empty());
669        assert!(enhanced.fix_suggestions[0].priority as u8 <= SuggestionPriority::Critical as u8);
670    }
671
672    #[test]
673    fn test_debug_summary() {
674        let debugger = PipelineDebugger::new();
675        let summary = debugger.get_debug_summary();
676
677        assert_eq!(summary.total_trace_entries, 0);
678        assert_eq!(summary.active_breakpoints, 0);
679        assert_eq!(summary.active_watches, 0);
680        assert_eq!(summary.total_execution_time_ms, 0.0);
681    }
682}