use std::path::PathBuf;
use std::sync::Arc;
use crate::resource_checker::{ConflictCheck, ResourceChecker};
use crate::state_model::{StateModelProposedOperation, StateSnapshot, ThreeStateModel};
use crate::validation_loop::ValidationSeverity;
type ProposedOperation = StateModelProposedOperation;
pub struct ValidationAgent {
state_model: Arc<ThreeStateModel>,
resource_checker: Arc<ResourceChecker>,
rules: Vec<ValidationRule>,
}
impl ValidationAgent {
pub fn new(state_model: Arc<ThreeStateModel>, resource_checker: Arc<ResourceChecker>) -> Self {
let mut agent = Self {
state_model,
resource_checker,
rules: Vec::new(),
};
agent.register_default_rules();
agent
}
fn register_default_rules(&mut self) {
self.rules.push(ValidationRule {
name: "file_exists_for_edit".into(),
description: "File must exist before editing".into(),
rule_type: RuleType::PreCondition,
check: Box::new(|ctx| {
for resource in &ctx.operation.resources_needed {
if resource.starts_with('/') || resource.contains('/') {
let path = PathBuf::from(resource);
if let Some(file_status) = ctx.current_state.files.get(&path)
&& !file_status.exists
{
return ValidationOutcome {
passed: false,
rule_name: "file_exists_for_edit".into(),
message: format!("File does not exist: {}", resource),
severity: ValidationSeverity::Warning,
};
}
}
}
ValidationOutcome::pass("file_exists_for_edit")
}),
});
self.rules.push(ValidationRule {
name: "no_conflicting_locks".into(),
description: "No other agent holds conflicting lock".into(),
rule_type: RuleType::PreCondition,
check: Box::new(|ctx| {
for resource in &ctx.operation.resources_needed {
if let Some(holder) = ctx.current_state.locks.get(resource)
&& holder != &ctx.agent_id
{
return ValidationOutcome {
passed: false,
rule_name: "no_conflicting_locks".into(),
message: format!(
"Resource '{}' is locked by agent '{}'",
resource, holder
),
severity: ValidationSeverity::Error,
};
}
}
ValidationOutcome::pass("no_conflicting_locks")
}),
});
self.rules.push(ValidationRule {
name: "no_git_conflicts".into(),
description: "Git working tree must not have conflicts".into(),
rule_type: RuleType::PreCondition,
check: Box::new(|ctx| {
if (ctx.operation.operation_type.starts_with("git_")
|| ctx.operation.operation_type == "commit"
|| ctx.operation.operation_type == "push")
&& ctx.current_state.git_state.has_conflicts
{
return ValidationOutcome {
passed: false,
rule_name: "no_git_conflicts".into(),
message: "Git working tree has unresolved conflicts".into(),
severity: ValidationSeverity::Error,
};
}
ValidationOutcome::pass("no_git_conflicts")
}),
});
self.rules.push(ValidationRule {
name: "artifacts_invalidated_after_edit".into(),
description: "Build artifacts should be marked invalid after source edit".into(),
rule_type: RuleType::PostCondition,
check: Box::new(|ctx| {
if ctx.operation.operation_type == "file_write"
|| ctx.operation.operation_type == "file_edit"
{
for resource in &ctx.operation.resources_produced {
if let Some(file_status) =
ctx.current_state.files.get(&PathBuf::from(resource))
&& !file_status.dirty
{
return ValidationOutcome {
passed: false,
rule_name: "artifacts_invalidated_after_edit".into(),
message: format!(
"File '{}' should be marked dirty after edit",
resource
),
severity: ValidationSeverity::Warning,
};
}
}
}
ValidationOutcome::pass("artifacts_invalidated_after_edit")
}),
});
self.rules.push(ValidationRule {
name: "files_clean_after_build".into(),
description: "Source files should be marked clean after successful build".into(),
rule_type: RuleType::PostCondition,
check: Box::new(|ctx| {
if ctx.operation.operation_type == "build" {
}
ValidationOutcome::pass("files_clean_after_build")
}),
});
self.rules.push(ValidationRule {
name: "no_deadlock".into(),
description: "Resource acquisition must not cause deadlock".into(),
rule_type: RuleType::Invariant,
check: Box::new(|ctx| {
let our_locks: Vec<_> = ctx
.current_state
.locks
.iter()
.filter(|(_, holder)| *holder == &ctx.agent_id)
.map(|(resource, _)| resource.clone())
.collect();
if !our_locks.is_empty() && !ctx.operation.resources_needed.is_empty() {
for other_agent in &ctx.other_agents {
let their_resources: std::collections::HashSet<_> =
other_agent.held_resources.iter().collect();
let we_need: std::collections::HashSet<_> =
ctx.operation.resources_needed.iter().collect();
let overlap: Vec<_> = their_resources.intersection(&we_need).collect();
if !overlap.is_empty() {
if let Some(ref waiting_for) = other_agent.waiting_for
&& our_locks.contains(waiting_for) {
return ValidationOutcome {
passed: false,
rule_name: "no_deadlock".into(),
message: format!(
"Potential deadlock: agent '{}' holds {:?} (we need it) and waits for '{}' (we hold it)",
other_agent.agent_id,
overlap,
waiting_for
),
severity: ValidationSeverity::Error,
};
}
}
}
}
ValidationOutcome::pass("no_deadlock")
}),
});
self.rules.push(ValidationRule {
name: "git_coordination".into(),
description: "Git operations must not conflict across agents".into(),
rule_type: RuleType::InterAgent,
check: Box::new(|ctx| {
if ctx.operation.operation_type.starts_with("git_") {
for other_agent in &ctx.other_agents {
if let Some(ref other_op) = other_agent.current_operation
&& other_op.starts_with("git_")
{
let conflicting_ops = [
"git_commit",
"git_push",
"git_pull",
"git_merge",
"git_rebase",
"git_checkout",
"git_branch_create",
"git_branch_delete",
];
let our_op = &ctx.operation.operation_type;
if conflicting_ops.contains(&our_op.as_str())
&& conflicting_ops.contains(&other_op.as_str())
{
return ValidationOutcome {
passed: false,
rule_name: "git_coordination".into(),
message: format!(
"Git operation '{}' conflicts with '{}' by agent '{}'",
our_op, other_op, other_agent.agent_id
),
severity: ValidationSeverity::Error,
};
}
}
}
}
ValidationOutcome::pass("git_coordination")
}),
});
self.rules.push(ValidationRule {
name: "build_coordination".into(),
description: "Build operations should not run concurrently on same project".into(),
rule_type: RuleType::InterAgent,
check: Box::new(|ctx| {
if ctx.operation.operation_type == "build" || ctx.operation.operation_type == "test"
{
for other_agent in &ctx.other_agents {
if let Some(ref other_op) = other_agent.current_operation
&& (other_op == "build" || other_op == "test")
{
let our_resources: std::collections::HashSet<_> =
ctx.operation.resources_needed.iter().collect();
let their_resources: std::collections::HashSet<_> =
other_agent.held_resources.iter().collect();
if !our_resources.is_disjoint(&their_resources) {
return ValidationOutcome {
passed: false,
rule_name: "build_coordination".into(),
message: format!(
"Build/test operation conflicts with '{}' by agent '{}'",
other_op, other_agent.agent_id
),
severity: ValidationSeverity::Error,
};
}
}
}
}
ValidationOutcome::pass("build_coordination")
}),
});
}
pub fn add_rule(&mut self, rule: ValidationRule) {
self.rules.push(rule);
}
pub fn remove_rule(&mut self, name: &str) {
self.rules.retain(|r| r.name != name);
}
pub async fn pre_validate(
&self,
agent_id: &str,
operation: &ProposedOperation,
) -> Vec<ValidationOutcome> {
let context = self.build_context(agent_id, operation).await;
self.rules
.iter()
.filter(|r| matches!(r.rule_type, RuleType::PreCondition | RuleType::Invariant))
.map(|r| (r.check)(&context))
.collect()
}
pub async fn post_validate(
&self,
agent_id: &str,
operation: &ProposedOperation,
_result: &ValidationOperationResult,
) -> Vec<ValidationOutcome> {
let context = self.build_context(agent_id, operation).await;
self.rules
.iter()
.filter(|r| matches!(r.rule_type, RuleType::PostCondition | RuleType::Invariant))
.map(|r| (r.check)(&context))
.collect()
}
pub async fn check_inter_agent(
&self,
operations: &[(String, ProposedOperation)],
) -> Vec<ValidationOutcome> {
let mut results = Vec::new();
for (agent_id, operation) in operations {
let other_agents: Vec<AgentStatus> = operations
.iter()
.filter(|(id, _)| id != agent_id)
.map(|(id, op)| AgentStatus {
agent_id: id.clone(),
current_operation: Some(op.operation_type.clone()),
held_resources: op.resources_needed.clone(),
waiting_for: None,
})
.collect();
let snapshot = self.state_model.snapshot().await;
let context = ValidationContext {
agent_id: agent_id.clone(),
operation: operation.clone(),
current_state: snapshot,
other_agents,
};
for rule in self
.rules
.iter()
.filter(|r| matches!(r.rule_type, RuleType::InterAgent))
{
results.push((rule.check)(&context));
}
}
results
}
pub async fn check_resource_conflicts(
&self,
operation: &crate::resource_checker::ProposedOperation,
) -> ConflictCheck {
self.resource_checker.check_conflicts(operation).await
}
async fn build_context(
&self,
agent_id: &str,
operation: &ProposedOperation,
) -> ValidationContext {
let snapshot = self.state_model.snapshot().await;
let active_ops = self
.state_model
.operation_state
.get_active_operations()
.await;
let other_agents: Vec<AgentStatus> = active_ops
.iter()
.filter(|op| op.agent_id != agent_id)
.map(|op| AgentStatus {
agent_id: op.agent_id.clone(),
current_operation: Some(op.operation_type.clone()),
held_resources: op.resources_needed.clone(),
waiting_for: None, })
.collect();
ValidationContext {
agent_id: agent_id.to_string(),
operation: operation.clone(),
current_state: snapshot,
other_agents,
}
}
pub async fn validate_with_failures_only(
&self,
agent_id: &str,
operation: &ProposedOperation,
) -> Vec<ValidationOutcome> {
let results = self.pre_validate(agent_id, operation).await;
results.into_iter().filter(|r| !r.passed).collect()
}
pub async fn can_proceed(&self, agent_id: &str, operation: &ProposedOperation) -> bool {
let results = self.pre_validate(agent_id, operation).await;
!results
.iter()
.any(|r| !r.passed && matches!(r.severity, ValidationSeverity::Error))
}
}
pub struct ValidationRule {
pub name: String,
pub description: String,
pub rule_type: RuleType,
pub check: Box<dyn Fn(&ValidationContext) -> ValidationOutcome + Send + Sync>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuleType {
PreCondition,
PostCondition,
Invariant,
InterAgent,
}
pub struct ValidationContext {
pub agent_id: String,
pub operation: ProposedOperation,
pub current_state: StateSnapshot,
pub other_agents: Vec<AgentStatus>,
}
#[derive(Debug, Clone)]
pub struct AgentStatus {
pub agent_id: String,
pub current_operation: Option<String>,
pub held_resources: Vec<String>,
pub waiting_for: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ValidationOutcome {
pub passed: bool,
pub rule_name: String,
pub message: String,
pub severity: ValidationSeverity,
}
impl ValidationOutcome {
pub fn pass(rule_name: &str) -> Self {
Self {
passed: true,
rule_name: rule_name.to_string(),
message: "OK".to_string(),
severity: ValidationSeverity::Info,
}
}
pub fn fail_error(rule_name: &str, message: impl Into<String>) -> Self {
Self {
passed: false,
rule_name: rule_name.to_string(),
message: message.into(),
severity: ValidationSeverity::Error,
}
}
pub fn fail_warning(rule_name: &str, message: impl Into<String>) -> Self {
Self {
passed: false,
rule_name: rule_name.to_string(),
message: message.into(),
severity: ValidationSeverity::Warning,
}
}
}
#[derive(Debug, Clone)]
pub struct ValidationOperationResult {
pub success: bool,
pub outputs: Option<serde_json::Value>,
pub error: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ValidationSummary {
pub total_rules: usize,
pub passed: usize,
pub warnings: usize,
pub errors: usize,
pub outcomes: Vec<ValidationOutcome>,
}
impl ValidationSummary {
pub fn from_outcomes(outcomes: Vec<ValidationOutcome>) -> Self {
let total_rules = outcomes.len();
let passed = outcomes.iter().filter(|o| o.passed).count();
let warnings = outcomes
.iter()
.filter(|o| !o.passed && matches!(o.severity, ValidationSeverity::Warning))
.count();
let errors = outcomes
.iter()
.filter(|o| !o.passed && matches!(o.severity, ValidationSeverity::Error))
.count();
Self {
total_rules,
passed,
warnings,
errors,
outcomes,
}
}
pub fn is_valid(&self) -> bool {
self.errors == 0
}
pub fn is_clean(&self) -> bool {
self.errors == 0 && self.warnings == 0
}
pub fn error_messages(&self) -> Vec<&str> {
self.outcomes
.iter()
.filter(|o| !o.passed && matches!(o.severity, ValidationSeverity::Error))
.map(|o| o.message.as_str())
.collect()
}
pub fn warning_messages(&self) -> Vec<&str> {
self.outcomes
.iter()
.filter(|o| !o.passed && matches!(o.severity, ValidationSeverity::Warning))
.map(|o| o.message.as_str())
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::file_locks::FileLockManager;
use crate::resource_locks::ResourceLockManager;
fn create_test_validation_agent() -> ValidationAgent {
let state_model = Arc::new(ThreeStateModel::new());
let file_locks = Arc::new(FileLockManager::new());
let resource_locks = Arc::new(ResourceLockManager::new());
let resource_checker = Arc::new(ResourceChecker::new(file_locks, resource_locks));
ValidationAgent::new(state_model, resource_checker)
}
#[test]
fn test_validation_outcome_pass() {
let outcome = ValidationOutcome::pass("test_rule");
assert!(outcome.passed);
assert_eq!(outcome.rule_name, "test_rule");
assert_eq!(outcome.severity, ValidationSeverity::Info);
}
#[test]
fn test_validation_outcome_fail() {
let outcome = ValidationOutcome::fail_error("test_rule", "Something went wrong");
assert!(!outcome.passed);
assert_eq!(outcome.rule_name, "test_rule");
assert_eq!(outcome.message, "Something went wrong");
assert_eq!(outcome.severity, ValidationSeverity::Error);
}
#[test]
fn test_validation_summary() {
let outcomes = vec![
ValidationOutcome::pass("rule1"),
ValidationOutcome::pass("rule2"),
ValidationOutcome::fail_warning("rule3", "warning"),
ValidationOutcome::fail_error("rule4", "error"),
];
let summary = ValidationSummary::from_outcomes(outcomes);
assert_eq!(summary.total_rules, 4);
assert_eq!(summary.passed, 2);
assert_eq!(summary.warnings, 1);
assert_eq!(summary.errors, 1);
assert!(!summary.is_valid());
assert!(!summary.is_clean());
}
#[test]
fn test_validation_summary_valid() {
let outcomes = vec![
ValidationOutcome::pass("rule1"),
ValidationOutcome::fail_warning("rule2", "just a warning"),
];
let summary = ValidationSummary::from_outcomes(outcomes);
assert!(summary.is_valid()); assert!(!summary.is_clean()); }
#[tokio::test]
async fn test_pre_validate_no_conflicts() {
let agent = create_test_validation_agent();
let operation = ProposedOperation {
agent_id: "agent-1".to_string(),
operation_type: "file_write".to_string(),
resources_needed: vec!["/test/file.rs".to_string()],
resources_produced: vec!["/test/file.rs".to_string()],
};
let results = agent.pre_validate("agent-1", &operation).await;
assert!(results.iter().all(|r| r.passed));
}
#[tokio::test]
async fn test_pre_validate_lock_conflict() {
let state_model = Arc::new(ThreeStateModel::new());
state_model
.dependency_state
.set_holder("/test/file.rs", Some("agent-2"))
.await;
let file_locks = Arc::new(FileLockManager::new());
let resource_locks = Arc::new(ResourceLockManager::new());
let resource_checker = Arc::new(ResourceChecker::new(file_locks, resource_locks));
let agent = ValidationAgent::new(state_model.clone(), resource_checker);
let operation = ProposedOperation {
agent_id: "agent-1".to_string(),
operation_type: "file_write".to_string(),
resources_needed: vec!["/test/file.rs".to_string()],
resources_produced: vec![],
};
let mut snapshot = state_model.snapshot().await;
snapshot
.locks
.insert("/test/file.rs".to_string(), "agent-2".to_string());
let results = agent.pre_validate("agent-1", &operation).await;
assert!(!results.is_empty());
}
#[tokio::test]
async fn test_can_proceed() {
let agent = create_test_validation_agent();
let operation = ProposedOperation {
agent_id: "agent-1".to_string(),
operation_type: "file_read".to_string(),
resources_needed: vec![],
resources_produced: vec![],
};
let can_proceed = agent.can_proceed("agent-1", &operation).await;
assert!(can_proceed);
}
#[tokio::test]
async fn test_check_inter_agent_no_conflicts() {
let agent = create_test_validation_agent();
let operations = vec![
(
"agent-1".to_string(),
ProposedOperation {
agent_id: "agent-1".to_string(),
operation_type: "file_read".to_string(),
resources_needed: vec!["/file1.rs".to_string()],
resources_produced: vec![],
},
),
(
"agent-2".to_string(),
ProposedOperation {
agent_id: "agent-2".to_string(),
operation_type: "file_read".to_string(),
resources_needed: vec!["/file2.rs".to_string()],
resources_produced: vec![],
},
),
];
let results = agent.check_inter_agent(&operations).await;
assert!(results.iter().all(|r| r.passed));
}
#[tokio::test]
async fn test_check_inter_agent_git_conflict() {
let agent = create_test_validation_agent();
let operations = vec![
(
"agent-1".to_string(),
ProposedOperation {
agent_id: "agent-1".to_string(),
operation_type: "git_commit".to_string(),
resources_needed: vec!["git_index".to_string()],
resources_produced: vec![],
},
),
(
"agent-2".to_string(),
ProposedOperation {
agent_id: "agent-2".to_string(),
operation_type: "git_push".to_string(),
resources_needed: vec!["git_remote".to_string()],
resources_produced: vec![],
},
),
];
let results = agent.check_inter_agent(&operations).await;
let has_conflict = results.iter().any(|r| !r.passed);
assert!(has_conflict);
}
#[test]
fn test_agent_status_creation() {
let status = AgentStatus {
agent_id: "agent-1".to_string(),
current_operation: Some("build".to_string()),
held_resources: vec!["build_lock".to_string()],
waiting_for: None,
};
assert_eq!(status.agent_id, "agent-1");
assert_eq!(status.current_operation, Some("build".to_string()));
assert_eq!(status.held_resources.len(), 1);
}
#[test]
fn test_add_custom_rule() {
let mut agent = create_test_validation_agent();
let initial_count = agent.rules.len();
agent.add_rule(ValidationRule {
name: "custom_rule".into(),
description: "A custom validation rule".into(),
rule_type: RuleType::PreCondition,
check: Box::new(|_ctx| ValidationOutcome::pass("custom_rule")),
});
assert_eq!(agent.rules.len(), initial_count + 1);
}
#[test]
fn test_remove_rule() {
let mut agent = create_test_validation_agent();
let initial_count = agent.rules.len();
agent.add_rule(ValidationRule {
name: "to_remove".into(),
description: "Will be removed".into(),
rule_type: RuleType::PreCondition,
check: Box::new(|_ctx| ValidationOutcome::pass("to_remove")),
});
assert_eq!(agent.rules.len(), initial_count + 1);
agent.remove_rule("to_remove");
assert_eq!(agent.rules.len(), initial_count);
}
}