1use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub enum ExperimentFramework {
14 MLflow,
16 WandB,
18 Neptune,
20 CometML,
22 Sacred,
24 Dvc,
26}
27
28impl ExperimentFramework {
29 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
68pub enum IntegrationType {
69 Replaces,
71}
72
73impl IntegrationType {
74 pub fn code(&self) -> &'static str {
76 match self {
77 IntegrationType::Replaces => "REP",
78 }
79 }
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct FrameworkComponent {
85 pub name: String,
87 pub description: String,
89 pub replacement: String,
91 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#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct FrameworkCategory {
118 pub name: String,
120 pub components: Vec<FrameworkComponent>,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct IntegrationMapping {
127 pub paiml_component: String,
129 pub python_component: String,
131 pub integration_type: IntegrationType,
133 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#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct ExperimentTree {
151 pub framework: String,
153 pub replacement: String,
155 pub categories: Vec<FrameworkCategory>,
157}
158
159pub 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
277pub 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
336pub 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
376pub 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
428pub fn build_integration_mappings() -> Vec<IntegrationMapping> {
430 vec![
431 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 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 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 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 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 IntegrationMapping::rep("Batuta serve", "mlflow models serve", "Deployment"),
485 IntegrationMapping::rep("SpilloverRouter", "MLflow Gateway", "Deployment"),
486 IntegrationMapping::rep("SovereignDistribution", "MLflow Docker/K8s", "Deployment"),
487 IntegrationMapping::rep("Trueno-DB::DatasetVersion", "dvc add", "Data Versioning"),
489 IntegrationMapping::rep("Batuta orchestrate", "dvc repro", "Data Versioning"),
490 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
497fn 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
530pub 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
547pub 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
569pub 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 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
623pub 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 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#[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 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 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 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 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 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 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}