cargo-governor 2.0.3

Machine-First, LLM-Ready, CI/CD-Native release automation tool for Rust crates
Documentation
//! Plan service - business logic for publication planning

pub mod graph;
pub mod metadata;
pub mod order;
pub mod version;

pub use graph::{build_dependency_graph, get_graph_stats, get_publish_order};
pub use order::{build_batches, build_publication_order, get_skipped_crates};
pub use version::analyze_versions;

use crate::cli::{OutputFormat, PlanOpts};
use crate::error::{CommandExitCode, Result};

// Import metadata functions for internal use
use metadata::parse_cargo_metadata;
use serde_json::json;

/// Service for generating publication plans
pub struct PlanService {
    workspace_path: String,
    opts: PlanOpts,
}

impl PlanService {
    pub const fn new(workspace_path: String, opts: PlanOpts) -> Self {
        Self {
            workspace_path,
            opts,
        }
    }

    pub async fn execute(&self, _format: OutputFormat) -> Result<CommandExitCode> {
        let start_time = std::time::Instant::now();

        let (metadata, packages) = parse_cargo_metadata(&self.workspace_path)?;
        let dep_graph = build_dependency_graph(&packages);
        let has_cycles = dep_graph.has_cycles();
        let publish_order = get_publish_order(&dep_graph, has_cycles);
        let (bump_type, current, new_version) = analyze_versions(&self.workspace_path).await?;
        let publication_order = build_publication_order(&publish_order, &new_version, &dep_graph);
        let crates_to_publish = publication_order.len();
        let total_estimated_time = crates_to_publish * 45;
        let batches = build_batches(&publish_order);
        let skipped_crates = get_skipped_crates(self.opts.include_published, &new_version);
        let (nodes, edges, cycles) = get_graph_stats(&dep_graph, has_cycles);

        let workspace_name = metadata
            .workspace_root
            .file_name()
            .map_or_else(|| "workspace".to_string(), std::string::ToString::to_string);

        let response = json!({
            "success": !has_cycles,
            "command": "plan",
            "workspace": workspace_name,
            "result": {
                "version_analysis": {
                    "current": current.to_string(),
                    "new": new_version.to_string(),
                    "bump": serde_json::to_value(bump_type).unwrap(),
                },
                "dependency_graph": {
                    "nodes": nodes,
                    "edges": edges,
                    "cycles": cycles,
                },
                "publication_order": publication_order,
                "crates_to_publish": crates_to_publish,
                "crates_to_skip": skipped_crates.len(),
                "skipped_crates": skipped_crates,
                "total_estimated_time_sec": total_estimated_time,
                "batches": batches,
            },
            "metrics": {
                "execution_time_ms": start_time.elapsed().as_millis(),
                "git_operations": 1,
                "api_calls": 0,
            }
        });

        println!("{}", serde_json::to_string_pretty(&response).unwrap());

        if has_cycles {
            Ok(CommandExitCode::GeneralError)
        } else {
            Ok(CommandExitCode::Success)
        }
    }
}