Skip to main content

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
215                .last()
216                .map(|b| b.location.as_str())
217                .unwrap_or("unknown")
218        );
219        id
220    }
221
222    /// Add a watch expression
223    pub fn add_watch(&mut self, name: String, expression: String) {
224        let watch = WatchExpression {
225            expression: expression.clone(),
226            current_value: None,
227            value_history: Vec::new(),
228            active: true,
229        };
230        self.watch_expressions.insert(name.clone(), watch);
231        println!("πŸ‘οΈ  Watch added: {name} -> {expression}");
232    }
233
234    /// Record a trace entry
235    pub fn record_trace(&mut self, entry: TraceEntry) {
236        self.execution_trace.push(entry);
237
238        // Check if we hit any breakpoints
239        let current_component = self
240            .execution_trace
241            .last()
242            .map(|t| t.component.clone())
243            .unwrap_or_default();
244        if let Some(breakpoint) = self.check_breakpoints(&current_component) {
245            self.pause_at_breakpoint(breakpoint);
246        }
247    }
248
249    /// Check if execution should pause at any breakpoints
250    fn check_breakpoints(&mut self, current_component: &str) -> Option<String> {
251        for breakpoint in &mut self.breakpoints {
252            if breakpoint.enabled && breakpoint.location == current_component {
253                breakpoint.hit_count += 1;
254                return Some(breakpoint.id.clone());
255            }
256        }
257        None
258    }
259
260    /// Pause execution at a breakpoint
261    fn pause_at_breakpoint(&mut self, breakpoint_id: String) {
262        let context = ExecutionContext {
263            current_component: "example_component".to_string(), // This would be filled with actual context
264            stage: "example_stage".to_string(),
265            variables: HashMap::new(),
266            input_shape: Some((100, 10)),
267            memory_usage_mb: 128.5,
268            execution_time_ms: 1500.0,
269        };
270
271        self.state = DebugState::Paused {
272            breakpoint_id: breakpoint_id.clone(),
273            context,
274        };
275        println!("⏸️  Paused at breakpoint: {breakpoint_id}");
276    }
277
278    /// Get debugging summary
279    pub fn get_debug_summary(&self) -> DebugSummary {
280        DebugSummary {
281            session_id: self.session_id.clone(),
282            total_trace_entries: self.execution_trace.len(),
283            active_breakpoints: self.breakpoints.iter().filter(|bp| bp.enabled).count(),
284            active_watches: self
285                .watch_expressions
286                .iter()
287                .filter(|(_, w)| w.active)
288                .count(),
289            current_state: format!("{:?}", self.state),
290            total_execution_time_ms: self.execution_trace.iter().map(|e| e.duration_ms).sum(),
291            peak_memory_usage_mb: self
292                .execution_trace
293                .iter()
294                .map(|e| e.memory_after_mb)
295                .fold(0.0, f64::max),
296        }
297    }
298}
299
300/// Summary of debugging session
301#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct DebugSummary {
303    pub session_id: String,
304    pub total_trace_entries: usize,
305    pub active_breakpoints: usize,
306    pub active_watches: usize,
307    pub current_state: String,
308    pub total_execution_time_ms: f64,
309    pub peak_memory_usage_mb: f64,
310}
311
312impl Default for PipelineDebugger {
313    fn default() -> Self {
314        Self::new()
315    }
316}
317
318/// Error message enhancer that provides developer-friendly error messages
319pub struct ErrorMessageEnhancer;
320
321impl ErrorMessageEnhancer {
322    /// Enhance a pipeline error with developer-friendly information
323    #[must_use]
324    pub fn enhance_error(error: PipelineError) -> DeveloperFriendlyError {
325        match &error {
326            PipelineError::ConfigurationError { message, .. } => {
327                Self::enhance_configuration_error(error.clone(), message)
328            }
329            PipelineError::DataCompatibilityError {
330                expected,
331                actual,
332                stage,
333                ..
334            } => Self::enhance_data_compatibility_error(error.clone(), expected, actual, stage),
335            PipelineError::StructureError {
336                error_type,
337                affected_components,
338                ..
339            } => Self::enhance_structure_error(error.clone(), error_type, affected_components),
340            PipelineError::PerformanceWarning {
341                warning_type,
342                impact_level,
343                ..
344            } => Self::enhance_performance_warning(error.clone(), warning_type, impact_level),
345            PipelineError::ResourceError {
346                resource_type,
347                limit,
348                current,
349                component,
350                ..
351            } => Self::enhance_resource_error(
352                error.clone(),
353                resource_type,
354                *limit,
355                *current,
356                component,
357            ),
358            PipelineError::TypeSafetyError {
359                violation_type,
360                expected_type,
361                actual_type,
362                ..
363            } => Self::enhance_type_safety_error(
364                error.clone(),
365                violation_type,
366                expected_type,
367                actual_type,
368            ),
369        }
370    }
371
372    fn enhance_configuration_error(error: PipelineError, message: &str) -> DeveloperFriendlyError {
373        DeveloperFriendlyError {
374            original_error: error,
375            explanation: format!(
376                "Configuration Error: {message}\n\n\
377                This error occurs when pipeline parameters are incorrectly configured. \
378                Common causes include invalid parameter values, missing required parameters, \
379                or incompatible parameter combinations."
380            ),
381            fix_suggestions: vec![
382                FixSuggestion {
383                    priority: SuggestionPriority::Critical,
384                    title: "Check parameter documentation".to_string(),
385                    description: "Review the documentation for valid parameter ranges and types".to_string(),
386                    code_snippet: Some("pipeline.builder().with_parameter(\"valid_value\").build()".to_string()),
387                    estimated_effort_minutes: 5,
388                },
389                FixSuggestion {
390                    priority: SuggestionPriority::High,
391                    title: "Use configuration validation".to_string(),
392                    description: "Enable configuration validation to catch errors early".to_string(),
393                    code_snippet: Some("pipeline.validate_config().expect(\"Valid configuration\")".to_string()),
394                    estimated_effort_minutes: 2,
395                },
396            ],
397            documentation_links: vec![
398                "https://docs.sklears.com/pipeline-configuration".to_string(),
399                "https://docs.sklears.com/parameter-validation".to_string(),
400            ],
401            code_examples: vec![
402                CodeExample {
403                    title: "Basic Pipeline Configuration".to_string(),
404                    description: "Example of properly configuring a pipeline".to_string(),
405                    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(),
406                    expected_output: "Successfully configured pipeline with 2 steps".to_string(),
407                },
408            ],
409        }
410    }
411
412    fn enhance_data_compatibility_error(
413        error: PipelineError,
414        expected: &crate::enhanced_errors::DataShape,
415        actual: &crate::enhanced_errors::DataShape,
416        stage: &str,
417    ) -> DeveloperFriendlyError {
418        DeveloperFriendlyError {
419            original_error: error,
420            explanation: format!(
421                "Data Compatibility Error at stage '{}'\n\n\
422                Expected: {} samples Γ— {} features ({})\n\
423                Actual: {} samples Γ— {} features ({})\n\n\
424                This error occurs when data shapes don't match between pipeline stages. \
425                This is often caused by incorrect data preprocessing or feature selection.",
426                stage,
427                expected.samples, expected.features, expected.data_type,
428                actual.samples, actual.features, actual.data_type
429            ),
430            fix_suggestions: vec![
431                FixSuggestion {
432                    priority: SuggestionPriority::Critical,
433                    title: "Check data preprocessing".to_string(),
434                    description: "Verify that preprocessing steps maintain expected data shapes".to_string(),
435                    code_snippet: Some("print(\"Data shape after preprocessing: {}\", X.shape)".to_string()),
436                    estimated_effort_minutes: 10,
437                },
438                FixSuggestion {
439                    priority: SuggestionPriority::High,
440                    title: "Add shape validation".to_string(),
441                    description: "Add explicit shape validation between pipeline stages".to_string(),
442                    code_snippet: Some("pipeline.add_validation_step(ShapeValidator::new(expected_shape))".to_string()),
443                    estimated_effort_minutes: 5,
444                },
445            ],
446            documentation_links: vec![
447                "https://docs.sklears.com/data-shapes".to_string(),
448                "https://docs.sklears.com/pipeline-validation".to_string(),
449            ],
450            code_examples: vec![
451                CodeExample {
452                    title: "Data Shape Debugging".to_string(),
453                    description: "How to debug data shape issues".to_string(),
454                    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(),
455                    expected_output: "Detailed shape information at each step".to_string(),
456                },
457            ],
458        }
459    }
460
461    fn enhance_structure_error(
462        error: PipelineError,
463        error_type: &crate::enhanced_errors::StructureErrorType,
464        affected_components: &[String],
465    ) -> DeveloperFriendlyError {
466        DeveloperFriendlyError {
467            original_error: error,
468            explanation: format!(
469                "Pipeline Structure Error: {:?}\n\
470                Affected components: {}\n\n\
471                This error indicates a problem with the pipeline's structure or component relationships.",
472                error_type,
473                affected_components.join(", ")
474            ),
475            fix_suggestions: vec![
476                FixSuggestion {
477                    priority: SuggestionPriority::Critical,
478                    title: "Review pipeline structure".to_string(),
479                    description: "Check the logical flow and dependencies between components".to_string(),
480                    code_snippet: Some("pipeline.visualize_structure()".to_string()),
481                    estimated_effort_minutes: 15,
482                },
483            ],
484            documentation_links: vec![
485                "https://docs.sklears.com/pipeline-structure".to_string(),
486            ],
487            code_examples: vec![],
488        }
489    }
490
491    fn enhance_performance_warning(
492        error: PipelineError,
493        warning_type: &crate::enhanced_errors::PerformanceWarningType,
494        impact_level: &crate::enhanced_errors::ImpactLevel,
495    ) -> DeveloperFriendlyError {
496        DeveloperFriendlyError {
497            original_error: error,
498            explanation: format!(
499                "Performance Warning: {warning_type:?} (Impact: {impact_level:?})\n\n\
500                This warning indicates potential performance issues that may affect execution."
501            ),
502            fix_suggestions: vec![FixSuggestion {
503                priority: SuggestionPriority::Medium,
504                title: "Profile pipeline performance".to_string(),
505                description: "Use the built-in profiler to identify bottlenecks".to_string(),
506                code_snippet: Some("pipeline.enable_profiling().run_with_profiling()".to_string()),
507                estimated_effort_minutes: 20,
508            }],
509            documentation_links: vec![
510                "https://docs.sklears.com/performance-optimization".to_string()
511            ],
512            code_examples: vec![],
513        }
514    }
515
516    fn enhance_resource_error(
517        error: PipelineError,
518        resource_type: &crate::enhanced_errors::ResourceType,
519        limit: f64,
520        current: f64,
521        component: &str,
522    ) -> DeveloperFriendlyError {
523        DeveloperFriendlyError {
524            original_error: error,
525            explanation: format!(
526                "Resource Constraint Error in component '{component}'\n\
527                Resource: {resource_type:?}\n\
528                Limit: {limit:.2}\n\
529                Current usage: {current:.2}\n\n\
530                The component is exceeding available resources."
531            ),
532            fix_suggestions: vec![FixSuggestion {
533                priority: SuggestionPriority::High,
534                title: "Optimize memory usage".to_string(),
535                description:
536                    "Consider using streaming or batch processing to reduce memory footprint"
537                        .to_string(),
538                code_snippet: Some("pipeline.set_batch_size(1000).enable_streaming()".to_string()),
539                estimated_effort_minutes: 30,
540            }],
541            documentation_links: vec!["https://docs.sklears.com/memory-optimization".to_string()],
542            code_examples: vec![],
543        }
544    }
545
546    fn enhance_type_safety_error(
547        error: PipelineError,
548        violation_type: &crate::enhanced_errors::TypeViolationType,
549        expected_type: &str,
550        actual_type: &str,
551    ) -> DeveloperFriendlyError {
552        DeveloperFriendlyError {
553            original_error: error,
554            explanation: format!(
555                "Type Safety Error: {violation_type:?}\n\
556                Expected type: {expected_type}\n\
557                Actual type: {actual_type}\n\n\
558                This error occurs when there's a type mismatch between pipeline components."
559            ),
560            fix_suggestions: vec![FixSuggestion {
561                priority: SuggestionPriority::Critical,
562                title: "Check type compatibility".to_string(),
563                description: "Ensure all pipeline components have compatible input/output types"
564                    .to_string(),
565                code_snippet: Some("pipeline.validate_type_safety()".to_string()),
566                estimated_effort_minutes: 10,
567            }],
568            documentation_links: vec!["https://docs.sklears.com/type-safety".to_string()],
569            code_examples: vec![],
570        }
571    }
572}
573
574/// Display implementations for better developer experience
575impl fmt::Display for DeveloperFriendlyError {
576    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
577        writeln!(f, "🚨 {}", self.explanation)?;
578
579        if !self.fix_suggestions.is_empty() {
580            writeln!(f, "\nπŸ’‘ Suggested fixes:")?;
581            for (i, suggestion) in self.fix_suggestions.iter().enumerate() {
582                writeln!(
583                    f,
584                    "  {}. [{}] {} (β‰ˆ{}min)",
585                    i + 1,
586                    match suggestion.priority {
587                        SuggestionPriority::Critical => "πŸ”΄ CRITICAL",
588                        SuggestionPriority::High => "🟑 HIGH",
589                        SuggestionPriority::Medium => "πŸ”΅ MEDIUM",
590                        SuggestionPriority::Low => "βšͺ LOW",
591                    },
592                    suggestion.title,
593                    suggestion.estimated_effort_minutes
594                )?;
595                writeln!(f, "     {}", suggestion.description)?;
596                if let Some(code) = &suggestion.code_snippet {
597                    writeln!(f, "     Example: {code}")?;
598                }
599            }
600        }
601
602        if !self.documentation_links.is_empty() {
603            writeln!(f, "\nπŸ“š Helpful documentation:")?;
604            for link in &self.documentation_links {
605                writeln!(f, "  β€’ {link}")?;
606            }
607        }
608
609        Ok(())
610    }
611}
612
613impl fmt::Display for SuggestionPriority {
614    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
615        match self {
616            SuggestionPriority::Critical => write!(f, "Critical"),
617            SuggestionPriority::High => write!(f, "High"),
618            SuggestionPriority::Medium => write!(f, "Medium"),
619            SuggestionPriority::Low => write!(f, "Low"),
620        }
621    }
622}
623
624#[allow(non_snake_case)]
625#[cfg(test)]
626mod tests {
627    use super::*;
628    use crate::enhanced_errors::*;
629
630    #[test]
631    fn test_debugger_creation() {
632        let debugger = PipelineDebugger::new();
633        assert!(matches!(debugger.state, DebugState::Idle));
634        assert_eq!(debugger.breakpoints.len(), 0);
635        assert_eq!(debugger.execution_trace.len(), 0);
636    }
637
638    #[test]
639    fn test_add_breakpoint() {
640        let mut debugger = PipelineDebugger::new();
641        let bp_id = debugger.add_breakpoint("test_component".to_string(), None);
642
643        assert_eq!(debugger.breakpoints.len(), 1);
644        assert_eq!(debugger.breakpoints[0].id, bp_id);
645        assert_eq!(debugger.breakpoints[0].location, "test_component");
646        assert!(debugger.breakpoints[0].enabled);
647    }
648
649    #[test]
650    fn test_add_watch() {
651        let mut debugger = PipelineDebugger::new();
652        debugger.add_watch("test_var".to_string(), "x.shape".to_string());
653
654        assert_eq!(debugger.watch_expressions.len(), 1);
655        assert!(debugger.watch_expressions.contains_key("test_var"));
656        assert_eq!(debugger.watch_expressions["test_var"].expression, "x.shape");
657    }
658
659    #[test]
660    fn test_error_enhancement() {
661        let error = PipelineError::ConfigurationError {
662            message: "Invalid parameter value".to_string(),
663            suggestions: vec!["Check documentation".to_string()],
664            context: ErrorContext {
665                pipeline_stage: "test".to_string(),
666                component_name: "test_component".to_string(),
667                input_shape: Some((100, 10)),
668                parameters: std::collections::HashMap::new(),
669                stack_trace: vec![],
670            },
671        };
672
673        let enhanced = ErrorMessageEnhancer::enhance_error(error);
674        assert!(enhanced.explanation.contains("Configuration Error"));
675        assert!(!enhanced.fix_suggestions.is_empty());
676        assert!(enhanced.fix_suggestions[0].priority as u8 <= SuggestionPriority::Critical as u8);
677    }
678
679    #[test]
680    fn test_debug_summary() {
681        let debugger = PipelineDebugger::new();
682        let summary = debugger.get_debug_summary();
683
684        assert_eq!(summary.total_trace_entries, 0);
685        assert_eq!(summary.active_breakpoints, 0);
686        assert_eq!(summary.active_watches, 0);
687        assert_eq!(summary.total_execution_time_ms, 0.0);
688    }
689}