use serde::Deserialize;
use std::collections::HashMap;
use std::path::Path;
use crate::error::{SimError, SimResult};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Version {
pub major: u32,
pub minor: u32,
pub patch: u32,
pub pre_release: Option<String>,
}
impl Version {
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
let s = s.trim();
let s = s
.trim_start_matches('^')
.trim_start_matches('~')
.trim_start_matches(">=")
.trim_start_matches("<=")
.trim_start_matches('>')
.trim_start_matches('<')
.trim_start_matches('=')
.trim();
let (version_part, pre_release) = s
.find('-')
.map_or((s, None), |idx| (&s[..idx], Some(s[idx + 1..].to_string())));
let parts: Vec<&str> = version_part.split('.').collect();
if parts.is_empty() || parts.len() > 3 {
return None;
}
let major = parts[0].parse().ok()?;
let minor = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
let patch = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
Some(Self {
major,
minor,
patch,
pre_release,
})
}
#[must_use]
pub fn satisfies_minimum(&self, min: &Self) -> bool {
if self.major != min.major {
return self.major > min.major;
}
if self.minor != min.minor {
return self.minor > min.minor;
}
self.patch >= min.patch
}
}
impl std::fmt::Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(ref pre) = self.pre_release {
write!(f, "{}.{}.{}-{}", self.major, self.minor, self.patch, pre)
} else {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
}
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
pub enum StackComponent {
Trueno,
TruenoDB,
TruenoGraph,
TruenoRag,
Aprender,
Entrenar,
Realizar,
Alimentar,
Pacha,
Renacer,
}
impl StackComponent {
#[must_use]
pub const fn crate_name(&self) -> &'static str {
match self {
Self::Trueno => "trueno",
Self::TruenoDB => "trueno-db",
Self::TruenoGraph => "trueno-graph",
Self::TruenoRag => "trueno-rag",
Self::Aprender => "aprender",
Self::Entrenar => "entrenar",
Self::Realizar => "realizar",
Self::Alimentar => "alimentar",
Self::Pacha => "pacha",
Self::Renacer => "renacer",
}
}
#[must_use]
pub const fn all() -> &'static [Self] {
&[
Self::Trueno,
Self::TruenoDB,
Self::TruenoGraph,
Self::TruenoRag,
Self::Aprender,
Self::Entrenar,
Self::Realizar,
Self::Alimentar,
Self::Pacha,
Self::Renacer,
]
}
}
impl std::fmt::Display for StackComponent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.crate_name())
}
}
#[derive(Debug, Clone, Default)]
pub struct StackDiscovery {
components: HashMap<StackComponent, Version>,
}
impl StackDiscovery {
#[must_use]
pub fn new() -> Self {
Self {
components: HashMap::new(),
}
}
pub fn from_cargo_toml(path: &Path) -> SimResult<Self> {
let content = std::fs::read_to_string(path)
.map_err(|e| SimError::config(format!("Failed to read Cargo.toml: {e}")))?;
Self::from_toml_str(&content)
}
pub fn from_toml_str(content: &str) -> SimResult<Self> {
let manifest: CargoManifest = toml::from_str(content)
.map_err(|e| SimError::config(format!("Failed to parse Cargo.toml: {e}")))?;
let mut discovery = Self::new();
if let Some(deps) = manifest.dependencies {
discovery.parse_dependencies(&deps);
}
if let Some(dev_deps) = manifest.dev_dependencies {
discovery.parse_dependencies(&dev_deps);
}
Ok(discovery)
}
fn parse_dependencies(&mut self, deps: &HashMap<String, toml::Value>) {
for (name, value) in deps {
if let Some(component) = Self::parse_stack_component(name) {
if let Some(version) = Self::extract_version(value) {
self.components.insert(component, version);
}
}
}
}
#[must_use]
pub fn parse_stack_component(name: &str) -> Option<StackComponent> {
let normalized = name.to_lowercase().replace('_', "-");
match normalized.as_str() {
"trueno" => Some(StackComponent::Trueno),
"trueno-db" => Some(StackComponent::TruenoDB),
"trueno-graph" => Some(StackComponent::TruenoGraph),
"trueno-rag" => Some(StackComponent::TruenoRag),
"aprender" => Some(StackComponent::Aprender),
"entrenar" => Some(StackComponent::Entrenar),
"realizar" => Some(StackComponent::Realizar),
"alimentar" => Some(StackComponent::Alimentar),
"pacha" => Some(StackComponent::Pacha),
"renacer" => Some(StackComponent::Renacer),
_ => None,
}
}
fn extract_version(value: &toml::Value) -> Option<Version> {
match value {
toml::Value::String(s) => Version::parse(s),
toml::Value::Table(t) => t
.get("version")
.and_then(|v| v.as_str())
.and_then(Version::parse),
_ => None,
}
}
#[must_use]
pub fn has(&self, component: StackComponent) -> bool {
self.components.contains_key(&component)
}
#[must_use]
pub fn version(&self, component: StackComponent) -> Option<&Version> {
self.components.get(&component)
}
#[must_use]
pub fn discovered(&self) -> &HashMap<StackComponent, Version> {
&self.components
}
#[must_use]
pub fn count(&self) -> usize {
self.components.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.components.is_empty()
}
pub fn register(&mut self, component: StackComponent, version: Version) {
self.components.insert(component, version);
}
#[must_use]
pub fn check_version(&self, component: StackComponent, min_version: &Version) -> bool {
self.version(component)
.is_some_and(|v| v.satisfies_minimum(min_version))
}
#[must_use]
pub fn summary(&self) -> String {
if self.is_empty() {
return String::from("No Sovereign AI Stack components detected");
}
let mut lines = vec![format!("Detected {} stack components:", self.count())];
for component in StackComponent::all() {
if let Some(version) = self.version(*component) {
lines.push(format!(" - {}: v{version}", component.crate_name()));
}
}
lines.join("\n")
}
}
#[derive(Deserialize)]
struct CargoManifest {
#[serde(default)]
dependencies: Option<HashMap<String, toml::Value>>,
#[serde(default, rename = "dev-dependencies")]
dev_dependencies: Option<HashMap<String, toml::Value>>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_parse_simple() {
let v = Version::parse("1.2.3").unwrap();
assert_eq!(v.major, 1);
assert_eq!(v.minor, 2);
assert_eq!(v.patch, 3);
assert!(v.pre_release.is_none());
}
#[test]
fn test_version_parse_with_prerelease() {
let v = Version::parse("2.0.0-beta").unwrap();
assert_eq!(v.major, 2);
assert_eq!(v.minor, 0);
assert_eq!(v.patch, 0);
assert_eq!(v.pre_release.as_deref(), Some("beta"));
}
#[test]
fn test_version_parse_partial() {
let v = Version::parse("1.5").unwrap();
assert_eq!(v.major, 1);
assert_eq!(v.minor, 5);
assert_eq!(v.patch, 0);
let v = Version::parse("3").unwrap();
assert_eq!(v.major, 3);
assert_eq!(v.minor, 0);
assert_eq!(v.patch, 0);
}
#[test]
fn test_version_parse_with_prefix() {
let v = Version::parse("^1.2.3").unwrap();
assert_eq!(v.major, 1);
assert_eq!(v.minor, 2);
assert_eq!(v.patch, 3);
let v = Version::parse(">=2.0").unwrap();
assert_eq!(v.major, 2);
assert_eq!(v.minor, 0);
}
#[test]
fn test_version_satisfies_minimum() {
let v1 = Version::parse("1.2.3").unwrap();
let v2 = Version::parse("1.2.0").unwrap();
let v3 = Version::parse("1.3.0").unwrap();
let v4 = Version::parse("2.0.0").unwrap();
assert!(v1.satisfies_minimum(&v2)); assert!(!v1.satisfies_minimum(&v3)); assert!(!v1.satisfies_minimum(&v4)); }
#[test]
fn test_version_display() {
let v = Version::parse("1.2.3").unwrap();
assert_eq!(v.to_string(), "1.2.3");
let v = Version::parse("1.0.0-alpha").unwrap();
assert_eq!(v.to_string(), "1.0.0-alpha");
}
#[test]
fn test_stack_component_crate_name() {
assert_eq!(StackComponent::Trueno.crate_name(), "trueno");
assert_eq!(StackComponent::TruenoDB.crate_name(), "trueno-db");
assert_eq!(StackComponent::Aprender.crate_name(), "aprender");
}
#[test]
fn test_parse_stack_component() {
assert_eq!(
StackDiscovery::parse_stack_component("trueno"),
Some(StackComponent::Trueno)
);
assert_eq!(
StackDiscovery::parse_stack_component("trueno-db"),
Some(StackComponent::TruenoDB)
);
assert_eq!(
StackDiscovery::parse_stack_component("trueno_db"),
Some(StackComponent::TruenoDB)
);
assert_eq!(
StackDiscovery::parse_stack_component("APRENDER"),
Some(StackComponent::Aprender)
);
assert_eq!(StackDiscovery::parse_stack_component("unknown"), None);
}
#[test]
fn test_discovery_from_toml_simple() {
let toml = r#"
[package]
name = "test"
version = "0.1.0"
[dependencies]
trueno = "1.0.0"
aprender = "0.5.0"
serde = "1.0"
"#;
let discovery = StackDiscovery::from_toml_str(toml).unwrap();
assert!(discovery.has(StackComponent::Trueno));
assert!(discovery.has(StackComponent::Aprender));
assert!(!discovery.has(StackComponent::Entrenar));
let trueno_v = discovery.version(StackComponent::Trueno).unwrap();
assert_eq!(trueno_v.major, 1);
assert_eq!(trueno_v.minor, 0);
}
#[test]
fn test_discovery_from_toml_table_format() {
let toml = r#"
[dependencies]
trueno = { version = "2.1.0", features = ["simd"] }
entrenar = { version = "0.3.0", optional = true }
"#;
let discovery = StackDiscovery::from_toml_str(toml).unwrap();
assert!(discovery.has(StackComponent::Trueno));
assert!(discovery.has(StackComponent::Entrenar));
let trueno_v = discovery.version(StackComponent::Trueno).unwrap();
assert_eq!(trueno_v.to_string(), "2.1.0");
}
#[test]
fn test_discovery_dev_dependencies() {
let toml = r#"
[dev-dependencies]
renacer = "0.1.0"
"#;
let discovery = StackDiscovery::from_toml_str(toml).unwrap();
assert!(discovery.has(StackComponent::Renacer));
}
#[test]
fn test_discovery_empty() {
let toml = r#"
[package]
name = "test"
version = "0.1.0"
[dependencies]
serde = "1.0"
"#;
let discovery = StackDiscovery::from_toml_str(toml).unwrap();
assert!(discovery.is_empty());
assert_eq!(discovery.count(), 0);
}
#[test]
fn test_discovery_check_version() {
let toml = r#"
[dependencies]
trueno = "1.5.0"
"#;
let discovery = StackDiscovery::from_toml_str(toml).unwrap();
let min_ok = Version::parse("1.0.0").unwrap();
let min_exact = Version::parse("1.5.0").unwrap();
let min_too_high = Version::parse("2.0.0").unwrap();
assert!(discovery.check_version(StackComponent::Trueno, &min_ok));
assert!(discovery.check_version(StackComponent::Trueno, &min_exact));
assert!(!discovery.check_version(StackComponent::Trueno, &min_too_high));
assert!(!discovery.check_version(StackComponent::Aprender, &min_ok)); }
#[test]
fn test_discovery_register() {
let mut discovery = StackDiscovery::new();
assert!(discovery.is_empty());
discovery.register(StackComponent::Trueno, Version::parse("1.0.0").unwrap());
assert!(discovery.has(StackComponent::Trueno));
assert_eq!(discovery.count(), 1);
}
#[test]
fn test_discovery_summary() {
let toml = r#"
[dependencies]
trueno = "1.0.0"
aprender = "0.5.0"
"#;
let discovery = StackDiscovery::from_toml_str(toml).unwrap();
let summary = discovery.summary();
assert!(summary.contains("2 stack components"));
assert!(summary.contains("trueno: v1.0.0"));
assert!(summary.contains("aprender: v0.5.0"));
}
#[test]
fn test_discovery_summary_empty() {
let discovery = StackDiscovery::new();
let summary = discovery.summary();
assert!(summary.contains("No Sovereign AI Stack components detected"));
}
#[test]
fn test_stack_component_all() {
let all = StackComponent::all();
assert_eq!(all.len(), 10);
let mut seen = std::collections::HashSet::new();
for component in all {
assert!(seen.insert(*component));
}
}
#[test]
fn test_stack_component_display() {
assert_eq!(format!("{}", StackComponent::Trueno), "trueno");
assert_eq!(format!("{}", StackComponent::TruenoGraph), "trueno-graph");
assert_eq!(format!("{}", StackComponent::TruenoRag), "trueno-rag");
assert_eq!(format!("{}", StackComponent::Entrenar), "entrenar");
assert_eq!(format!("{}", StackComponent::Realizar), "realizar");
assert_eq!(format!("{}", StackComponent::Alimentar), "alimentar");
assert_eq!(format!("{}", StackComponent::Pacha), "pacha");
assert_eq!(format!("{}", StackComponent::Renacer), "renacer");
}
#[test]
fn test_version_clone() {
let v = Version::parse("1.2.3-beta").unwrap();
let cloned = v.clone();
assert_eq!(cloned.major, v.major);
assert_eq!(cloned.pre_release, v.pre_release);
}
#[test]
fn test_version_eq() {
let v1 = Version::parse("1.2.3").unwrap();
let v2 = Version::parse("1.2.3").unwrap();
let v3 = Version::parse("1.2.4").unwrap();
assert_eq!(v1, v2);
assert_ne!(v1, v3);
}
#[test]
fn test_version_parse_invalid() {
assert!(Version::parse("").is_none());
assert!(Version::parse("1.2.3.4.5").is_none());
assert!(Version::parse("abc.def.ghi").is_none());
}
#[test]
fn test_version_satisfies_major_gt() {
let v_new = Version::parse("2.0.0").unwrap();
let v_old = Version::parse("1.5.0").unwrap();
assert!(v_new.satisfies_minimum(&v_old));
}
#[test]
fn test_version_satisfies_minor_gt() {
let v_new = Version::parse("1.5.0").unwrap();
let v_old = Version::parse("1.3.0").unwrap();
assert!(v_new.satisfies_minimum(&v_old));
}
#[test]
fn test_stack_component_clone_eq() {
let c1 = StackComponent::Trueno;
let c2 = c1.clone();
assert_eq!(c1, c2);
}
#[test]
fn test_stack_component_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(StackComponent::Trueno);
set.insert(StackComponent::Aprender);
set.insert(StackComponent::Trueno); assert_eq!(set.len(), 2);
}
#[test]
fn test_version_parse_with_tilde() {
let v = Version::parse("~1.2.3").unwrap();
assert_eq!(v.major, 1);
assert_eq!(v.minor, 2);
assert_eq!(v.patch, 3);
}
#[test]
fn test_version_parse_with_lt_gt() {
let v = Version::parse(">1.0.0").unwrap();
assert_eq!(v.major, 1);
let v = Version::parse("<2.0.0").unwrap();
assert_eq!(v.major, 2);
let v = Version::parse("<=3.0.0").unwrap();
assert_eq!(v.major, 3);
}
}