acp/constraints/
enforcer.rs

1//! @acp:module "Guardrail Enforcer"
2//! @acp:summary "Enforces guardrails on proposed changes"
3//! @acp:domain cli
4//! @acp:layer service
5
6use serde::{Deserialize, Serialize};
7
8use super::guardrails::FileGuardrails;
9
10/// @acp:summary "Result of checking guardrails against proposed changes"
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct GuardrailCheck {
13    pub passed: bool,
14    pub violations: Vec<Violation>,
15    pub warnings: Vec<Warning>,
16    pub required_actions: Vec<RequiredAction>,
17}
18
19/// @acp:summary "A guardrail violation"
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct Violation {
22    pub rule: String,
23    pub message: String,
24    pub severity: Severity,
25}
26
27/// @acp:summary "A guardrail warning"
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct Warning {
30    pub rule: String,
31    pub message: String,
32}
33
34/// @acp:summary "A required action before changes can proceed"
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct RequiredAction {
37    pub action: String,
38    pub reason: String,
39}
40
41/// @acp:summary "Severity level of a violation"
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "lowercase")]
44pub enum Severity {
45    Error,
46    Warning,
47    Info,
48}
49
50/// @acp:summary "Enforces guardrails on proposed changes"
51pub struct GuardrailEnforcer;
52
53impl GuardrailEnforcer {
54    /// Check if AI should modify this file
55    pub fn can_modify(guardrails: &FileGuardrails) -> GuardrailCheck {
56        let mut check = GuardrailCheck {
57            passed: true,
58            violations: vec![],
59            warnings: vec![],
60            required_actions: vec![],
61        };
62
63        // Check readonly
64        if guardrails.ai_behavior.readonly {
65            check.passed = false;
66            check.violations.push(Violation {
67                rule: "ai-readonly".to_string(),
68                message: format!(
69                    "File is marked as AI-readonly{}",
70                    guardrails
71                        .ai_behavior
72                        .readonly_reason
73                        .as_ref()
74                        .map(|r| format!(": {}", r))
75                        .unwrap_or_default()
76                ),
77                severity: Severity::Error,
78            });
79        }
80
81        // Check ai-careful
82        for careful in &guardrails.ai_behavior.careful {
83            check.warnings.push(Warning {
84                rule: "ai-careful".to_string(),
85                message: format!("Extra caution required: {}", careful),
86            });
87        }
88
89        // Check ai-ask
90        for ask in &guardrails.ai_behavior.ask_before {
91            check.required_actions.push(RequiredAction {
92                action: "ask-user".to_string(),
93                reason: format!("Must ask before: {}", ask),
94            });
95        }
96
97        // Check review requirements
98        for review in &guardrails.review.required {
99            check.required_actions.push(RequiredAction {
100                action: "flag-for-review".to_string(),
101                reason: format!("Requires {} review", review),
102            });
103        }
104
105        check
106    }
107
108    /// Check if proposed changes violate constraints
109    pub fn check_changes(guardrails: &FileGuardrails, proposed_content: &str) -> GuardrailCheck {
110        let mut check = Self::can_modify(guardrails);
111
112        // Check forbids
113        for forbidden in &guardrails.constraints.forbids {
114            if proposed_content.contains(forbidden) {
115                check.passed = false;
116                check.violations.push(Violation {
117                    rule: "forbids".to_string(),
118                    message: format!("Contains forbidden pattern: {}", forbidden),
119                    severity: Severity::Error,
120                });
121            }
122        }
123
124        // Check test requirements
125        if !guardrails.constraints.test_required.is_empty() {
126            check.required_actions.push(RequiredAction {
127                action: "write-tests".to_string(),
128                reason: format!(
129                    "Tests required: {}",
130                    guardrails.constraints.test_required.join(", ")
131                ),
132            });
133        }
134
135        check
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use crate::constraints::guardrails::AIBehavior;
143
144    #[test]
145    fn test_enforcer_readonly() {
146        let guardrails = FileGuardrails {
147            ai_behavior: AIBehavior {
148                readonly: true,
149                readonly_reason: Some("test".to_string()),
150                ..Default::default()
151            },
152            ..Default::default()
153        };
154
155        let check = GuardrailEnforcer::can_modify(&guardrails);
156        assert!(!check.passed);
157        assert_eq!(check.violations.len(), 1);
158    }
159}