acp/constraints/
enforcer.rs1use serde::{Deserialize, Serialize};
7
8use super::guardrails::FileGuardrails;
9
10#[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#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct Violation {
22 pub rule: String,
23 pub message: String,
24 pub severity: Severity,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct Warning {
30 pub rule: String,
31 pub message: String,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct RequiredAction {
37 pub action: String,
38 pub reason: String,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "lowercase")]
44pub enum Severity {
45 Error,
46 Warning,
47 Info,
48}
49
50pub struct GuardrailEnforcer;
52
53impl GuardrailEnforcer {
54 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 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 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 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 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 pub fn check_changes(guardrails: &FileGuardrails, proposed_content: &str) -> GuardrailCheck {
110 let mut check = Self::can_modify(guardrails);
111
112 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 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}