helen 0.1.0

Repository review gate.
Documentation
//! Environment-derived elenchus settings.

use super::{
    artifacts::RetainedAttemptCount,
    error::{ElenchusError, Result},
};
use std::env;

/// Default added plus deleted line budget for automatic elenchus review.
const DEFAULT_MAX_LINES: u64 = 2_000;

/// Environment-derived elenchus settings.
#[derive(Clone, Debug)]
pub(super) struct Settings {
    /// Codex CLI binary name or path.
    pub(super) codex_bin: String,
    /// Model used for read-only review.
    pub(super) review_model: String,
    /// Reasoning effort passed to Codex review.
    pub(super) effort: String,
    /// Maximum added plus deleted lines before automatic elenchus refuses.
    pub(super) max_lines: u64,
    /// Test command run through `bash -lc`.
    pub(super) test_cmd: String,
    /// Optional risk-acceptance token from a previous passing review.
    pub(super) risk_accepted_token: Option<String>,
    /// Test execution policy.
    pub(super) test_policy: TestPolicy,
    /// Reviewer transcript retention policy.
    pub(super) transcript_policy: TranscriptPolicy,
    /// Automatic commit policy for `main` and `master`.
    pub(super) main_branch_policy: MainBranchPolicy,
    /// Final commit policy.
    pub(super) commit_policy: CommitPolicy,
    /// Number of timestamped artifact attempts to keep before archiving older ones.
    pub(super) retained_attempts: RetainedAttemptCount,
}

impl Settings {
    /// Reads settings from the process environment.
    pub(super) fn from_env() -> Result<Self> {
        Ok(Self {
            codex_bin: env_value("ELENCHUS_CODEX_BIN").unwrap_or_else(|| String::from("codex")),
            review_model: env_value("ELENCHUS_REVIEW_MODEL")
                .unwrap_or_else(|| String::from("gpt-5.5")),
            effort: env_value("ELENCHUS_EFFORT").unwrap_or_else(|| String::from("high")),
            max_lines: env_value("ELENCHUS_MAX_LINES")
                .map(|value| {
                    value.parse::<u64>().map_err(|error| {
                        ElenchusError::usage(format!(
                            "error: ELENCHUS_MAX_LINES must be an integer: {error}"
                        ))
                    })
                })
                .transpose()?
                .unwrap_or(DEFAULT_MAX_LINES),
            test_cmd: env_value("ELENCHUS_TEST_CMD")
                .unwrap_or_else(|| String::from("cargo test --workspace --all-features")),
            risk_accepted_token: env_value("ELENCHUS_RISK_ACCEPTED")
                .filter(|value| !value.is_empty()),
            test_policy: TestPolicy::from_skip_flag(env_flag("ELENCHUS_SKIP_TESTS")),
            transcript_policy: TranscriptPolicy::from_keep_flag(env_flag(
                "ELENCHUS_KEEP_REVIEW_TRANSCRIPT",
            )),
            main_branch_policy: MainBranchPolicy::from_allow_flag(env_flag("ELENCHUS_ALLOW_MAIN")),
            commit_policy: CommitPolicy::from_dry_run_flag(env_flag("ELENCHUS_DRY_RUN")),
            retained_attempts: env_value("ELENCHUS_RETAIN_ATTEMPTS")
                .map(|value| {
                    let count = value.parse::<usize>().map_err(|error| {
                        ElenchusError::usage(format!(
                            "error: ELENCHUS_RETAIN_ATTEMPTS must be a positive integer: {error}"
                        ))
                    })?;
                    RetainedAttemptCount::new(count).ok_or_else(|| {
                        ElenchusError::usage(
                            "error: ELENCHUS_RETAIN_ATTEMPTS must be greater than zero",
                        )
                    })
                })
                .transpose()?
                .unwrap_or_else(RetainedAttemptCount::default_count),
        })
    }
}

/// Test execution policy.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum TestPolicy {
    /// Run the configured test command.
    Run,
    /// Skip tests after formatting and linting.
    Skip,
}

impl TestPolicy {
    /// Builds a policy from `ELENCHUS_SKIP_TESTS`.
    const fn from_skip_flag(skip: bool) -> Self {
        if skip { Self::Skip } else { Self::Run }
    }

    /// Returns true when tests should be skipped.
    pub(super) const fn skips_tests(self) -> bool {
        matches!(self, Self::Skip)
    }
}

/// Reviewer transcript retention policy.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum TranscriptPolicy {
    /// Keep raw reviewer stdout and stderr.
    KeepRaw,
    /// Replace raw reviewer stdout and stderr with a concise artifact.
    DiscardRaw,
}

impl TranscriptPolicy {
    /// Builds a policy from `ELENCHUS_KEEP_REVIEW_TRANSCRIPT`.
    const fn from_keep_flag(keep: bool) -> Self {
        if keep {
            Self::KeepRaw
        } else {
            Self::DiscardRaw
        }
    }

    /// Returns true when raw reviewer output should be kept.
    pub(super) const fn keeps_raw(self) -> bool {
        matches!(self, Self::KeepRaw)
    }
}

/// Automatic commit policy for protected branch names.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum MainBranchPolicy {
    /// Refuse automatic commits on `main` and `master`.
    Refuse,
    /// Allow automatic commits on `main` and `master`.
    Allow,
}

impl MainBranchPolicy {
    /// Builds a policy from `ELENCHUS_ALLOW_MAIN`.
    const fn from_allow_flag(allow: bool) -> Self {
        if allow { Self::Allow } else { Self::Refuse }
    }

    /// Returns true when automatic commits on `main` and `master` are allowed.
    pub(super) const fn allows_main(self) -> bool {
        matches!(self, Self::Allow)
    }
}

/// Final elenchus commit policy.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum CommitPolicy {
    /// Commit after all gates pass.
    Commit,
    /// Run all gates and leave the worktree uncommitted.
    DryRun,
}

impl CommitPolicy {
    /// Builds a policy from `ELENCHUS_DRY_RUN`.
    const fn from_dry_run_flag(dry_run: bool) -> Self {
        if dry_run { Self::DryRun } else { Self::Commit }
    }

    /// Returns true when the gate should stop before committing.
    pub(super) const fn is_dry_run(self) -> bool {
        matches!(self, Self::DryRun)
    }
}

/// Returns an environment variable when it is present and UTF-8.
fn env_value(name: &str) -> Option<String> {
    env::var(name).ok()
}

/// Returns true when an environment flag is exactly `1`.
fn env_flag(name: &str) -> bool {
    env::var(name).is_ok_and(|value| value == "1")
}