1use serde::{Deserialize, Serialize};
6use std::fmt;
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum HealthStatus {
16 Synced,
18 Behind,
20 Ahead,
22 NotFound,
24 Error(String),
26}
27
28impl fmt::Display for HealthStatus {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 Self::Synced => write!(f, "✓"),
32 Self::Behind => write!(f, "⚠"),
33 Self::Ahead => write!(f, "↑"),
34 Self::NotFound => write!(f, "?"),
35 Self::Error(_) => write!(f, "✗"),
36 }
37 }
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct Component {
43 pub name: String,
45 pub description: String,
47 pub version_local: Option<semver::Version>,
49 pub version_remote: Option<semver::Version>,
51 pub health: HealthStatus,
53}
54
55impl Component {
56 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
58 Self {
59 name: name.into(),
60 description: description.into(),
61 version_local: None,
62 version_remote: None,
63 health: HealthStatus::NotFound,
64 }
65 }
66
67 pub fn with_local_version(mut self, version: semver::Version) -> Self {
69 self.version_local = Some(version);
70 self.update_health();
71 self
72 }
73
74 pub fn with_remote_version(mut self, version: semver::Version) -> Self {
76 self.version_remote = Some(version);
77 self.update_health();
78 self
79 }
80
81 fn update_health(&mut self) {
83 self.health = match (&self.version_local, &self.version_remote) {
84 (Some(local), Some(remote)) => {
85 if local == remote {
86 HealthStatus::Synced
87 } else if local < remote {
88 HealthStatus::Behind
89 } else {
90 HealthStatus::Ahead
91 }
92 }
93 (Some(_), None) => HealthStatus::NotFound,
94 (None, Some(_)) => HealthStatus::NotFound,
95 (None, None) => HealthStatus::NotFound,
96 };
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct StackLayer {
103 pub name: String,
105 pub components: Vec<Component>,
107}
108
109impl StackLayer {
110 pub fn new(name: impl Into<String>) -> Self {
112 Self { name: name.into(), components: Vec::new() }
113 }
114
115 pub fn add_component(mut self, component: Component) -> Self {
117 self.components.push(component);
118 self
119 }
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct StackTree {
125 pub name: String,
127 pub total_crates: usize,
129 pub layers: Vec<StackLayer>,
131}
132
133impl StackTree {
134 pub fn new(name: impl Into<String>) -> Self {
136 Self { name: name.into(), total_crates: 0, layers: Vec::new() }
137 }
138
139 pub fn add_layer(mut self, layer: StackLayer) -> Self {
141 self.total_crates += layer.components.len();
142 self.layers.push(layer);
143 self
144 }
145
146 pub fn synced_count(&self) -> usize {
148 self.layers
149 .iter()
150 .flat_map(|l| &l.components)
151 .filter(|c| c.health == HealthStatus::Synced)
152 .count()
153 }
154
155 pub fn behind_count(&self) -> usize {
157 self.layers
158 .iter()
159 .flat_map(|l| &l.components)
160 .filter(|c| c.health == HealthStatus::Behind)
161 .count()
162 }
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
171pub enum OutputFormat {
172 #[default]
174 Ascii,
175 Json,
177 Dot,
179}
180
181impl std::str::FromStr for OutputFormat {
182 type Err = String;
183
184 fn from_str(s: &str) -> Result<Self, Self::Err> {
185 match s.to_lowercase().as_str() {
186 "ascii" => Ok(Self::Ascii),
187 "json" => Ok(Self::Json),
188 "dot" => Ok(Self::Dot),
189 _ => Err(format!("Unknown format: {s}")),
190 }
191 }
192}
193
194fn format_component_line(
200 comp: &Component,
201 show_health: bool,
202 comp_prefix: &str,
203 comp_branch: &str,
204) -> String {
205 if !show_health {
206 return format!("{}{}{}\n", comp_prefix, comp_branch, comp.name);
207 }
208 let version_str = match (&comp.version_local, &comp.version_remote) {
209 (Some(local), Some(remote)) if local != remote => {
210 format!("v{} → {}", local, remote)
211 }
212 (Some(local), _) => format!("v{}", local),
213 _ => String::new(),
214 };
215 format!("{}{}{} {} {}\n", comp_prefix, comp_branch, comp.name, comp.health, version_str)
216}
217
218pub fn format_ascii(tree: &StackTree, show_health: bool) -> String {
220 let mut output = format!("{} ({} crates)\n", tree.name, tree.total_crates);
221
222 for (layer_idx, layer) in tree.layers.iter().enumerate() {
223 let is_last_layer = layer_idx == tree.layers.len() - 1;
224 let layer_prefix = if is_last_layer { "└── " } else { "├── " };
225 output.push_str(&format!("{}{}\n", layer_prefix, layer.name));
226
227 let comp_prefix = if is_last_layer { " " } else { "│ " };
228 for (comp_idx, comp) in layer.components.iter().enumerate() {
229 let comp_branch =
230 if comp_idx == layer.components.len() - 1 { "└── " } else { "├── " };
231 output.push_str(&format_component_line(comp, show_health, comp_prefix, comp_branch));
232 }
233 }
234
235 output
236}
237
238pub fn format_json(tree: &StackTree) -> Result<String, serde_json::Error> {
240 serde_json::to_string_pretty(tree)
241}
242
243pub fn format_dot(tree: &StackTree) -> String {
245 let mut output = String::from("digraph paiml_stack {\n");
246 output.push_str(" rankdir=TB;\n");
247 output.push_str(" node [shape=box];\n\n");
248
249 for layer in &tree.layers {
250 output.push_str(&format!(" subgraph cluster_{} {{\n", layer.name.replace(' ', "_")));
251 output.push_str(&format!(" label=\"{}\";\n", layer.name));
252
253 for comp in &layer.components {
254 let color = match comp.health {
255 HealthStatus::Synced => "green",
256 HealthStatus::Behind => "orange",
257 HealthStatus::Ahead => "blue",
258 HealthStatus::NotFound => "gray",
259 HealthStatus::Error(_) => "red",
260 };
261 output.push_str(&format!(" {} [color={}];\n", comp.name.replace('-', "_"), color));
262 }
263
264 output.push_str(" }\n\n");
265 }
266
267 output.push_str("}\n");
268 output
269}
270
271pub const LAYER_DEFINITIONS: &[(&str, &[&str])] = &[
277 ("core", &["trueno", "trueno-viz", "trueno-db", "trueno-graph", "trueno-rag", "trueno-zram"]),
278 ("ml", &["aprender", "aprender-shell", "aprender-tsp"]),
279 ("inference", &["realizar", "renacer", "alimentar", "entrenar"]),
280 ("orchestration", &["batuta", "certeza", "presentar", "pacha"]),
281 ("distributed", &["repartir", "pepita"]),
282 ("inference", &["whisper-apr"]),
283 ("transpilation", &["ruchy", "decy", "depyler", "bashrs"]),
284 ("docs", &["sovereign-ai-stack-book"]),
285];
286
287pub fn get_component_description(name: &str) -> &'static str {
289 match name {
290 "trueno" => "SIMD tensor operations",
291 "trueno-viz" => "Visualization",
292 "trueno-db" => "Vector database",
293 "trueno-graph" => "Graph algorithms",
294 "trueno-rag" => "RAG framework",
295 "trueno-zram" => "SIMD memory compression",
296 "aprender" => "ML algorithms",
297 "aprender-shell" => "REPL",
298 "aprender-tsp" => "TSP solver",
299 "realizar" => "Inference engine",
300 "renacer" => "Model lifecycle",
301 "alimentar" => "Data pipelines",
302 "entrenar" => "Experiment tracking",
303 "batuta" => "Orchestrator",
304 "certeza" => "Quality gates",
305 "presentar" => "Presentation",
306 "pacha" => "Knowledge base",
307 "repartir" => "Distributed computing",
308 "pepita" => "io_uring kernel interfaces",
309 "whisper-apr" => "Speech-to-text inference",
310 "ruchy" => "Rust-Python bridge",
311 "decy" => "Decision engine",
312 "depyler" => "Python→Rust transpiler",
313 "bashrs" => "Bash→Rust transpiler",
314 "sovereign-ai-stack-book" => "Documentation",
315 _ => "Unknown component",
316 }
317}
318
319pub fn build_tree() -> StackTree {
321 let mut tree = StackTree::new("PAIML Stack");
322
323 for (layer_name, components) in LAYER_DEFINITIONS {
324 let mut layer = StackLayer::new(*layer_name);
325 for comp_name in *components {
326 let component = Component::new(*comp_name, get_component_description(comp_name));
327 layer = layer.add_component(component);
328 }
329 tree = tree.add_layer(layer);
330 }
331
332 tree
333}
334
335#[cfg(test)]
340#[allow(non_snake_case)]
341mod tests {
342 use super::*;
343
344 #[test]
349 fn test_TREE_001_health_status_display_synced() {
350 assert_eq!(format!("{}", HealthStatus::Synced), "✓");
351 }
352
353 #[test]
354 fn test_TREE_001_health_status_display_behind() {
355 assert_eq!(format!("{}", HealthStatus::Behind), "⚠");
356 }
357
358 #[test]
359 fn test_TREE_001_health_status_display_ahead() {
360 assert_eq!(format!("{}", HealthStatus::Ahead), "↑");
361 }
362
363 #[test]
364 fn test_TREE_001_health_status_display_not_found() {
365 assert_eq!(format!("{}", HealthStatus::NotFound), "?");
366 }
367
368 #[test]
369 fn test_TREE_001_health_status_display_error() {
370 assert_eq!(format!("{}", HealthStatus::Error("test".into())), "✗");
371 }
372
373 #[test]
374 fn test_TREE_001_health_status_serialize() {
375 let json = serde_json::to_string(&HealthStatus::Synced).expect("json serialize failed");
376 assert_eq!(json, "\"synced\"");
377 }
378
379 #[test]
380 fn test_TREE_001_health_status_deserialize() {
381 let status: HealthStatus =
382 serde_json::from_str("\"behind\"").expect("json deserialize failed");
383 assert_eq!(status, HealthStatus::Behind);
384 }
385
386 #[test]
391 fn test_TREE_002_component_new() {
392 let comp = Component::new("trueno", "SIMD ops");
393 assert_eq!(comp.name, "trueno");
394 assert_eq!(comp.description, "SIMD ops");
395 assert_eq!(comp.health, HealthStatus::NotFound);
396 }
397
398 #[test]
399 fn test_TREE_002_component_with_local_version() {
400 let comp =
401 Component::new("trueno", "SIMD").with_local_version(semver::Version::new(1, 0, 0));
402 assert_eq!(comp.version_local, Some(semver::Version::new(1, 0, 0)));
403 }
404
405 #[test]
406 fn test_TREE_002_component_health_synced() {
407 let comp = Component::new("trueno", "SIMD")
408 .with_local_version(semver::Version::new(1, 0, 0))
409 .with_remote_version(semver::Version::new(1, 0, 0));
410 assert_eq!(comp.health, HealthStatus::Synced);
411 }
412
413 #[test]
414 fn test_TREE_002_component_health_behind() {
415 let comp = Component::new("trueno", "SIMD")
416 .with_local_version(semver::Version::new(1, 0, 0))
417 .with_remote_version(semver::Version::new(1, 1, 0));
418 assert_eq!(comp.health, HealthStatus::Behind);
419 }
420
421 #[test]
422 fn test_TREE_002_component_health_ahead() {
423 let comp = Component::new("trueno", "SIMD")
424 .with_local_version(semver::Version::new(2, 0, 0))
425 .with_remote_version(semver::Version::new(1, 0, 0));
426 assert_eq!(comp.health, HealthStatus::Ahead);
427 }
428
429 #[test]
434 fn test_TREE_003_stack_layer_new() {
435 let layer = StackLayer::new("core");
436 assert_eq!(layer.name, "core");
437 assert!(layer.components.is_empty());
438 }
439
440 #[test]
441 fn test_TREE_003_stack_layer_add_component() {
442 let layer =
443 StackLayer::new("core").add_component(Component::new("trueno", "SIMD tensor ops"));
444 assert_eq!(layer.components.len(), 1);
445 assert_eq!(layer.components[0].name, "trueno");
446 }
447
448 #[test]
453 fn test_TREE_004_stack_tree_new() {
454 let tree = StackTree::new("PAIML Stack");
455 assert_eq!(tree.name, "PAIML Stack");
456 assert_eq!(tree.total_crates, 0);
457 assert!(tree.layers.is_empty());
458 }
459
460 #[test]
461 fn test_TREE_004_stack_tree_add_layer() {
462 let layer =
463 StackLayer::new("core").add_component(Component::new("trueno", "SIMD tensor ops"));
464 let tree = StackTree::new("PAIML Stack").add_layer(layer);
465 assert_eq!(tree.total_crates, 1);
466 assert_eq!(tree.layers.len(), 1);
467 }
468
469 #[test]
470 fn test_TREE_004_stack_tree_synced_count() {
471 let layer = StackLayer::new("core").add_component(
472 Component::new("trueno", "SIMD")
473 .with_local_version(semver::Version::new(1, 0, 0))
474 .with_remote_version(semver::Version::new(1, 0, 0)),
475 );
476 let tree = StackTree::new("Test").add_layer(layer);
477 assert_eq!(tree.synced_count(), 1);
478 }
479
480 #[test]
481 fn test_TREE_004_stack_tree_behind_count() {
482 let layer = StackLayer::new("core").add_component(
483 Component::new("trueno", "SIMD")
484 .with_local_version(semver::Version::new(1, 0, 0))
485 .with_remote_version(semver::Version::new(2, 0, 0)),
486 );
487 let tree = StackTree::new("Test").add_layer(layer);
488 assert_eq!(tree.behind_count(), 1);
489 }
490
491 #[test]
496 fn test_TREE_005_output_format_from_str_ascii() {
497 assert_eq!("ascii".parse::<OutputFormat>().expect("parse failed"), OutputFormat::Ascii);
498 }
499
500 #[test]
501 fn test_TREE_005_output_format_from_str_json() {
502 assert_eq!("json".parse::<OutputFormat>().expect("parse failed"), OutputFormat::Json);
503 }
504
505 #[test]
506 fn test_TREE_005_output_format_from_str_dot() {
507 assert_eq!("dot".parse::<OutputFormat>().expect("parse failed"), OutputFormat::Dot);
508 }
509
510 #[test]
511 fn test_TREE_005_output_format_from_str_case_insensitive() {
512 assert_eq!("JSON".parse::<OutputFormat>().expect("parse failed"), OutputFormat::Json);
513 }
514
515 #[test]
516 fn test_TREE_005_output_format_from_str_invalid() {
517 assert!("xml".parse::<OutputFormat>().is_err());
518 }
519
520 #[test]
521 fn test_TREE_005_output_format_default() {
522 assert_eq!(OutputFormat::default(), OutputFormat::Ascii);
523 }
524
525 #[test]
530 fn test_TREE_006_format_ascii_header() {
531 let tree = StackTree::new("Test Stack");
532 let output = format_ascii(&tree, false);
533 assert!(output.starts_with("Test Stack (0 crates)"));
534 }
535
536 #[test]
537 fn test_TREE_006_format_ascii_with_layer() {
538 let layer = StackLayer::new("core").add_component(Component::new("trueno", "SIMD"));
539 let tree = StackTree::new("Test").add_layer(layer);
540 let output = format_ascii(&tree, false);
541 assert!(output.contains("└── core"));
542 assert!(output.contains("trueno"));
543 }
544
545 #[test]
546 fn test_TREE_006_format_ascii_with_health() {
547 let layer = StackLayer::new("core").add_component(
548 Component::new("trueno", "SIMD")
549 .with_local_version(semver::Version::new(1, 0, 0))
550 .with_remote_version(semver::Version::new(1, 0, 0)),
551 );
552 let tree = StackTree::new("Test").add_layer(layer);
553 let output = format_ascii(&tree, true);
554 assert!(output.contains("✓"));
555 assert!(output.contains("v1.0.0"));
556 }
557
558 #[test]
559 fn test_TREE_006_format_ascii_version_diff() {
560 let layer = StackLayer::new("core").add_component(
561 Component::new("trueno", "SIMD")
562 .with_local_version(semver::Version::new(1, 0, 0))
563 .with_remote_version(semver::Version::new(2, 0, 0)),
564 );
565 let tree = StackTree::new("Test").add_layer(layer);
566 let output = format_ascii(&tree, true);
567 assert!(output.contains("v1.0.0 → 2.0.0"));
568 }
569
570 #[test]
575 fn test_TREE_007_format_json_valid() {
576 let tree = StackTree::new("Test");
577 let json = format_json(&tree).expect("unexpected failure");
578 assert!(json.contains("\"name\": \"Test\""));
579 }
580
581 #[test]
582 fn test_TREE_007_format_json_roundtrip() {
583 let layer = StackLayer::new("core").add_component(Component::new("trueno", "SIMD"));
584 let tree = StackTree::new("Test").add_layer(layer);
585 let json = format_json(&tree).expect("unexpected failure");
586 let parsed: StackTree = serde_json::from_str(&json).expect("json deserialize failed");
587 assert_eq!(parsed.name, "Test");
588 assert_eq!(parsed.layers[0].components[0].name, "trueno");
589 }
590
591 #[test]
596 fn test_TREE_008_format_dot_header() {
597 let tree = StackTree::new("Test");
598 let dot = format_dot(&tree);
599 assert!(dot.starts_with("digraph paiml_stack {"));
600 assert!(dot.contains("rankdir=TB"));
601 }
602
603 #[test]
604 fn test_TREE_008_format_dot_cluster() {
605 let layer = StackLayer::new("core").add_component(Component::new("trueno", "SIMD"));
606 let tree = StackTree::new("Test").add_layer(layer);
607 let dot = format_dot(&tree);
608 assert!(dot.contains("subgraph cluster_core"));
609 assert!(dot.contains("label=\"core\""));
610 }
611
612 #[test]
613 fn test_TREE_008_format_dot_health_colors() {
614 let layer = StackLayer::new("core").add_component(
615 Component::new("trueno", "SIMD")
616 .with_local_version(semver::Version::new(1, 0, 0))
617 .with_remote_version(semver::Version::new(1, 0, 0)),
618 );
619 let tree = StackTree::new("Test").add_layer(layer);
620 let dot = format_dot(&tree);
621 assert!(dot.contains("color=green"));
622 }
623
624 #[test]
629 fn test_TREE_009_build_tree_creates_all_layers() {
630 let tree = build_tree();
631 assert_eq!(tree.layers.len(), 8);
632 }
633
634 #[test]
635 fn test_TREE_009_build_tree_total_crates() {
636 let tree = build_tree();
637 assert_eq!(tree.total_crates, 25);
638 }
639
640 #[test]
641 fn test_TREE_009_build_tree_core_layer() {
642 let tree = build_tree();
643 let core = &tree.layers[0];
644 assert_eq!(core.name, "core");
645 assert_eq!(core.components.len(), 6);
646 assert_eq!(core.components[0].name, "trueno");
647 }
648
649 #[test]
650 fn test_TREE_009_get_component_description() {
651 assert_eq!(get_component_description("trueno"), "SIMD tensor operations");
652 assert_eq!(get_component_description("batuta"), "Orchestrator");
653 assert_eq!(get_component_description("unknown"), "Unknown component");
654 }
655
656 #[test]
661 fn test_TREE_010_full_tree_ascii_output() {
662 let tree = build_tree();
663 let output = format_ascii(&tree, false);
664 assert!(output.contains("PAIML Stack (25 crates)"));
665 assert!(output.contains("core"));
666 assert!(output.contains("ml"));
667 assert!(output.contains("orchestration"));
668 assert!(output.contains("trueno"));
669 assert!(output.contains("batuta"));
670 }
671
672 #[test]
673 fn test_TREE_010_full_tree_json_output() {
674 let tree = build_tree();
675 let json = format_json(&tree).expect("unexpected failure");
676 let parsed: serde_json::Value =
677 serde_json::from_str(&json).expect("json deserialize failed");
678 assert_eq!(parsed["total_crates"], 25);
679 }
680
681 #[test]
682 fn test_TREE_010_full_tree_dot_output() {
683 let tree = build_tree();
684 let dot = format_dot(&tree);
685 assert!(dot.contains("digraph"));
686 assert!(dot.contains("cluster_core"));
687 assert!(dot.contains("cluster_ml"));
688 }
689}