1use serde::Deserialize;
8use std::collections::HashMap;
9use std::path::Path;
10
11use crate::error::{SimError, SimResult};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct Version {
16 pub major: u32,
18 pub minor: u32,
20 pub patch: u32,
22 pub pre_release: Option<String>,
24}
25
26impl Version {
27 #[must_use]
29 pub fn parse(s: &str) -> Option<Self> {
30 let s = s.trim();
31
32 let s = s
34 .trim_start_matches('^')
35 .trim_start_matches('~')
36 .trim_start_matches(">=")
37 .trim_start_matches("<=")
38 .trim_start_matches('>')
39 .trim_start_matches('<')
40 .trim_start_matches('=')
41 .trim();
42
43 let (version_part, pre_release) = s
45 .find('-')
46 .map_or((s, None), |idx| (&s[..idx], Some(s[idx + 1..].to_string())));
47
48 let parts: Vec<&str> = version_part.split('.').collect();
49 if parts.is_empty() || parts.len() > 3 {
50 return None;
51 }
52
53 let major = parts[0].parse().ok()?;
54 let minor = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
55 let patch = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
56
57 Some(Self {
58 major,
59 minor,
60 patch,
61 pre_release,
62 })
63 }
64
65 #[must_use]
67 pub fn satisfies_minimum(&self, min: &Self) -> bool {
68 if self.major != min.major {
69 return self.major > min.major;
70 }
71 if self.minor != min.minor {
72 return self.minor > min.minor;
73 }
74 self.patch >= min.patch
75 }
76}
77
78impl std::fmt::Display for Version {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 if let Some(ref pre) = self.pre_release {
81 write!(f, "{}.{}.{}-{}", self.major, self.minor, self.patch, pre)
82 } else {
83 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
84 }
85 }
86}
87
88#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
90pub enum StackComponent {
91 Trueno,
93 TruenoDB,
95 TruenoGraph,
97 TruenoRag,
99 Aprender,
101 Entrenar,
103 Realizar,
105 Alimentar,
107 Pacha,
109 Renacer,
111}
112
113impl StackComponent {
114 #[must_use]
116 pub const fn crate_name(&self) -> &'static str {
117 match self {
118 Self::Trueno => "trueno",
119 Self::TruenoDB => "trueno-db",
120 Self::TruenoGraph => "trueno-graph",
121 Self::TruenoRag => "trueno-rag",
122 Self::Aprender => "aprender",
123 Self::Entrenar => "entrenar",
124 Self::Realizar => "realizar",
125 Self::Alimentar => "alimentar",
126 Self::Pacha => "pacha",
127 Self::Renacer => "renacer",
128 }
129 }
130
131 #[must_use]
133 pub const fn all() -> &'static [Self] {
134 &[
135 Self::Trueno,
136 Self::TruenoDB,
137 Self::TruenoGraph,
138 Self::TruenoRag,
139 Self::Aprender,
140 Self::Entrenar,
141 Self::Realizar,
142 Self::Alimentar,
143 Self::Pacha,
144 Self::Renacer,
145 ]
146 }
147}
148
149impl std::fmt::Display for StackComponent {
150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 write!(f, "{}", self.crate_name())
152 }
153}
154
155#[derive(Debug, Clone, Default)]
160pub struct StackDiscovery {
161 components: HashMap<StackComponent, Version>,
163}
164
165impl StackDiscovery {
166 #[must_use]
168 pub fn new() -> Self {
169 Self {
170 components: HashMap::new(),
171 }
172 }
173
174 pub fn from_cargo_toml(path: &Path) -> SimResult<Self> {
180 let content = std::fs::read_to_string(path)
181 .map_err(|e| SimError::config(format!("Failed to read Cargo.toml: {e}")))?;
182
183 Self::from_toml_str(&content)
184 }
185
186 pub fn from_toml_str(content: &str) -> SimResult<Self> {
192 let manifest: CargoManifest = toml::from_str(content)
193 .map_err(|e| SimError::config(format!("Failed to parse Cargo.toml: {e}")))?;
194
195 let mut discovery = Self::new();
196
197 if let Some(deps) = manifest.dependencies {
199 discovery.parse_dependencies(&deps);
200 }
201
202 if let Some(dev_deps) = manifest.dev_dependencies {
204 discovery.parse_dependencies(&dev_deps);
205 }
206
207 Ok(discovery)
208 }
209
210 fn parse_dependencies(&mut self, deps: &HashMap<String, toml::Value>) {
212 for (name, value) in deps {
213 if let Some(component) = Self::parse_stack_component(name) {
214 if let Some(version) = Self::extract_version(value) {
215 self.components.insert(component, version);
216 }
217 }
218 }
219 }
220
221 #[must_use]
225 pub fn parse_stack_component(name: &str) -> Option<StackComponent> {
226 let normalized = name.to_lowercase().replace('_', "-");
227 match normalized.as_str() {
228 "trueno" => Some(StackComponent::Trueno),
229 "trueno-db" => Some(StackComponent::TruenoDB),
230 "trueno-graph" => Some(StackComponent::TruenoGraph),
231 "trueno-rag" => Some(StackComponent::TruenoRag),
232 "aprender" => Some(StackComponent::Aprender),
233 "entrenar" => Some(StackComponent::Entrenar),
234 "realizar" => Some(StackComponent::Realizar),
235 "alimentar" => Some(StackComponent::Alimentar),
236 "pacha" => Some(StackComponent::Pacha),
237 "renacer" => Some(StackComponent::Renacer),
238 _ => None,
239 }
240 }
241
242 fn extract_version(value: &toml::Value) -> Option<Version> {
244 match value {
245 toml::Value::String(s) => Version::parse(s),
247 toml::Value::Table(t) => t
249 .get("version")
250 .and_then(|v| v.as_str())
251 .and_then(Version::parse),
252 _ => None,
253 }
254 }
255
256 #[must_use]
258 pub fn has(&self, component: StackComponent) -> bool {
259 self.components.contains_key(&component)
260 }
261
262 #[must_use]
264 pub fn version(&self, component: StackComponent) -> Option<&Version> {
265 self.components.get(&component)
266 }
267
268 #[must_use]
270 pub fn discovered(&self) -> &HashMap<StackComponent, Version> {
271 &self.components
272 }
273
274 #[must_use]
276 pub fn count(&self) -> usize {
277 self.components.len()
278 }
279
280 #[must_use]
282 pub fn is_empty(&self) -> bool {
283 self.components.is_empty()
284 }
285
286 pub fn register(&mut self, component: StackComponent, version: Version) {
288 self.components.insert(component, version);
289 }
290
291 #[must_use]
296 pub fn check_version(&self, component: StackComponent, min_version: &Version) -> bool {
297 self.version(component)
298 .is_some_and(|v| v.satisfies_minimum(min_version))
299 }
300
301 #[must_use]
303 pub fn summary(&self) -> String {
304 if self.is_empty() {
305 return String::from("No Sovereign AI Stack components detected");
306 }
307
308 let mut lines = vec![format!("Detected {} stack components:", self.count())];
309
310 for component in StackComponent::all() {
311 if let Some(version) = self.version(*component) {
312 lines.push(format!(" - {}: v{version}", component.crate_name()));
313 }
314 }
315
316 lines.join("\n")
317 }
318}
319
320#[derive(Deserialize)]
322struct CargoManifest {
323 #[serde(default)]
324 dependencies: Option<HashMap<String, toml::Value>>,
325 #[serde(default, rename = "dev-dependencies")]
326 dev_dependencies: Option<HashMap<String, toml::Value>>,
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn test_version_parse_simple() {
335 let v = Version::parse("1.2.3").unwrap();
336 assert_eq!(v.major, 1);
337 assert_eq!(v.minor, 2);
338 assert_eq!(v.patch, 3);
339 assert!(v.pre_release.is_none());
340 }
341
342 #[test]
343 fn test_version_parse_with_prerelease() {
344 let v = Version::parse("2.0.0-beta").unwrap();
345 assert_eq!(v.major, 2);
346 assert_eq!(v.minor, 0);
347 assert_eq!(v.patch, 0);
348 assert_eq!(v.pre_release.as_deref(), Some("beta"));
349 }
350
351 #[test]
352 fn test_version_parse_partial() {
353 let v = Version::parse("1.5").unwrap();
354 assert_eq!(v.major, 1);
355 assert_eq!(v.minor, 5);
356 assert_eq!(v.patch, 0);
357
358 let v = Version::parse("3").unwrap();
359 assert_eq!(v.major, 3);
360 assert_eq!(v.minor, 0);
361 assert_eq!(v.patch, 0);
362 }
363
364 #[test]
365 fn test_version_parse_with_prefix() {
366 let v = Version::parse("^1.2.3").unwrap();
367 assert_eq!(v.major, 1);
368 assert_eq!(v.minor, 2);
369 assert_eq!(v.patch, 3);
370
371 let v = Version::parse(">=2.0").unwrap();
372 assert_eq!(v.major, 2);
373 assert_eq!(v.minor, 0);
374 }
375
376 #[test]
377 fn test_version_satisfies_minimum() {
378 let v1 = Version::parse("1.2.3").unwrap();
379 let v2 = Version::parse("1.2.0").unwrap();
380 let v3 = Version::parse("1.3.0").unwrap();
381 let v4 = Version::parse("2.0.0").unwrap();
382
383 assert!(v1.satisfies_minimum(&v2)); assert!(!v1.satisfies_minimum(&v3)); assert!(!v1.satisfies_minimum(&v4)); }
387
388 #[test]
389 fn test_version_display() {
390 let v = Version::parse("1.2.3").unwrap();
391 assert_eq!(v.to_string(), "1.2.3");
392
393 let v = Version::parse("1.0.0-alpha").unwrap();
394 assert_eq!(v.to_string(), "1.0.0-alpha");
395 }
396
397 #[test]
398 fn test_stack_component_crate_name() {
399 assert_eq!(StackComponent::Trueno.crate_name(), "trueno");
400 assert_eq!(StackComponent::TruenoDB.crate_name(), "trueno-db");
401 assert_eq!(StackComponent::Aprender.crate_name(), "aprender");
402 }
403
404 #[test]
405 fn test_parse_stack_component() {
406 assert_eq!(
407 StackDiscovery::parse_stack_component("trueno"),
408 Some(StackComponent::Trueno)
409 );
410 assert_eq!(
411 StackDiscovery::parse_stack_component("trueno-db"),
412 Some(StackComponent::TruenoDB)
413 );
414 assert_eq!(
415 StackDiscovery::parse_stack_component("trueno_db"),
416 Some(StackComponent::TruenoDB)
417 );
418 assert_eq!(
419 StackDiscovery::parse_stack_component("APRENDER"),
420 Some(StackComponent::Aprender)
421 );
422 assert_eq!(StackDiscovery::parse_stack_component("unknown"), None);
423 }
424
425 #[test]
426 fn test_discovery_from_toml_simple() {
427 let toml = r#"
428[package]
429name = "test"
430version = "0.1.0"
431
432[dependencies]
433trueno = "1.0.0"
434aprender = "0.5.0"
435serde = "1.0"
436"#;
437
438 let discovery = StackDiscovery::from_toml_str(toml).unwrap();
439
440 assert!(discovery.has(StackComponent::Trueno));
441 assert!(discovery.has(StackComponent::Aprender));
442 assert!(!discovery.has(StackComponent::Entrenar));
443
444 let trueno_v = discovery.version(StackComponent::Trueno).unwrap();
445 assert_eq!(trueno_v.major, 1);
446 assert_eq!(trueno_v.minor, 0);
447 }
448
449 #[test]
450 fn test_discovery_from_toml_table_format() {
451 let toml = r#"
452[dependencies]
453trueno = { version = "2.1.0", features = ["simd"] }
454entrenar = { version = "0.3.0", optional = true }
455"#;
456
457 let discovery = StackDiscovery::from_toml_str(toml).unwrap();
458
459 assert!(discovery.has(StackComponent::Trueno));
460 assert!(discovery.has(StackComponent::Entrenar));
461
462 let trueno_v = discovery.version(StackComponent::Trueno).unwrap();
463 assert_eq!(trueno_v.to_string(), "2.1.0");
464 }
465
466 #[test]
467 fn test_discovery_dev_dependencies() {
468 let toml = r#"
469[dev-dependencies]
470renacer = "0.1.0"
471"#;
472
473 let discovery = StackDiscovery::from_toml_str(toml).unwrap();
474 assert!(discovery.has(StackComponent::Renacer));
475 }
476
477 #[test]
478 fn test_discovery_empty() {
479 let toml = r#"
480[package]
481name = "test"
482version = "0.1.0"
483
484[dependencies]
485serde = "1.0"
486"#;
487
488 let discovery = StackDiscovery::from_toml_str(toml).unwrap();
489
490 assert!(discovery.is_empty());
491 assert_eq!(discovery.count(), 0);
492 }
493
494 #[test]
495 fn test_discovery_check_version() {
496 let toml = r#"
497[dependencies]
498trueno = "1.5.0"
499"#;
500
501 let discovery = StackDiscovery::from_toml_str(toml).unwrap();
502
503 let min_ok = Version::parse("1.0.0").unwrap();
504 let min_exact = Version::parse("1.5.0").unwrap();
505 let min_too_high = Version::parse("2.0.0").unwrap();
506
507 assert!(discovery.check_version(StackComponent::Trueno, &min_ok));
508 assert!(discovery.check_version(StackComponent::Trueno, &min_exact));
509 assert!(!discovery.check_version(StackComponent::Trueno, &min_too_high));
510 assert!(!discovery.check_version(StackComponent::Aprender, &min_ok)); }
512
513 #[test]
514 fn test_discovery_register() {
515 let mut discovery = StackDiscovery::new();
516 assert!(discovery.is_empty());
517
518 discovery.register(StackComponent::Trueno, Version::parse("1.0.0").unwrap());
519
520 assert!(discovery.has(StackComponent::Trueno));
521 assert_eq!(discovery.count(), 1);
522 }
523
524 #[test]
525 fn test_discovery_summary() {
526 let toml = r#"
527[dependencies]
528trueno = "1.0.0"
529aprender = "0.5.0"
530"#;
531
532 let discovery = StackDiscovery::from_toml_str(toml).unwrap();
533 let summary = discovery.summary();
534
535 assert!(summary.contains("2 stack components"));
536 assert!(summary.contains("trueno: v1.0.0"));
537 assert!(summary.contains("aprender: v0.5.0"));
538 }
539
540 #[test]
541 fn test_discovery_summary_empty() {
542 let discovery = StackDiscovery::new();
543 let summary = discovery.summary();
544
545 assert!(summary.contains("No Sovereign AI Stack components detected"));
546 }
547
548 #[test]
549 fn test_stack_component_all() {
550 let all = StackComponent::all();
551 assert_eq!(all.len(), 10);
552
553 let mut seen = std::collections::HashSet::new();
555 for component in all {
556 assert!(seen.insert(*component));
557 }
558 }
559
560 #[test]
561 fn test_stack_component_display() {
562 assert_eq!(format!("{}", StackComponent::Trueno), "trueno");
563 assert_eq!(format!("{}", StackComponent::TruenoGraph), "trueno-graph");
564 assert_eq!(format!("{}", StackComponent::TruenoRag), "trueno-rag");
565 assert_eq!(format!("{}", StackComponent::Entrenar), "entrenar");
566 assert_eq!(format!("{}", StackComponent::Realizar), "realizar");
567 assert_eq!(format!("{}", StackComponent::Alimentar), "alimentar");
568 assert_eq!(format!("{}", StackComponent::Pacha), "pacha");
569 assert_eq!(format!("{}", StackComponent::Renacer), "renacer");
570 }
571
572 #[test]
573 fn test_version_clone() {
574 let v = Version::parse("1.2.3-beta").unwrap();
575 let cloned = v.clone();
576 assert_eq!(cloned.major, v.major);
577 assert_eq!(cloned.pre_release, v.pre_release);
578 }
579
580 #[test]
581 fn test_version_eq() {
582 let v1 = Version::parse("1.2.3").unwrap();
583 let v2 = Version::parse("1.2.3").unwrap();
584 let v3 = Version::parse("1.2.4").unwrap();
585 assert_eq!(v1, v2);
586 assert_ne!(v1, v3);
587 }
588
589 #[test]
590 fn test_version_parse_invalid() {
591 assert!(Version::parse("").is_none());
593 assert!(Version::parse("1.2.3.4.5").is_none());
595 assert!(Version::parse("abc.def.ghi").is_none());
597 }
598
599 #[test]
600 fn test_version_satisfies_major_gt() {
601 let v_new = Version::parse("2.0.0").unwrap();
602 let v_old = Version::parse("1.5.0").unwrap();
603 assert!(v_new.satisfies_minimum(&v_old));
604 }
605
606 #[test]
607 fn test_version_satisfies_minor_gt() {
608 let v_new = Version::parse("1.5.0").unwrap();
609 let v_old = Version::parse("1.3.0").unwrap();
610 assert!(v_new.satisfies_minimum(&v_old));
611 }
612
613 #[test]
614 fn test_stack_component_clone_eq() {
615 let c1 = StackComponent::Trueno;
616 let c2 = c1.clone();
617 assert_eq!(c1, c2);
618 }
619
620 #[test]
621 fn test_stack_component_hash() {
622 use std::collections::HashSet;
623 let mut set = HashSet::new();
624 set.insert(StackComponent::Trueno);
625 set.insert(StackComponent::Aprender);
626 set.insert(StackComponent::Trueno); assert_eq!(set.len(), 2);
628 }
629
630 #[test]
631 fn test_version_parse_with_tilde() {
632 let v = Version::parse("~1.2.3").unwrap();
633 assert_eq!(v.major, 1);
634 assert_eq!(v.minor, 2);
635 assert_eq!(v.patch, 3);
636 }
637
638 #[test]
639 fn test_version_parse_with_lt_gt() {
640 let v = Version::parse(">1.0.0").unwrap();
641 assert_eq!(v.major, 1);
642
643 let v = Version::parse("<2.0.0").unwrap();
644 assert_eq!(v.major, 2);
645
646 let v = Version::parse("<=3.0.0").unwrap();
647 assert_eq!(v.major, 3);
648 }
649}