governor-application 2.0.3

Application use cases and machine contracts for cargo-governor
Documentation
//! Agent-facing repository context.

use serde::Serialize;

/// Safe or mutating command class.
#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CommandSafety {
    /// Read-only command.
    Safe,
    /// Mutating command.
    Mutating,
    /// Mutating command gated by dry-run or confirmation.
    Gated,
}

/// Single command documented for coding agents.
#[derive(Debug, Clone, Serialize)]
pub struct AgentCommand {
    /// Canonical CLI invocation.
    pub command: String,
    /// Purpose of the command.
    pub description: String,
    /// Safety category.
    pub safety: CommandSafety,
}

/// Exit code documentation for agents.
#[derive(Debug, Clone, Serialize)]
pub struct ExitCodeInfo {
    /// Numeric process exit code.
    pub code: u8,
    /// Stable symbolic name.
    pub name: String,
    /// Operational meaning.
    pub meaning: String,
}

/// Machine-readable repository context for coding agents.
#[derive(Debug, Clone, Serialize)]
pub struct AgentContext {
    /// Stable schema version for the context payload.
    pub schema_version: u32,
    /// Repository identifier.
    pub repository: String,
    /// Tool this repo uses for releases.
    pub release_tool: String,
    /// How an agent can detect the tool.
    pub detection_hints: Vec<String>,
    /// Read-only commands agents can run freely.
    pub safe_commands: Vec<AgentCommand>,
    /// Mutating commands with guidance.
    pub gated_commands: Vec<AgentCommand>,
    /// Important release guarantees and caveats.
    pub release_policy: Vec<String>,
    /// Important owners-management guarantees and caveats.
    pub owners_policy: Vec<String>,
    /// Recommended dry-run workflow for agents.
    pub dry_run_workflow: Vec<String>,
    /// Important exit codes.
    pub exit_codes: Vec<ExitCodeInfo>,
    /// Schema documents an agent can fetch.
    pub schema_paths: Vec<String>,
}

impl AgentContext {
    fn push_wrapped_bullet(output: &mut String, text: &str) {
        const WIDTH: usize = 78;
        let mut line = String::from("- ");

        for word in text.split_whitespace() {
            let separator = if line == "- " { "" } else { " " };
            if line.len() + separator.len() + word.len() > WIDTH {
                output.push_str(&line);
                output.push('\n');
                line = format!("  {word}");
            } else {
                line.push_str(separator);
                line.push_str(word);
            }
        }

        output.push_str(&line);
        output.push('\n');
    }

    fn detection_hints() -> Vec<String> {
        vec![
            "Look for a `cargo-governor` binary crate in `crates/`.".to_string(),
            "Look for `workspace.metadata.governor` in the root `Cargo.toml`.".to_string(),
            "Look for repo docs mentioning `cargo-governor release ...`.".to_string(),
        ]
    }

    fn safe_commands() -> Vec<AgentCommand> {
        vec![
            AgentCommand {
                command: "cargo-governor agent context --format json".to_string(),
                description: "Load machine-readable release and safety context.".to_string(),
                safety: CommandSafety::Safe,
            },
            AgentCommand {
                command: "cargo-governor release analyze --format json".to_string(),
                description: "Analyze commits and proposed semantic version bump.".to_string(),
                safety: CommandSafety::Safe,
            },
            AgentCommand {
                command: "cargo-governor --dry-run release plan --format json".to_string(),
                description: "Inspect publication order and already-published crates.".to_string(),
                safety: CommandSafety::Safe,
            },
            AgentCommand {
                command: "cargo-governor release status --format json".to_string(),
                description: "Read branch, tag and workspace release status.".to_string(),
                safety: CommandSafety::Safe,
            },
            AgentCommand {
                command: "cargo-governor --dry-run release check --format json".to_string(),
                description: "Run release checks without mutating the workspace.".to_string(),
                safety: CommandSafety::Safe,
            },
        ]
    }

    fn gated_commands() -> Vec<AgentCommand> {
        vec![
            AgentCommand {
                command: "cargo-governor --dry-run release bump --format json".to_string(),
                description: "Preview version/changelog/git effects before mutation.".to_string(),
                safety: CommandSafety::Gated,
            },
            AgentCommand {
                command: "cargo-governor --dry-run release publish --format json".to_string(),
                description: "Preview publish decisions and registry skips.".to_string(),
                safety: CommandSafety::Gated,
            },
            AgentCommand {
                command: "cargo-governor --dry-run release full --format json".to_string(),
                description: "Preview the end-to-end release workflow.".to_string(),
                safety: CommandSafety::Gated,
            },
            AgentCommand {
                command: "cargo-governor owners sync --dry-run".to_string(),
                description: "Preview crates.io owner changes before applying them.".to_string(),
                safety: CommandSafety::Gated,
            },
        ]
    }

    fn release_policy() -> Vec<String> {
        vec![
            "Normal UX: make code changes, commit them with Conventional Commits, then let `cargo-governor release full` do the version bump, changelog, tag, and publish."
                .to_string(),
            "Do not pre-bump versions in `Cargo.toml`; cargo-governor expects the workspace version to match the last release tag before a new release starts."
                .to_string(),
            "Mutating release commands expect a clean working tree so the release commit only contains cargo-governor-managed changes."
                .to_string(),
            "Prefer `analyze`, `plan`, `status`, and `check` before any mutating release step."
                .to_string(),
            "Use global `--dry-run` for release commands when an agent is exploring impact."
                .to_string(),
            "Treat `release bump`, `release publish`, `release full`, and `owners sync` as mutating commands.".to_string(),
            "Interpret non-zero exit codes as policy failures, not only process crashes."
                .to_string(),
        ]
    }

    fn owners_policy() -> Vec<String> {
        vec![
            "Resolve owners from workspace and package metadata before checking crates.io."
                .to_string(),
            "Use `owners show` or `owners check` before `owners sync`.".to_string(),
            "Treat `owners sync` as gated even in automated environments.".to_string(),
        ]
    }

    fn dry_run_workflow() -> Vec<String> {
        vec![
            "Run `cargo-governor agent context --format json` to discover policies and schemas."
                .to_string(),
            "Run `cargo-governor release status --format json` to confirm the tree is clean and the workspace version still matches the last release tag."
                .to_string(),
            "Run `cargo-governor release analyze --format json` to determine semantic impact."
                .to_string(),
            "Run `cargo-governor --dry-run release plan --format json` to inspect order and skips."
                .to_string(),
            "Run `cargo-governor --dry-run release full --format json` before any mutating release."
                .to_string(),
        ]
    }

    fn exit_codes() -> Vec<ExitCodeInfo> {
        vec![
            ExitCodeInfo {
                code: 0,
                name: "success".to_string(),
                meaning: "Command completed successfully.".to_string(),
            },
            ExitCodeInfo {
                code: 2,
                name: "invalid_arguments".to_string(),
                meaning: "CLI or policy arguments were invalid.".to_string(),
            },
            ExitCodeInfo {
                code: 10,
                name: "git_error".to_string(),
                meaning: "Source control state blocked the workflow.".to_string(),
            },
            ExitCodeInfo {
                code: 11,
                name: "registry_error".to_string(),
                meaning: "Registry access or publish checks failed.".to_string(),
            },
            ExitCodeInfo {
                code: 13,
                name: "check_failed".to_string(),
                meaning: "Pre-release checks failed.".to_string(),
            },
            ExitCodeInfo {
                code: 20,
                name: "partial_success".to_string(),
                meaning: "The workflow completed with partial failures.".to_string(),
            },
            ExitCodeInfo {
                code: 32,
                name: "drift_detected".to_string(),
                meaning: "Owner drift was detected and requires action.".to_string(),
            },
        ]
    }

    /// Build the default context for cargo-governor repositories.
    #[must_use]
    pub fn cargo_governor() -> Self {
        Self {
            schema_version: 2,
            repository: "cargo-governor".to_string(),
            release_tool: "cargo-governor".to_string(),
            detection_hints: Self::detection_hints(),
            safe_commands: Self::safe_commands(),
            gated_commands: Self::gated_commands(),
            release_policy: Self::release_policy(),
            owners_policy: Self::owners_policy(),
            dry_run_workflow: Self::dry_run_workflow(),
            exit_codes: Self::exit_codes(),
            schema_paths: vec![
                "schemas/agent-context.schema.json".to_string(),
                "schemas/cli-envelope.schema.json".to_string(),
                "schemas/mcp-tools.schema.json".to_string(),
            ],
        }
    }

    /// Render the context in markdown form.
    #[must_use]
    pub fn to_markdown(&self) -> String {
        let mut output = String::new();
        output.push_str("## Agent Context\n\n");
        output.push_str("Use `cargo-governor` for release and owners workflows.\n\n");
        output.push_str("## Detect\n");
        for hint in &self.detection_hints {
            Self::push_wrapped_bullet(&mut output, hint);
        }
        output.push_str("\n## Safe commands\n");
        for command in &self.safe_commands {
            Self::push_wrapped_bullet(
                &mut output,
                &format!("`{}`: {}", command.command, command.description),
            );
        }
        output.push_str("\n## Gated commands\n");
        for command in &self.gated_commands {
            Self::push_wrapped_bullet(
                &mut output,
                &format!("`{}`: {}", command.command, command.description),
            );
        }
        output.push_str("\n## Release policy\n");
        for rule in &self.release_policy {
            Self::push_wrapped_bullet(&mut output, rule);
        }
        output.push_str("\n## Owners policy\n");
        for rule in &self.owners_policy {
            Self::push_wrapped_bullet(&mut output, rule);
        }
        output.push_str("\n## Dry-run workflow\n");
        for step in &self.dry_run_workflow {
            Self::push_wrapped_bullet(&mut output, step);
        }
        output.push_str("\n## Schemas\n");
        for path in &self.schema_paths {
            Self::push_wrapped_bullet(&mut output, &format!("`{path}`"));
        }
        output
    }
}