ito-core 0.1.33

Core functionality and business logic for Ito
Documentation
//! Rules under the `audit/*` namespace.
//!
//! Two rules live here:
//!
//! - `audit/mirror-branch-set` — when audit mirroring is enabled, the
//!   configured mirror branch must be non-empty and SHOULD live under
//!   `ito/internal/`.
//! - `audit/mirror-branch-distinct-from-coordination` — the mirror branch
//!   must not collide with the coordination branch when both worktree-based
//!   features are enabled.

#[cfg(feature = "coordination-branch")]
use ito_config::types::CoordinationStorage;
use ito_config::types::ItoConfig;

use crate::errors::CoreError;
#[cfg(feature = "coordination-branch")]
use crate::validate::error;
use crate::validate::{ValidationIssue, warning, with_metadata, with_rule_id};

use super::rule::{Rule, RuleContext, RuleId, RuleSeverity};

const MIRROR_BRANCH_SET_ID: RuleId = RuleId::new("audit/mirror-branch-set");
#[cfg(feature = "coordination-branch")]
const MIRROR_BRANCH_DISTINCT_ID: RuleId =
    RuleId::new("audit/mirror-branch-distinct-from-coordination");

const ITO_INTERNAL_PREFIX: &str = "ito/internal/";

/// True when coordination storage is `worktree`.
#[cfg(feature = "coordination-branch")]
fn coordination_is_worktree(config: &ItoConfig) -> bool {
    match config.changes.coordination_branch.storage {
        CoordinationStorage::Worktree => true,
        CoordinationStorage::Embedded => false,
    }
}

// ── audit/mirror-branch-set ──────────────────────────────────────────────

/// `audit/mirror-branch-set` — mirror branch must be non-empty and follow
/// the `ito/internal/` convention.
pub(crate) struct MirrorBranchSetRule;

impl Rule for MirrorBranchSetRule {
    fn id(&self) -> RuleId {
        MIRROR_BRANCH_SET_ID
    }

    fn severity(&self) -> RuleSeverity {
        RuleSeverity::Warning
    }

    fn description(&self) -> &'static str {
        "Audit mirror branch is non-empty and follows the `ito/internal/` convention."
    }

    fn gate(&self) -> Option<&'static str> {
        Some("audit.mirror.enabled == true")
    }

    fn is_active(&self, config: &ItoConfig) -> bool {
        config.audit.mirror.enabled
    }

    fn check(&self, ctx: &RuleContext<'_>) -> Result<Vec<ValidationIssue>, CoreError> {
        let branch = ctx.config.audit.mirror.branch.trim();
        let mut issues = Vec::new();

        if branch.is_empty() {
            let issue = warning(
                ".ito/config.json",
                "`audit.mirror.branch` is empty. \
                 Without a branch name the audit subsystem cannot push mirror \
                 commits and the mirror feature is effectively disabled.",
            );
            let issue = with_rule_id(issue, MIRROR_BRANCH_SET_ID.as_str());
            issues.push(with_metadata(
                issue,
                serde_json::json!({
                    "fix": format!(
                        "Set `audit.mirror.branch` to a value under `{ITO_INTERNAL_PREFIX}` (for example `{ITO_INTERNAL_PREFIX}audit`)."
                    ),
                    "config_key": "audit.mirror.branch",
                }),
            ));
            return Ok(issues);
        }

        if !branch.starts_with(ITO_INTERNAL_PREFIX) {
            let issue = warning(
                ".ito/config.json",
                format!(
                    "`audit.mirror.branch = \"{branch}\"` does not follow the \
                     `{ITO_INTERNAL_PREFIX}*` convention.",
                ),
            );
            let issue = with_rule_id(issue, MIRROR_BRANCH_SET_ID.as_str());
            issues.push(with_metadata(
                issue,
                serde_json::json!({
                    "fix": format!(
                        "Rename the branch under `{ITO_INTERNAL_PREFIX}` (for example \
                         `{ITO_INTERNAL_PREFIX}audit`) to keep audit mirrors in the \
                         workspace's internal namespace."
                    ),
                    "current": branch,
                }),
            ));
        }

        Ok(issues)
    }
}

// ── audit/mirror-branch-distinct-from-coordination ───────────────────────

/// `audit/mirror-branch-distinct-from-coordination` — the audit mirror
/// branch must not be the same as the coordination branch.
#[cfg(feature = "coordination-branch")]
pub(crate) struct MirrorBranchDistinctRule;

#[cfg(feature = "coordination-branch")]
impl Rule for MirrorBranchDistinctRule {
    fn id(&self) -> RuleId {
        MIRROR_BRANCH_DISTINCT_ID
    }

    fn severity(&self) -> RuleSeverity {
        RuleSeverity::Error
    }

    fn description(&self) -> &'static str {
        "Audit mirror branch is distinct from the coordination branch."
    }

    fn gate(&self) -> Option<&'static str> {
        Some("audit.mirror.enabled == true && changes.coordination_branch.storage == worktree")
    }

    fn is_active(&self, config: &ItoConfig) -> bool {
        config.audit.mirror.enabled && coordination_is_worktree(config)
    }

    fn check(&self, ctx: &RuleContext<'_>) -> Result<Vec<ValidationIssue>, CoreError> {
        let mirror = ctx.config.audit.mirror.branch.trim();
        let coord = ctx.config.changes.coordination_branch.name.trim();

        if mirror.is_empty() || coord.is_empty() || mirror != coord {
            return Ok(Vec::new());
        }

        let issue = error(
            ".ito/config.json",
            format!(
                "`audit.mirror.branch` and `changes.coordination_branch.name` are both \
                 set to `{mirror}`. A single git branch cannot store both audit events \
                 and coordination state — pushes from each subsystem would clobber the \
                 other.",
            ),
        );
        let issue = with_rule_id(issue, MIRROR_BRANCH_DISTINCT_ID.as_str());
        let issue = with_metadata(
            issue,
            serde_json::json!({
                "fix": format!(
                    "Use distinct branch names — for example `{ITO_INTERNAL_PREFIX}changes` \
                     for coordination and `{ITO_INTERNAL_PREFIX}audit` for the mirror."
                ),
                "shared_branch": mirror,
            }),
        );
        Ok(vec![issue])
    }
}

#[cfg(test)]
#[path = "audit_rules_tests.rs"]
mod audit_rules_tests;