agentic-forge-core 0.1.0

Blueprint engine for complete project architecture before code generation
Documentation
//! Tier 5: Dependency inventions.
//! DependencyInferrer, VersionResolver, ApiSpecExtractor, ConflictResolver

use crate::types::blueprint::*;
use crate::types::intent::*;

pub struct DependencyInferrer;

impl DependencyInferrer {
    pub fn infer(
        domain: Domain,
        entities: &[Entity],
        constraints: &[Constraint],
    ) -> Vec<Dependency> {
        let mut deps = Vec::new();

        // Core deps for Rust
        deps.push(Dependency::new("serde", "1.0"));
        deps.push(Dependency::new("serde_json", "1.0"));
        deps.push(Dependency::new("thiserror", "2.0"));

        match domain {
            Domain::Web | Domain::Api => {
                deps.push(Dependency::new("axum", "0.7"));
                deps.push(Dependency::new("tokio", "1.35"));
                deps.push(Dependency::new("tower", "0.4"));
                deps.push(Dependency::new("tower-http", "0.5"));
                deps.push(Dependency::new("tracing", "0.1"));
            }
            Domain::Cli => {
                deps.push(Dependency::new("clap", "4.4"));
                deps.push(Dependency::new("anyhow", "1.0"));
            }
            Domain::Library => {
                deps.push(Dependency::new("tracing", "0.1"));
            }
            Domain::Service => {
                deps.push(Dependency::new("tokio", "1.35"));
                deps.push(Dependency::new("tracing", "0.1"));
            }
            _ => {}
        }

        if !entities.is_empty() {
            deps.push(Dependency::new("uuid", "1.6"));
            deps.push(Dependency::new("chrono", "0.4"));
        }

        for constraint in constraints {
            if matches!(constraint.constraint_type, ConstraintType::Security(_)) {
                deps.push(Dependency::new("argon2", "0.5"));
                deps.push(Dependency::new("jsonwebtoken", "9.2"));
            }
        }

        deps
    }

    pub fn name() -> &'static str {
        "DependencyInferrer"
    }
    pub fn tier() -> u8 {
        5
    }
}

pub struct VersionResolver;

impl VersionResolver {
    pub fn resolve(deps: &[Dependency]) -> Vec<ResolvedDependency> {
        deps.iter()
            .map(|d| {
                let compatible = Self::check_compatibility(&d.name, &d.version);
                ResolvedDependency {
                    name: d.name.clone(),
                    requested_version: d.version.clone(),
                    resolved_version: d.version.clone(),
                    is_compatible: compatible,
                    conflicts: Vec::new(),
                }
            })
            .collect()
    }

    fn check_compatibility(name: &str, version: &str) -> bool {
        let major: u32 = version
            .split('.')
            .next()
            .and_then(|v| v.parse().ok())
            .unwrap_or(0);
        match name {
            "serde" => major >= 1,
            "tokio" => major >= 1,
            "axum" => version.starts_with("0.7") || major >= 1,
            _ => true,
        }
    }

    pub fn name() -> &'static str {
        "VersionResolver"
    }
    pub fn tier() -> u8 {
        5
    }
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ResolvedDependency {
    pub name: String,
    pub requested_version: String,
    pub resolved_version: String,
    pub is_compatible: bool,
    pub conflicts: Vec<String>,
}

pub struct ApiSpecExtractor;

impl ApiSpecExtractor {
    pub fn extract(entities: &[Entity], domain: Domain) -> Vec<ApiEndpoint> {
        if !matches!(domain, Domain::Web | Domain::Api | Domain::Service) {
            return Vec::new();
        }

        let mut endpoints = Vec::new();
        for entity in entities {
            let base = format!("/api/{}", entity.name.to_lowercase());
            endpoints.push(ApiEndpoint {
                method: "GET".into(),
                path: base.clone(),
                description: format!("List all {}s", entity.name.to_lowercase()),
                request_body: None,
                response_type: format!("Vec<{}>", entity.name),
            });
            endpoints.push(ApiEndpoint {
                method: "POST".into(),
                path: base.clone(),
                description: format!("Create a {}", entity.name.to_lowercase()),
                request_body: Some(format!("Create{}Input", entity.name)),
                response_type: entity.name.clone(),
            });
            endpoints.push(ApiEndpoint {
                method: "GET".into(),
                path: format!("{}/:id", base),
                description: format!("Get {} by ID", entity.name.to_lowercase()),
                request_body: None,
                response_type: entity.name.clone(),
            });
            endpoints.push(ApiEndpoint {
                method: "PUT".into(),
                path: format!("{}/:id", base),
                description: format!("Update {}", entity.name.to_lowercase()),
                request_body: Some(format!("Update{}Input", entity.name)),
                response_type: entity.name.clone(),
            });
            endpoints.push(ApiEndpoint {
                method: "DELETE".into(),
                path: format!("{}/:id", base),
                description: format!("Delete {}", entity.name.to_lowercase()),
                request_body: None,
                response_type: "()".into(),
            });
        }
        endpoints
    }

    pub fn name() -> &'static str {
        "ApiSpecExtractor"
    }
    pub fn tier() -> u8 {
        5
    }
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ApiEndpoint {
    pub method: String,
    pub path: String,
    pub description: String,
    pub request_body: Option<String>,
    pub response_type: String,
}

pub struct ConflictResolver;

impl ConflictResolver {
    pub fn resolve(deps: &[Dependency]) -> Vec<DependencyConflict> {
        let mut conflicts = Vec::new();
        let mut seen: std::collections::HashMap<String, Vec<&Dependency>> =
            std::collections::HashMap::new();

        for dep in deps {
            seen.entry(dep.name.clone()).or_default().push(dep);
        }

        for (name, versions) in &seen {
            if versions.len() > 1 {
                let version_strs: Vec<&str> = versions.iter().map(|d| d.version.as_str()).collect();
                conflicts.push(DependencyConflict {
                    dependency: name.clone(),
                    versions: version_strs.iter().map(|v| v.to_string()).collect(),
                    resolution: format!("Use latest: {}", version_strs.last().unwrap()),
                });
            }
        }

        conflicts
    }

    pub fn name() -> &'static str {
        "ConflictResolver"
    }
    pub fn tier() -> u8 {
        5
    }
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct DependencyConflict {
    pub dependency: String,
    pub versions: Vec<String>,
    pub resolution: String,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_dependency_inferrer_api() {
        let deps = DependencyInferrer::infer(Domain::Api, &[], &[]);
        assert!(deps.iter().any(|d| d.name == "axum"));
        assert!(deps.iter().any(|d| d.name == "tokio"));
        assert!(deps.iter().any(|d| d.name == "serde"));
    }

    #[test]
    fn test_dependency_inferrer_cli() {
        let deps = DependencyInferrer::infer(Domain::Cli, &[], &[]);
        assert!(deps.iter().any(|d| d.name == "clap"));
        assert!(!deps.iter().any(|d| d.name == "axum"));
    }

    #[test]
    fn test_dependency_inferrer_with_entities() {
        let entities = vec![Entity::new("User", "A user")];
        let deps = DependencyInferrer::infer(Domain::Api, &entities, &[]);
        assert!(deps.iter().any(|d| d.name == "uuid"));
        assert!(deps.iter().any(|d| d.name == "chrono"));
    }

    #[test]
    fn test_dependency_inferrer_security() {
        let constraints = vec![Constraint::new(
            "auth",
            ConstraintType::Security("jwt".into()),
            "JWT auth",
        )];
        let deps = DependencyInferrer::infer(Domain::Api, &[], &constraints);
        assert!(deps.iter().any(|d| d.name == "jsonwebtoken"));
    }

    #[test]
    fn test_version_resolver() {
        let deps = vec![
            Dependency::new("serde", "1.0"),
            Dependency::new("tokio", "1.35"),
        ];
        let resolved = VersionResolver::resolve(&deps);
        assert!(resolved.iter().all(|r| r.is_compatible));
    }

    #[test]
    fn test_api_spec_extractor() {
        let entities = vec![Entity::new("User", "A user")];
        let endpoints = ApiSpecExtractor::extract(&entities, Domain::Api);
        assert_eq!(endpoints.len(), 5);
        assert!(endpoints.iter().any(|e| e.method == "POST"));
    }

    #[test]
    fn test_api_spec_extractor_non_api() {
        let entities = vec![Entity::new("User", "A user")];
        let endpoints = ApiSpecExtractor::extract(&entities, Domain::Library);
        assert!(endpoints.is_empty());
    }

    #[test]
    fn test_conflict_resolver_no_conflicts() {
        let deps = vec![
            Dependency::new("serde", "1.0"),
            Dependency::new("tokio", "1.35"),
        ];
        let conflicts = ConflictResolver::resolve(&deps);
        assert!(conflicts.is_empty());
    }

    #[test]
    fn test_conflict_resolver_with_conflicts() {
        let deps = vec![
            Dependency::new("serde", "1.0"),
            Dependency::new("serde", "2.0"),
        ];
        let conflicts = ConflictResolver::resolve(&deps);
        assert_eq!(conflicts.len(), 1);
        assert_eq!(conflicts[0].dependency, "serde");
    }

    #[test]
    fn test_invention_metadata() {
        assert_eq!(DependencyInferrer::name(), "DependencyInferrer");
        assert_eq!(VersionResolver::name(), "VersionResolver");
        assert_eq!(ApiSpecExtractor::name(), "ApiSpecExtractor");
        assert_eq!(ConflictResolver::name(), "ConflictResolver");
    }
}