Skip to main content

batuta/experiment/
tree.rs

1//! Experiment Tracking Frameworks Tree Visualization
2//!
3//! Displays comparison tree of Python experiment tracking frameworks
4//! (MLflow, Weights & Biases, Neptune, etc.) and their PAIML Rust replacements.
5//!
6//! Core principle: Python experiment tracking is replaced by sovereign Rust alternatives.
7//! No Python runtime dependencies permitted in production.
8
9use serde::{Deserialize, Serialize};
10
11/// Experiment tracking framework being compared
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub enum ExperimentFramework {
14    /// MLflow - Open source experiment tracking
15    MLflow,
16    /// Weights & Biases - Commercial experiment tracking
17    WandB,
18    /// Neptune.ai - Commercial ML metadata store
19    Neptune,
20    /// Comet ML - Commercial experiment tracking
21    CometML,
22    /// Sacred - Academic experiment tracking
23    Sacred,
24    /// DVC - Data Version Control with experiment tracking
25    Dvc,
26}
27
28impl ExperimentFramework {
29    /// Get the display name
30    pub fn name(&self) -> &'static str {
31        match self {
32            ExperimentFramework::MLflow => "MLflow",
33            ExperimentFramework::WandB => "Weights & Biases",
34            ExperimentFramework::Neptune => "Neptune.ai",
35            ExperimentFramework::CometML => "Comet ML",
36            ExperimentFramework::Sacred => "Sacred",
37            ExperimentFramework::Dvc => "DVC",
38        }
39    }
40
41    /// Get the PAIML replacement
42    pub fn replacement(&self) -> &'static str {
43        match self {
44            ExperimentFramework::MLflow => "Entrenar + Batuta",
45            ExperimentFramework::WandB => "Entrenar + Trueno-Viz",
46            ExperimentFramework::Neptune => "Entrenar",
47            ExperimentFramework::CometML => "Entrenar + Presentar",
48            ExperimentFramework::Sacred => "Entrenar",
49            ExperimentFramework::Dvc => "Batuta + Trueno-DB",
50        }
51    }
52
53    /// Get all frameworks
54    pub fn all() -> Vec<ExperimentFramework> {
55        vec![
56            ExperimentFramework::MLflow,
57            ExperimentFramework::WandB,
58            ExperimentFramework::Neptune,
59            ExperimentFramework::CometML,
60            ExperimentFramework::Sacred,
61            ExperimentFramework::Dvc,
62        ]
63    }
64}
65
66/// Integration type - all are replacements (Python eliminated)
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
68pub enum IntegrationType {
69    /// PAIML component fully replaces Python equivalent
70    Replaces,
71}
72
73impl IntegrationType {
74    /// Get the display code
75    pub fn code(&self) -> &'static str {
76        match self {
77            IntegrationType::Replaces => "REP",
78        }
79    }
80}
81
82/// A component within a framework
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct FrameworkComponent {
85    /// Component name
86    pub name: String,
87    /// Description
88    pub description: String,
89    /// PAIML replacement
90    pub replacement: String,
91    /// Sub-components
92    pub sub_components: Vec<String>,
93}
94
95impl FrameworkComponent {
96    fn new(name: &str, description: &str, replacement: &str) -> Self {
97        Self {
98            name: name.to_string(),
99            description: description.to_string(),
100            replacement: replacement.to_string(),
101            sub_components: Vec::new(),
102        }
103    }
104
105    fn with_subs(name: &str, description: &str, replacement: &str, subs: Vec<&str>) -> Self {
106        Self {
107            name: name.to_string(),
108            description: description.to_string(),
109            replacement: replacement.to_string(),
110            sub_components: subs.into_iter().map(String::from).collect(),
111        }
112    }
113}
114
115/// A category of components
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct FrameworkCategory {
118    /// Category name
119    pub name: String,
120    /// Components in this category
121    pub components: Vec<FrameworkComponent>,
122}
123
124/// Integration mapping between Python and Rust
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct IntegrationMapping {
127    /// PAIML component
128    pub paiml_component: String,
129    /// Python component being replaced
130    pub python_component: String,
131    /// Integration type
132    pub integration_type: IntegrationType,
133    /// Category
134    pub category: String,
135}
136
137impl IntegrationMapping {
138    fn rep(paiml: &str, python: &str, category: &str) -> Self {
139        Self {
140            paiml_component: paiml.to_string(),
141            python_component: python.to_string(),
142            integration_type: IntegrationType::Replaces,
143            category: category.to_string(),
144        }
145    }
146}
147
148/// Experiment tracking tree structure
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct ExperimentTree {
151    /// Framework name
152    pub framework: String,
153    /// PAIML replacement
154    pub replacement: String,
155    /// Categories
156    pub categories: Vec<FrameworkCategory>,
157}
158
159/// Build MLflow tree
160pub fn build_mlflow_tree() -> ExperimentTree {
161    ExperimentTree {
162        framework: "MLflow".to_string(),
163        replacement: "Entrenar + Batuta".to_string(),
164        categories: vec![
165            FrameworkCategory {
166                name: "Experiment Tracking".to_string(),
167                components: vec![
168                    FrameworkComponent::with_subs(
169                        "mlflow.start_run()",
170                        "Run lifecycle management",
171                        "Entrenar::ExperimentRun::new()",
172                        vec!["log_param", "log_metric", "log_artifact"],
173                    ),
174                    FrameworkComponent::new(
175                        "mlflow.log_params()",
176                        "Hyperparameter logging",
177                        "ExperimentRun::log_param()",
178                    ),
179                    FrameworkComponent::new(
180                        "mlflow.log_metrics()",
181                        "Metric logging",
182                        "ExperimentRun::log_metric()",
183                    ),
184                    FrameworkComponent::new(
185                        "mlflow.set_tags()",
186                        "Run tagging",
187                        "ExperimentRun::tags",
188                    ),
189                ],
190            },
191            FrameworkCategory {
192                name: "Model Registry".to_string(),
193                components: vec![
194                    FrameworkComponent::with_subs(
195                        "mlflow.register_model()",
196                        "Model versioning",
197                        "SovereignDistribution",
198                        vec!["stage_transitions", "model_versions"],
199                    ),
200                    FrameworkComponent::new(
201                        "mlflow.pyfunc.log_model()",
202                        "Model artifact storage",
203                        "SovereignArtifact",
204                    ),
205                ],
206            },
207            FrameworkCategory {
208                name: "Artifact Storage".to_string(),
209                components: vec![
210                    FrameworkComponent::new(
211                        "mlflow.log_artifact()",
212                        "File storage",
213                        "SovereignArtifact",
214                    ),
215                    FrameworkComponent::new(
216                        "S3/Azure/GCS backends",
217                        "Cloud storage",
218                        "Batuta deploy (sovereign)",
219                    ),
220                ],
221            },
222            FrameworkCategory {
223                name: "Search & Query".to_string(),
224                components: vec![
225                    FrameworkComponent::new(
226                        "mlflow.search_runs()",
227                        "Run search",
228                        "ExperimentStorage::list_runs()",
229                    ),
230                    FrameworkComponent::new(
231                        "mlflow.search_experiments()",
232                        "Experiment search",
233                        "ExperimentStorage trait",
234                    ),
235                ],
236            },
237            FrameworkCategory {
238                name: "GenAI / LLM".to_string(),
239                components: vec![
240                    FrameworkComponent::with_subs(
241                        "mlflow.tracing",
242                        "LLM trace capture",
243                        "Realizar::trace()",
244                        vec!["OpenAI", "LangChain", "LlamaIndex"],
245                    ),
246                    FrameworkComponent::new(
247                        "mlflow.evaluate()",
248                        "LLM evaluation",
249                        "Entrenar::evaluate()",
250                    ),
251                    FrameworkComponent::new(
252                        "Prompt Registry",
253                        "Prompt versioning",
254                        "Realizar::PromptTemplate",
255                    ),
256                ],
257            },
258            FrameworkCategory {
259                name: "Deployment".to_string(),
260                components: vec![
261                    FrameworkComponent::new(
262                        "mlflow models serve",
263                        "Model serving",
264                        "Batuta serve (GGUF)",
265                    ),
266                    FrameworkComponent::new(
267                        "MLflow Gateway",
268                        "LLM gateway",
269                        "Batuta serve + SpilloverRouter",
270                    ),
271                ],
272            },
273        ],
274    }
275}
276
277/// Build Weights & Biases tree
278pub fn build_wandb_tree() -> ExperimentTree {
279    ExperimentTree {
280        framework: "Weights & Biases".to_string(),
281        replacement: "Entrenar + Trueno-Viz".to_string(),
282        categories: vec![
283            FrameworkCategory {
284                name: "Experiment Tracking".to_string(),
285                components: vec![
286                    FrameworkComponent::new(
287                        "wandb.init()",
288                        "Run initialization",
289                        "Entrenar::ExperimentRun::new()",
290                    ),
291                    FrameworkComponent::new(
292                        "wandb.log()",
293                        "Metric logging",
294                        "ExperimentRun::log_metric()",
295                    ),
296                    FrameworkComponent::new(
297                        "wandb.config",
298                        "Hyperparameter config",
299                        "ExperimentRun::hyperparameters",
300                    ),
301                ],
302            },
303            FrameworkCategory {
304                name: "Visualization".to_string(),
305                components: vec![
306                    FrameworkComponent::new("wandb.plot", "Custom plots", "Trueno-Viz::Chart"),
307                    FrameworkComponent::new("Tables", "Data tables", "Trueno-Viz::DataGrid"),
308                    FrameworkComponent::new(
309                        "Media logging",
310                        "Images/audio/video",
311                        "Presentar::MediaView",
312                    ),
313                ],
314            },
315            FrameworkCategory {
316                name: "Sweeps".to_string(),
317                components: vec![FrameworkComponent::with_subs(
318                    "wandb.sweep()",
319                    "Hyperparameter search",
320                    "Entrenar::HyperparameterSearch",
321                    vec!["grid", "random", "bayes"],
322                )],
323            },
324            FrameworkCategory {
325                name: "Artifacts".to_string(),
326                components: vec![FrameworkComponent::new(
327                    "wandb.Artifact",
328                    "Dataset/model versioning",
329                    "SovereignArtifact",
330                )],
331            },
332        ],
333    }
334}
335
336/// Build Neptune tree
337pub fn build_neptune_tree() -> ExperimentTree {
338    ExperimentTree {
339        framework: "Neptune.ai".to_string(),
340        replacement: "Entrenar".to_string(),
341        categories: vec![
342            FrameworkCategory {
343                name: "Experiment Tracking".to_string(),
344                components: vec![
345                    FrameworkComponent::new(
346                        "neptune.init_run()",
347                        "Run initialization",
348                        "Entrenar::ExperimentRun::new()",
349                    ),
350                    FrameworkComponent::new(
351                        "run[\"metric\"].log()",
352                        "Metric logging",
353                        "ExperimentRun::log_metric()",
354                    ),
355                ],
356            },
357            FrameworkCategory {
358                name: "Metadata Store".to_string(),
359                components: vec![
360                    FrameworkComponent::new(
361                        "Namespace hierarchy",
362                        "Nested metadata",
363                        "ExperimentRun::hyperparameters (JSON)",
364                    ),
365                    FrameworkComponent::new(
366                        "System metrics",
367                        "CPU/GPU/memory",
368                        "EnergyMetrics + ComputeDevice",
369                    ),
370                ],
371            },
372        ],
373    }
374}
375
376/// Build DVC tree
377pub fn build_dvc_tree() -> ExperimentTree {
378    ExperimentTree {
379        framework: "DVC".to_string(),
380        replacement: "Batuta + Trueno-DB".to_string(),
381        categories: vec![
382            FrameworkCategory {
383                name: "Data Versioning".to_string(),
384                components: vec![
385                    FrameworkComponent::new(
386                        "dvc add",
387                        "Track data files",
388                        "Trueno-DB::DatasetVersion",
389                    ),
390                    FrameworkComponent::new(
391                        "dvc push/pull",
392                        "Remote storage",
393                        "SovereignDistribution",
394                    ),
395                ],
396            },
397            FrameworkCategory {
398                name: "Experiment Tracking".to_string(),
399                components: vec![
400                    FrameworkComponent::new("dvc exp run", "Run experiments", "Batuta orchestrate"),
401                    FrameworkComponent::new(
402                        "dvc exp show",
403                        "Compare experiments",
404                        "Batuta experiment tree",
405                    ),
406                    FrameworkComponent::new(
407                        "dvc metrics",
408                        "Metric tracking",
409                        "ExperimentRun::metrics",
410                    ),
411                ],
412            },
413            FrameworkCategory {
414                name: "Pipelines".to_string(),
415                components: vec![
416                    FrameworkComponent::new("dvc.yaml", "Pipeline definition", "batuta.toml"),
417                    FrameworkComponent::new(
418                        "dvc repro",
419                        "Reproduce pipeline",
420                        "Batuta transpile + validate",
421                    ),
422                ],
423            },
424        ],
425    }
426}
427
428/// Build all integration mappings
429pub fn build_integration_mappings() -> Vec<IntegrationMapping> {
430    vec![
431        // Experiment Tracking
432        IntegrationMapping::rep(
433            "Entrenar::ExperimentRun",
434            "mlflow.start_run()",
435            "Experiment Tracking",
436        ),
437        IntegrationMapping::rep(
438            "ExperimentRun::log_metric()",
439            "mlflow.log_metrics()",
440            "Experiment Tracking",
441        ),
442        IntegrationMapping::rep(
443            "ExperimentRun::log_param()",
444            "mlflow.log_params()",
445            "Experiment Tracking",
446        ),
447        IntegrationMapping::rep("ExperimentRun::tags", "mlflow.set_tags()", "Experiment Tracking"),
448        IntegrationMapping::rep("Entrenar::ExperimentRun", "wandb.init()", "Experiment Tracking"),
449        IntegrationMapping::rep(
450            "Entrenar::ExperimentRun",
451            "neptune.init_run()",
452            "Experiment Tracking",
453        ),
454        // Model Registry
455        IntegrationMapping::rep(
456            "SovereignDistribution",
457            "mlflow.register_model()",
458            "Model Registry",
459        ),
460        IntegrationMapping::rep("SovereignArtifact", "mlflow.pyfunc.log_model()", "Model Registry"),
461        IntegrationMapping::rep("SovereignArtifact", "wandb.Artifact", "Model Registry"),
462        // Cost & Energy
463        IntegrationMapping::rep("EnergyMetrics", "System metrics (wandb/neptune)", "Cost & Energy"),
464        IntegrationMapping::rep("CostMetrics", "N/A (not in MLflow)", "Cost & Energy"),
465        IntegrationMapping::rep(
466            "CostPerformanceBenchmark",
467            "N/A (Pareto frontier)",
468            "Cost & Energy",
469        ),
470        IntegrationMapping::rep("ComputeDevice", "mlflow.system_metrics", "Cost & Energy"),
471        // Visualization
472        IntegrationMapping::rep("Trueno-Viz::Chart", "wandb.plot", "Visualization"),
473        IntegrationMapping::rep("Trueno-Viz::DataGrid", "wandb.Table", "Visualization"),
474        IntegrationMapping::rep("Presentar::Dashboard", "MLflow UI", "Visualization"),
475        // LLM / GenAI
476        IntegrationMapping::rep("Realizar::trace()", "mlflow.tracing", "LLM / GenAI"),
477        IntegrationMapping::rep("Entrenar::evaluate()", "mlflow.evaluate()", "LLM / GenAI"),
478        IntegrationMapping::rep(
479            "Realizar::PromptTemplate",
480            "MLflow Prompt Registry",
481            "LLM / GenAI",
482        ),
483        // Deployment
484        IntegrationMapping::rep("Batuta serve", "mlflow models serve", "Deployment"),
485        IntegrationMapping::rep("SpilloverRouter", "MLflow Gateway", "Deployment"),
486        IntegrationMapping::rep("SovereignDistribution", "MLflow Docker/K8s", "Deployment"),
487        // Data Versioning
488        IntegrationMapping::rep("Trueno-DB::DatasetVersion", "dvc add", "Data Versioning"),
489        IntegrationMapping::rep("Batuta orchestrate", "dvc repro", "Data Versioning"),
490        // Academic / Research
491        IntegrationMapping::rep("ResearchArtifact", "N/A (ORCID/CRediT)", "Academic / Research"),
492        IntegrationMapping::rep("CitationMetadata", "N/A (BibTeX/CFF)", "Academic / Research"),
493        IntegrationMapping::rep("PreRegistration", "N/A (reproducibility)", "Academic / Research"),
494    ]
495}
496
497/// Format a category's components as ASCII tree lines.
498fn format_category_components(
499    output: &mut String,
500    category: &FrameworkCategory,
501    cat_continuation: &str,
502) {
503    for (comp_idx, component) in category.components.iter().enumerate() {
504        let is_last_comp = comp_idx == category.components.len() - 1;
505        let comp_prefix = if is_last_comp { "└──" } else { "├──" };
506
507        output.push_str(&format!(
508            "{}{} {} → {}\n",
509            cat_continuation, comp_prefix, component.name, component.replacement
510        ));
511
512        if !component.sub_components.is_empty() {
513            let sub_cont = if is_last_comp {
514                format!("{}    ", cat_continuation)
515            } else {
516                format!("{}│   ", cat_continuation)
517            };
518            for (sub_idx, sub) in component.sub_components.iter().enumerate() {
519                let sub_prefix = if sub_idx == component.sub_components.len() - 1 {
520                    "└──"
521                } else {
522                    "├──"
523                };
524                output.push_str(&format!("{}{} {}\n", sub_cont, sub_prefix, sub));
525            }
526        }
527    }
528}
529
530/// Format a single framework tree as ASCII
531pub fn format_framework_tree(tree: &ExperimentTree) -> String {
532    let mut output = String::new();
533    output.push_str(&format!("{} (Python) → {} (Rust)\n", tree.framework, tree.replacement));
534
535    for (cat_idx, category) in tree.categories.iter().enumerate() {
536        let is_last_cat = cat_idx == tree.categories.len() - 1;
537        let cat_prefix = if is_last_cat { "└──" } else { "├──" };
538        let cat_continuation = if is_last_cat { "    " } else { "│   " };
539
540        output.push_str(&format!("{} {}\n", cat_prefix, category.name));
541        format_category_components(&mut output, category, cat_continuation);
542    }
543
544    output
545}
546
547/// Format all frameworks
548pub fn format_all_frameworks() -> String {
549    let mut output = String::new();
550    output.push_str("EXPERIMENT TRACKING FRAMEWORKS ECOSYSTEM\n");
551    output.push_str("========================================\n\n");
552
553    output.push_str(&format_framework_tree(&build_mlflow_tree()));
554    output.push('\n');
555    output.push_str(&format_framework_tree(&build_wandb_tree()));
556    output.push('\n');
557    output.push_str(&format_framework_tree(&build_neptune_tree()));
558    output.push('\n');
559    output.push_str(&format_framework_tree(&build_dvc_tree()));
560
561    output.push_str(&format!(
562        "\nSummary: {} Python frameworks replaced by sovereign Rust stack\n",
563        ExperimentFramework::all().len()
564    ));
565
566    output
567}
568
569/// Format integration mappings
570pub fn format_integration_mappings() -> String {
571    let mappings = build_integration_mappings();
572    let mut output = String::new();
573
574    output.push_str("PAIML REPLACEMENTS FOR PYTHON EXPERIMENT TRACKING\n");
575    output.push_str("=================================================\n\n");
576
577    // Group by category
578    let categories = [
579        "Experiment Tracking",
580        "Model Registry",
581        "Cost & Energy",
582        "Visualization",
583        "LLM / GenAI",
584        "Deployment",
585        "Data Versioning",
586        "Academic / Research",
587    ];
588
589    for category in categories {
590        let cat_mappings: Vec<_> = mappings.iter().filter(|m| m.category == category).collect();
591
592        if cat_mappings.is_empty() {
593            continue;
594        }
595
596        output.push_str(&format!("{}\n", category.to_uppercase()));
597
598        for (idx, mapping) in cat_mappings.iter().enumerate() {
599            let is_last = idx == cat_mappings.len() - 1;
600            let prefix = if is_last { "└──" } else { "├──" };
601
602            output.push_str(&format!(
603                "{} [{}] {} ← {}\n",
604                prefix,
605                mapping.integration_type.code(),
606                mapping.paiml_component,
607                mapping.python_component
608            ));
609        }
610        output.push('\n');
611    }
612
613    output.push_str("Legend: [REP]=Replaces (Python eliminated)\n\n");
614    output.push_str(&format!(
615        "Summary: {} Python components replaced by sovereign Rust alternatives\n",
616        mappings.len()
617    ));
618    output.push_str("         Zero Python dependencies in production\n");
619
620    output
621}
622
623/// Format as JSON
624pub fn format_json(framework: Option<ExperimentFramework>, integration: bool) -> String {
625    if integration {
626        let mappings = build_integration_mappings();
627        serde_json::to_string_pretty(&mappings).unwrap_or_default()
628    } else {
629        match framework {
630            Some(ExperimentFramework::MLflow) => {
631                serde_json::to_string_pretty(&build_mlflow_tree()).unwrap_or_default()
632            }
633            Some(ExperimentFramework::WandB) => {
634                serde_json::to_string_pretty(&build_wandb_tree()).unwrap_or_default()
635            }
636            Some(ExperimentFramework::Neptune) => {
637                serde_json::to_string_pretty(&build_neptune_tree()).unwrap_or_default()
638            }
639            Some(ExperimentFramework::Dvc) => {
640                serde_json::to_string_pretty(&build_dvc_tree()).unwrap_or_default()
641            }
642            Some(fw @ (ExperimentFramework::CometML | ExperimentFramework::Sacred)) => {
643                // Minimal trees for these
644                let tree = ExperimentTree {
645                    framework: fw.name().to_string(),
646                    replacement: fw.replacement().to_string(),
647                    categories: vec![],
648                };
649                serde_json::to_string_pretty(&tree).unwrap_or_default()
650            }
651            None => {
652                let trees = vec![
653                    build_mlflow_tree(),
654                    build_wandb_tree(),
655                    build_neptune_tree(),
656                    build_dvc_tree(),
657                ];
658                serde_json::to_string_pretty(&trees).unwrap_or_default()
659            }
660        }
661    }
662}
663
664// ============================================================================
665// TESTS
666// ============================================================================
667
668#[cfg(test)]
669#[allow(non_snake_case)]
670mod tests {
671    use super::*;
672
673    #[test]
674    fn test_EXP_TREE_001_framework_names() {
675        assert_eq!(ExperimentFramework::MLflow.name(), "MLflow");
676        assert_eq!(ExperimentFramework::WandB.name(), "Weights & Biases");
677        assert_eq!(ExperimentFramework::Neptune.name(), "Neptune.ai");
678        assert_eq!(ExperimentFramework::Dvc.name(), "DVC");
679    }
680
681    #[test]
682    fn test_EXP_TREE_002_framework_replacements() {
683        assert_eq!(ExperimentFramework::MLflow.replacement(), "Entrenar + Batuta");
684        assert_eq!(ExperimentFramework::WandB.replacement(), "Entrenar + Trueno-Viz");
685        assert_eq!(ExperimentFramework::Neptune.replacement(), "Entrenar");
686        assert_eq!(ExperimentFramework::Dvc.replacement(), "Batuta + Trueno-DB");
687    }
688
689    #[test]
690    fn test_EXP_TREE_003_all_frameworks() {
691        let all = ExperimentFramework::all();
692        assert_eq!(all.len(), 6);
693    }
694
695    #[test]
696    fn test_EXP_TREE_004_integration_type_code() {
697        assert_eq!(IntegrationType::Replaces.code(), "REP");
698    }
699
700    #[test]
701    fn test_EXP_TREE_005_mlflow_tree_structure() {
702        let tree = build_mlflow_tree();
703        assert_eq!(tree.framework, "MLflow");
704        assert_eq!(tree.replacement, "Entrenar + Batuta");
705        assert!(!tree.categories.is_empty());
706
707        // Check experiment tracking category exists
708        let exp_tracking = tree.categories.iter().find(|c| c.name == "Experiment Tracking");
709        assert!(exp_tracking.is_some());
710    }
711
712    #[test]
713    fn test_EXP_TREE_006_mlflow_has_genai() {
714        let tree = build_mlflow_tree();
715        let genai = tree.categories.iter().find(|c| c.name == "GenAI / LLM");
716        assert!(genai.is_some());
717
718        let genai = genai.expect("unexpected failure");
719        assert!(genai.components.iter().any(|c| c.name == "mlflow.tracing"));
720    }
721
722    #[test]
723    fn test_EXP_TREE_007_wandb_tree_structure() {
724        let tree = build_wandb_tree();
725        assert_eq!(tree.framework, "Weights & Biases");
726        assert_eq!(tree.replacement, "Entrenar + Trueno-Viz");
727
728        // Check visualization category
729        let viz = tree.categories.iter().find(|c| c.name == "Visualization");
730        assert!(viz.is_some());
731    }
732
733    #[test]
734    fn test_EXP_TREE_008_neptune_tree_structure() {
735        let tree = build_neptune_tree();
736        assert_eq!(tree.framework, "Neptune.ai");
737        assert_eq!(tree.replacement, "Entrenar");
738    }
739
740    #[test]
741    fn test_EXP_TREE_009_dvc_tree_structure() {
742        let tree = build_dvc_tree();
743        assert_eq!(tree.framework, "DVC");
744        assert_eq!(tree.replacement, "Batuta + Trueno-DB");
745
746        // Check data versioning
747        let data_ver = tree.categories.iter().find(|c| c.name == "Data Versioning");
748        assert!(data_ver.is_some());
749    }
750
751    #[test]
752    fn test_EXP_TREE_010_integration_mappings_count() {
753        let mappings = build_integration_mappings();
754        assert!(mappings.len() >= 20);
755    }
756
757    #[test]
758    fn test_EXP_TREE_011_integration_mappings_categories() {
759        let mappings = build_integration_mappings();
760        let categories: std::collections::HashSet<_> =
761            mappings.iter().map(|m| m.category.as_str()).collect();
762
763        assert!(categories.contains("Experiment Tracking"));
764        assert!(categories.contains("Model Registry"));
765        assert!(categories.contains("Visualization"));
766        assert!(categories.contains("LLM / GenAI"));
767    }
768
769    #[test]
770    fn test_EXP_TREE_012_format_framework_tree() {
771        let tree = build_mlflow_tree();
772        let output = format_framework_tree(&tree);
773
774        assert!(output.contains("MLflow (Python) → Entrenar + Batuta (Rust)"));
775        assert!(output.contains("Experiment Tracking"));
776        assert!(output.contains("mlflow.start_run()"));
777    }
778
779    #[test]
780    fn test_EXP_TREE_013_format_all_frameworks() {
781        let output = format_all_frameworks();
782
783        assert!(output.contains("EXPERIMENT TRACKING FRAMEWORKS ECOSYSTEM"));
784        assert!(output.contains("MLflow"));
785        assert!(output.contains("Weights & Biases"));
786        assert!(output.contains("Neptune.ai"));
787        assert!(output.contains("DVC"));
788        assert!(output.contains("Summary:"));
789    }
790
791    #[test]
792    fn test_EXP_TREE_014_format_integration_mappings() {
793        let output = format_integration_mappings();
794
795        assert!(output.contains("PAIML REPLACEMENTS"));
796        assert!(output.contains("[REP]"));
797        assert!(output.contains("Entrenar::ExperimentRun"));
798        assert!(output.contains("mlflow.start_run()"));
799        assert!(output.contains("Legend:"));
800    }
801
802    #[test]
803    fn test_EXP_TREE_015_json_output_single() {
804        let json = format_json(Some(ExperimentFramework::MLflow), false);
805        assert!(json.contains("MLflow"));
806        assert!(json.contains("Entrenar"));
807
808        // Verify it's valid JSON
809        let parsed: Result<ExperimentTree, _> = serde_json::from_str(&json);
810        assert!(parsed.is_ok());
811    }
812
813    #[test]
814    fn test_EXP_TREE_016_json_output_all() {
815        let json = format_json(None, false);
816
817        // Verify it's valid JSON array
818        let parsed: Result<Vec<ExperimentTree>, _> = serde_json::from_str(&json);
819        assert!(parsed.is_ok());
820        assert_eq!(parsed.expect("unexpected failure").len(), 4);
821    }
822
823    #[test]
824    fn test_EXP_TREE_017_json_output_integration() {
825        let json = format_json(None, true);
826
827        // Verify it's valid JSON array
828        let parsed: Result<Vec<IntegrationMapping>, _> = serde_json::from_str(&json);
829        assert!(parsed.is_ok());
830        assert!(parsed.expect("unexpected failure").len() >= 20);
831    }
832
833    #[test]
834    fn test_EXP_TREE_018_all_mappings_have_replacements() {
835        let mappings = build_integration_mappings();
836        for mapping in &mappings {
837            assert_eq!(mapping.integration_type, IntegrationType::Replaces);
838            assert!(!mapping.paiml_component.is_empty());
839            assert!(!mapping.python_component.is_empty());
840        }
841    }
842
843    #[test]
844    fn test_EXP_TREE_019_academic_features() {
845        let mappings = build_integration_mappings();
846        let academic: Vec<_> =
847            mappings.iter().filter(|m| m.category == "Academic / Research").collect();
848
849        assert!(academic.len() >= 3);
850        assert!(academic.iter().any(|m| m.paiml_component.contains("ResearchArtifact")));
851        assert!(academic.iter().any(|m| m.paiml_component.contains("CitationMetadata")));
852    }
853
854    #[test]
855    fn test_EXP_TREE_020_cost_energy_features() {
856        let mappings = build_integration_mappings();
857        let cost_energy: Vec<_> =
858            mappings.iter().filter(|m| m.category == "Cost & Energy").collect();
859
860        assert!(cost_energy.len() >= 3);
861        assert!(cost_energy.iter().any(|m| m.paiml_component.contains("EnergyMetrics")));
862        assert!(cost_energy.iter().any(|m| m.paiml_component.contains("CostMetrics")));
863    }
864}