macot 0.1.11

Multi Agent Control Tower - CLI for orchestrating Claude CLI instances
Documentation
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Decision {
    pub id: String,
    pub made_by: u32,
    pub timestamp: DateTime<Utc>,
    pub topic: String,
    pub decision: String,
    pub rationale: String,
    #[serde(default)]
    pub affects_experts: Vec<u32>,
}

impl Decision {
    pub fn new(made_by: u32, topic: String, decision: String, rationale: String) -> Self {
        Self {
            id: format!("decision-{}", Utc::now().timestamp()),
            made_by,
            timestamp: Utc::now(),
            topic,
            decision,
            rationale,
            affects_experts: Vec::new(),
        }
    }

    #[allow(dead_code)]
    pub fn with_affects_experts(mut self, experts: Vec<u32>) -> Self {
        self.affects_experts = experts;
        self
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Convention {
    pub pattern: String,
    pub description: String,
    pub discovered_at: DateTime<Utc>,
    pub discovered_by: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct FileDependency {
    pub file: String,
    pub depends_on: Vec<String>,
    pub depended_by: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SharedContext {
    #[serde(default)]
    pub decisions: Vec<Decision>,
    #[serde(default)]
    pub conventions: Vec<Convention>,
    #[serde(default)]
    pub dependencies: Vec<FileDependency>,
}

impl SharedContext {
    pub fn add_decision(&mut self, decision: Decision) {
        self.decisions.push(decision);
    }

    #[allow(dead_code)]
    pub fn get_decisions_for_expert(&self, expert_id: u32) -> Vec<&Decision> {
        self.decisions
            .iter()
            .filter(|d| d.affects_experts.is_empty() || d.affects_experts.contains(&expert_id))
            .collect()
    }

    #[allow(dead_code)]
    pub fn get_decisions_by_topic(&self, topic: &str) -> Vec<&Decision> {
        let topic_lower = topic.to_lowercase();
        self.decisions
            .iter()
            .filter(|d| d.topic.to_lowercase().contains(&topic_lower))
            .collect()
    }

    #[allow(dead_code)]
    pub fn add_convention(&mut self, convention: Convention) {
        self.conventions.push(convention);
    }

    #[allow(dead_code)]
    pub fn get_conventions(&self) -> &[Convention] {
        &self.conventions
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn decision_new_creates_with_timestamp() {
        let decision = Decision::new(
            0,
            "Architecture".to_string(),
            "Use microservices".to_string(),
            "Better scalability".to_string(),
        );

        assert!(decision.id.starts_with("decision-"));
        assert_eq!(decision.made_by, 0);
        assert_eq!(decision.topic, "Architecture");
        assert!(decision.affects_experts.is_empty());
    }

    #[test]
    fn decision_with_affects_experts_sets_list() {
        let decision = Decision::new(
            0,
            "API".to_string(),
            "Use REST".to_string(),
            "Standard".to_string(),
        )
        .with_affects_experts(vec![1, 2]);

        assert_eq!(decision.affects_experts, vec![1, 2]);
    }

    #[test]
    fn shared_context_add_decision_appends() {
        let mut ctx = SharedContext::default();

        ctx.add_decision(Decision::new(
            0,
            "Auth".to_string(),
            "Use JWT".to_string(),
            "Stateless".to_string(),
        ));

        assert_eq!(ctx.decisions.len(), 1);
    }

    #[test]
    fn shared_context_get_decisions_for_expert_filters_correctly() {
        let mut ctx = SharedContext::default();

        ctx.add_decision(Decision::new(
            0,
            "Global".to_string(),
            "Use TypeScript".to_string(),
            "Type safety".to_string(),
        ));

        ctx.add_decision(
            Decision::new(
                0,
                "Frontend".to_string(),
                "Use React".to_string(),
                "Popular".to_string(),
            )
            .with_affects_experts(vec![1]),
        );

        ctx.add_decision(
            Decision::new(
                0,
                "Backend".to_string(),
                "Use Rust".to_string(),
                "Performance".to_string(),
            )
            .with_affects_experts(vec![2]),
        );

        let expert1_decisions = ctx.get_decisions_for_expert(1);
        assert_eq!(expert1_decisions.len(), 2);

        let expert2_decisions = ctx.get_decisions_for_expert(2);
        assert_eq!(expert2_decisions.len(), 2);
    }

    #[test]
    fn shared_context_get_decisions_by_topic_filters_case_insensitive() {
        let mut ctx = SharedContext::default();

        ctx.add_decision(Decision::new(
            0,
            "Authentication".to_string(),
            "Use OAuth2".to_string(),
            "Standard".to_string(),
        ));

        ctx.add_decision(Decision::new(
            0,
            "Database".to_string(),
            "Use PostgreSQL".to_string(),
            "Reliable".to_string(),
        ));

        let auth_decisions = ctx.get_decisions_by_topic("auth");
        assert_eq!(auth_decisions.len(), 1);
        assert_eq!(auth_decisions[0].topic, "Authentication");
    }

    #[test]
    fn shared_context_serializes_to_yaml() {
        let mut ctx = SharedContext::default();
        ctx.add_decision(Decision::new(
            0,
            "Test".to_string(),
            "Decision".to_string(),
            "Reason".to_string(),
        ));

        let yaml = serde_yaml::to_string(&ctx).unwrap();
        assert!(yaml.contains("topic: Test"));
        assert!(yaml.contains("decision: Decision"));
    }

    #[test]
    fn shared_context_deserializes_from_yaml() {
        let yaml = r#"
decisions:
  - id: decision-123
    made_by: 0
    timestamp: "2024-01-15T10:00:00Z"
    topic: Architecture
    decision: Use microservices
    rationale: Scalability
    affects_experts: [1, 2]
conventions:
  - pattern: "*.test.ts"
    description: "Test file naming"
    discovered_at: "2024-01-15T10:00:00Z"
    discovered_by: 3
dependencies: []
"#;

        let ctx: SharedContext = serde_yaml::from_str(yaml).unwrap();
        assert_eq!(ctx.decisions.len(), 1);
        assert_eq!(ctx.decisions[0].topic, "Architecture");
        assert_eq!(ctx.conventions.len(), 1);
    }
}