organism_simulation/
policy.rs1use crate::{DimensionResult, Sample, SimulationDimension};
8
9#[derive(Debug, Clone)]
10pub struct PolicySimulatorConfig {
11 pub required_authority_level: u32,
12 pub require_compliance_tags: Vec<String>,
13 pub block_on_missing_authority: bool,
14}
15
16impl Default for PolicySimulatorConfig {
17 fn default() -> Self {
18 Self {
19 required_authority_level: 1,
20 require_compliance_tags: Vec::new(),
21 block_on_missing_authority: true,
22 }
23 }
24}
25
26pub struct PolicySimulator {
27 config: PolicySimulatorConfig,
28}
29
30impl PolicySimulator {
31 #[must_use]
32 pub fn new(config: PolicySimulatorConfig) -> Self {
33 Self { config }
34 }
35
36 fn extract_authority_level(plan: &serde_json::Value) -> Option<u32> {
37 plan.get("annotation")
38 .and_then(|a| a.get("authority_level"))
39 .and_then(serde_json::Value::as_u64)
40 .map(|v| u32::try_from(v).unwrap_or(0))
41 }
42
43 fn extract_compliance_tags(plan: &serde_json::Value) -> Vec<String> {
44 plan.get("annotation")
45 .and_then(|a| a.get("compliance_tags"))
46 .and_then(|t| t.as_array())
47 .map(|arr| {
48 arr.iter()
49 .filter_map(|v| v.as_str().map(String::from))
50 .collect()
51 })
52 .unwrap_or_default()
53 }
54
55 fn extract_approval_gates(plan: &serde_json::Value) -> Vec<String> {
56 plan.get("annotation")
57 .and_then(|a| a.get("approval_gates"))
58 .and_then(|g| g.as_array())
59 .map(|arr| {
60 arr.iter()
61 .filter_map(|v| v.as_str().map(String::from))
62 .collect()
63 })
64 .unwrap_or_default()
65 }
66
67 fn sample(confidence: f64) -> Vec<Sample> {
68 let buckets = 5;
69 let mut samples = Vec::with_capacity(buckets);
70
71 for i in 0..buckets {
72 let bucket_center = (f64::from(u32::try_from(i).unwrap_or(0)) + 0.5)
73 / f64::from(u32::try_from(buckets).unwrap_or(5));
74 let distance = (bucket_center - confidence).abs();
75 let weight = (-distance * 4.0).exp();
76 samples.push(Sample {
77 value: bucket_center,
78 probability: weight,
79 });
80 }
81
82 let total: f64 = samples.iter().map(|s| s.probability).sum();
83 if total > 0.0 {
84 for s in &mut samples {
85 s.probability /= total;
86 }
87 }
88
89 samples
90 }
91
92 pub fn simulate(&self, plan: &serde_json::Value) -> DimensionResult {
93 let authority = Self::extract_authority_level(plan);
94 let tags = Self::extract_compliance_tags(plan);
95 let gates = Self::extract_approval_gates(plan);
96
97 let mut findings = Vec::new();
98 let mut violations = 0u32;
99
100 match authority {
102 Some(level) if level >= self.config.required_authority_level => {
103 findings.push(format!("authority level {level} meets requirement"));
104 }
105 Some(level) => {
106 findings.push(format!(
107 "authority level {level} below required {}",
108 self.config.required_authority_level,
109 ));
110 violations += 1;
111 }
112 None if self.config.block_on_missing_authority => {
113 findings.push("no authority level declared — blocked by policy".into());
114 violations += 1;
115 }
116 None => {
117 findings.push("no authority level declared — not required".into());
118 }
119 }
120
121 for required in &self.config.require_compliance_tags {
123 if tags.iter().any(|t| t == required) {
124 findings.push(format!("compliance: {required} satisfied"));
125 } else {
126 findings.push(format!("compliance: {required} missing"));
127 violations += 1;
128 }
129 }
130
131 if !gates.is_empty() {
133 findings.push(format!(
134 "{} approval gate(s) required: {}",
135 gates.len(),
136 gates.join(", ")
137 ));
138 }
139
140 let passed = violations == 0;
141 let total_checks =
142 1 + u32::try_from(self.config.require_compliance_tags.len()).unwrap_or(0);
143 let confidence = if total_checks == 0 {
144 1.0
145 } else {
146 f64::from(total_checks - violations.min(total_checks)) / f64::from(total_checks)
147 };
148
149 let samples = Self::sample(confidence);
150
151 DimensionResult {
152 dimension: SimulationDimension::Policy,
153 passed,
154 confidence,
155 findings,
156 samples,
157 }
158 }
159}
160
161use crate::types::{SimulationRecommendation, SimulationVerdict};
164use converge_pack::{AgentEffect, Context, ContextKey, ProposedFact, Suggestor};
165
166pub struct PolicySimulationAgent {
167 simulator: PolicySimulator,
168}
169
170impl PolicySimulationAgent {
171 #[must_use]
172 pub fn new(config: PolicySimulatorConfig) -> Self {
173 Self {
174 simulator: PolicySimulator::new(config),
175 }
176 }
177
178 #[must_use]
179 pub fn default_config() -> Self {
180 Self {
181 simulator: PolicySimulator::new(PolicySimulatorConfig::default()),
182 }
183 }
184}
185
186#[async_trait::async_trait]
187#[allow(clippy::unnecessary_literal_bound)]
188impl Suggestor for PolicySimulationAgent {
189 fn name(&self) -> &'static str {
190 "policy-simulation"
191 }
192
193 fn dependencies(&self) -> &[ContextKey] {
194 &[ContextKey::Strategies]
195 }
196
197 fn accepts(&self, ctx: &dyn Context) -> bool {
198 ctx.has(ContextKey::Strategies) && !ctx.has(ContextKey::Evaluations)
199 }
200
201 async fn execute(&self, ctx: &dyn Context) -> AgentEffect {
202 let strategies = ctx.get(ContextKey::Strategies);
203 let mut proposals = Vec::new();
204
205 for fact in strategies {
206 let plan_json: serde_json::Value = serde_json::from_str(&fact.content)
207 .unwrap_or_else(|_| serde_json::json!({"description": fact.content}));
208
209 let result = self.simulator.simulate(&plan_json);
210
211 let verdict = SimulationVerdict {
212 strategy_id: fact.id.clone(),
213 dimension: crate::SimulationDimension::Policy,
214 passed: result.passed,
215 confidence: result.confidence,
216 findings: result.findings,
217 recommendation: if result.passed {
218 None
219 } else {
220 Some(SimulationRecommendation::DoNotProceed)
221 },
222 };
223
224 let key = if result.passed {
225 ContextKey::Evaluations
226 } else {
227 ContextKey::Constraints
228 };
229
230 proposals.push(ProposedFact::new(
231 key,
232 verdict.fact_id(),
233 verdict.to_json(),
234 "policy-simulation",
235 ));
236 }
237
238 AgentEffect::with_proposals(proposals)
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use serde_json::json;
246
247 fn default_simulator() -> PolicySimulator {
248 PolicySimulator::new(PolicySimulatorConfig::default())
249 }
250
251 #[test]
252 fn sufficient_authority_passes() {
253 let sim = default_simulator();
254 let plan = json!({
255 "annotation": {
256 "authority_level": 2
257 }
258 });
259 let result = sim.simulate(&plan);
260 assert_eq!(result.dimension, SimulationDimension::Policy);
261 assert!(result.passed);
262 }
263
264 #[test]
265 fn insufficient_authority_fails() {
266 let sim = PolicySimulator::new(PolicySimulatorConfig {
267 required_authority_level: 3,
268 ..PolicySimulatorConfig::default()
269 });
270 let plan = json!({
271 "annotation": {
272 "authority_level": 1
273 }
274 });
275 let result = sim.simulate(&plan);
276 assert!(!result.passed);
277 assert!(result.findings.iter().any(|f| f.contains("below required")));
278 }
279
280 #[test]
281 fn missing_authority_blocks_by_default() {
282 let sim = default_simulator();
283 let plan = json!({});
284 let result = sim.simulate(&plan);
285 assert!(!result.passed);
286 assert!(
287 result
288 .findings
289 .iter()
290 .any(|f| f.contains("blocked by policy"))
291 );
292 }
293
294 #[test]
295 fn missing_authority_allowed_when_configured() {
296 let sim = PolicySimulator::new(PolicySimulatorConfig {
297 block_on_missing_authority: false,
298 ..PolicySimulatorConfig::default()
299 });
300 let plan = json!({});
301 let result = sim.simulate(&plan);
302 assert!(result.passed);
303 }
304
305 #[test]
306 fn compliance_tags_checked() {
307 let sim = PolicySimulator::new(PolicySimulatorConfig {
308 require_compliance_tags: vec!["gdpr".into(), "soc2".into()],
309 ..PolicySimulatorConfig::default()
310 });
311 let plan = json!({
312 "annotation": {
313 "authority_level": 1,
314 "compliance_tags": ["gdpr"]
315 }
316 });
317 let result = sim.simulate(&plan);
318 assert!(!result.passed); assert!(result.findings.iter().any(|f| f.contains("soc2 missing")));
320 }
321
322 #[test]
323 fn all_compliance_satisfied() {
324 let sim = PolicySimulator::new(PolicySimulatorConfig {
325 require_compliance_tags: vec!["gdpr".into()],
326 ..PolicySimulatorConfig::default()
327 });
328 let plan = json!({
329 "annotation": {
330 "authority_level": 1,
331 "compliance_tags": ["gdpr", "hipaa"]
332 }
333 });
334 let result = sim.simulate(&plan);
335 assert!(result.passed);
336 }
337
338 #[test]
339 fn approval_gates_noted() {
340 let sim = default_simulator();
341 let plan = json!({
342 "annotation": {
343 "authority_level": 1,
344 "approval_gates": ["legal-review", "cfo-sign-off"]
345 }
346 });
347 let result = sim.simulate(&plan);
348 assert!(result.passed);
349 assert!(result.findings.iter().any(|f| f.contains("approval gate")));
350 }
351}