#[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/";
#[cfg(feature = "coordination-branch")]
fn coordination_is_worktree(config: &ItoConfig) -> bool {
match config.changes.coordination_branch.storage {
CoordinationStorage::Worktree => true,
CoordinationStorage::Embedded => false,
}
}
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)
}
}
#[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;