Skip to main content

clawft_kernel/
governance.rs

1//! Constitutional governance engine for WeftOS.
2//!
3//! Implements the three-branch governance model where:
4//! - **Legislative** (SOPs, rules, manifests) defines boundaries
5//! - **Executive** (agents) acts within defined boundaries
6//! - **Judicial** (CGR engine) validates every action
7//!
8//! No branch can modify another's constraints. Governance violations
9//! are type-level impossibilities, not merely audited events.
10//!
11//! # Design
12//!
13//! All types compile unconditionally. The CGR validation engine and
14//! effect algebra scoring require the `governance` or `ruvector-apps`
15//! feature gates. Without them, `GovernanceEngine::evaluate()` returns
16//! `GovernanceDecision::Permit` (open governance).
17
18use std::collections::HashMap;
19
20use serde::{Deserialize, Serialize};
21use tracing::debug;
22
23use crate::environment::{Environment, EnvironmentClass};
24
25/// A governance rule that restricts agent behavior.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct GovernanceRule {
28    /// Unique rule identifier.
29    pub id: String,
30
31    /// Human-readable rule description.
32    pub description: String,
33
34    /// Which branch defined this rule.
35    pub branch: GovernanceBranch,
36
37    /// Rule severity (how critical the violation is).
38    pub severity: RuleSeverity,
39
40    /// Whether this rule is currently active.
41    #[serde(default = "default_true")]
42    pub active: bool,
43
44    /// SOP reference URL for agents to consult for full procedure.
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub reference_url: Option<String>,
47
48    /// SOP category tag for filtering rules by domain.
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub sop_category: Option<String>,
51}
52
53impl GovernanceRule {
54    /// Get rules by SOP category from a slice of rules.
55    pub fn filter_by_category<'a>(rules: &'a [GovernanceRule], category: &str) -> Vec<&'a GovernanceRule> {
56        rules.iter().filter(|r| r.sop_category.as_deref() == Some(category)).collect()
57    }
58}
59
60fn default_true() -> bool {
61    true
62}
63
64/// Governance branch that owns a rule.
65#[non_exhaustive]
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub enum GovernanceBranch {
68    /// Rules from SOPs, genesis protocol, weftapp.toml.
69    Legislative,
70    /// Rules from agent execution policies.
71    Executive,
72    /// Rules from CGR validation engine.
73    Judicial,
74}
75
76impl std::fmt::Display for GovernanceBranch {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        match self {
79            GovernanceBranch::Legislative => write!(f, "legislative"),
80            GovernanceBranch::Executive => write!(f, "executive"),
81            GovernanceBranch::Judicial => write!(f, "judicial"),
82        }
83    }
84}
85
86/// Rule violation severity.
87#[non_exhaustive]
88#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
89pub enum RuleSeverity {
90    /// Advisory -- logged but not enforced.
91    Advisory,
92    /// Warning -- logged and flagged, action proceeds.
93    Warning,
94    /// Blocking -- action is prevented.
95    Blocking,
96    /// Critical -- action prevented and agent capability may be revoked.
97    Critical,
98}
99
100impl std::fmt::Display for RuleSeverity {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        match self {
103            RuleSeverity::Advisory => write!(f, "advisory"),
104            RuleSeverity::Warning => write!(f, "warning"),
105            RuleSeverity::Blocking => write!(f, "blocking"),
106            RuleSeverity::Critical => write!(f, "critical"),
107        }
108    }
109}
110
111/// 5-dimensional effect vector for scoring agent actions.
112///
113/// Each dimension is scored from 0.0 (no impact) to 1.0 (maximum impact).
114/// The magnitude of the vector determines whether an action exceeds
115/// the environment's governance threshold.
116#[derive(Debug, Clone, Default, Serialize, Deserialize)]
117pub struct EffectVector {
118    /// Risk score: probability of negative outcome.
119    #[serde(default)]
120    pub risk: f64,
121
122    /// Fairness score: impact on equitable treatment.
123    #[serde(default)]
124    pub fairness: f64,
125
126    /// Privacy score: impact on data privacy.
127    #[serde(default)]
128    pub privacy: f64,
129
130    /// Novelty score: how unprecedented the action is.
131    #[serde(default)]
132    pub novelty: f64,
133
134    /// Security score: impact on system security.
135    #[serde(default)]
136    pub security: f64,
137}
138
139impl EffectVector {
140    /// Compute the magnitude of the effect vector (L2 norm).
141    pub fn magnitude(&self) -> f64 {
142        (self.risk * self.risk
143            + self.fairness * self.fairness
144            + self.privacy * self.privacy
145            + self.novelty * self.novelty
146            + self.security * self.security)
147            .sqrt()
148    }
149
150    /// Check if any dimension exceeds a threshold.
151    pub fn any_exceeds(&self, threshold: f64) -> bool {
152        self.risk > threshold
153            || self.fairness > threshold
154            || self.privacy > threshold
155            || self.novelty > threshold
156            || self.security > threshold
157    }
158
159    /// Get the maximum dimension value.
160    pub fn max_dimension(&self) -> f64 {
161        [
162            self.risk,
163            self.fairness,
164            self.privacy,
165            self.novelty,
166            self.security,
167        ]
168        .into_iter()
169        .fold(0.0_f64, f64::max)
170    }
171}
172
173/// Governance decision for an action.
174#[non_exhaustive]
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
176pub enum GovernanceDecision {
177    /// Action is permitted.
178    Permit,
179    /// Action is permitted with advisory note.
180    PermitWithWarning(String),
181    /// Action requires human approval before proceeding.
182    EscalateToHuman(String),
183    /// Action is denied.
184    Deny(String),
185}
186
187impl std::fmt::Display for GovernanceDecision {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        match self {
190            GovernanceDecision::Permit => write!(f, "permit"),
191            GovernanceDecision::PermitWithWarning(msg) => write!(f, "permit (warning: {msg})"),
192            GovernanceDecision::EscalateToHuman(msg) => write!(f, "escalate ({msg})"),
193            GovernanceDecision::Deny(reason) => write!(f, "deny: {reason}"),
194        }
195    }
196}
197
198/// Governance evaluation request.
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct GovernanceRequest {
201    /// Agent identifier making the request.
202    pub agent_id: String,
203
204    /// Action being requested.
205    pub action: String,
206
207    /// Computed effect vector for the action.
208    #[serde(default)]
209    pub effect: EffectVector,
210
211    /// Additional context for the evaluator.
212    #[serde(default)]
213    pub context: std::collections::HashMap<String, String>,
214
215    /// Node ID of the requesting node (for distributed governance in K6).
216    #[serde(default, skip_serializing_if = "Option::is_none")]
217    pub node_id: Option<String>,
218}
219
220impl GovernanceRequest {
221    /// Create a new governance request.
222    pub fn new(agent_id: impl Into<String>, action: impl Into<String>) -> Self {
223        Self {
224            agent_id: agent_id.into(),
225            action: action.into(),
226            effect: EffectVector::default(),
227            context: std::collections::HashMap::new(),
228            node_id: None,
229        }
230    }
231
232    /// Set the node ID for distributed governance evaluation.
233    pub fn with_node_id(mut self, node_id: impl Into<String>) -> Self {
234        self.node_id = Some(node_id.into());
235        self
236    }
237
238    /// Set the effect vector.
239    pub fn with_effect(mut self, effect: EffectVector) -> Self {
240        self.effect = effect;
241        self
242    }
243
244    /// Enrich the request with tool execution context (k3:D2).
245    ///
246    /// Sets the `context` map with tool-specific fields so governance
247    /// rules can distinguish between different tool invocations even
248    /// when the action string is the generic `"tool.exec"`.
249    ///
250    /// # Fields set
251    ///
252    /// - `tool` — tool name (e.g. `"fs.read_file"`)
253    /// - `gate_action` — the per-tool gate action from the catalog
254    /// - `pid` — stringified PID of the requesting agent
255    ///
256    /// The `effect` field is set from the tool's declared effect vector,
257    /// enabling threshold-based governance that varies per tool.
258    pub fn with_tool_context(
259        mut self,
260        tool_name: impl Into<String>,
261        gate_action: impl Into<String>,
262        effect: EffectVector,
263        pid: u64,
264    ) -> Self {
265        self.context.insert("tool".into(), tool_name.into());
266        self.context.insert("gate_action".into(), gate_action.into());
267        self.context.insert("pid".into(), pid.to_string());
268        self.effect = effect;
269        self
270    }
271
272    /// Add a single key-value pair to the context map.
273    pub fn with_context_entry(
274        mut self,
275        key: impl Into<String>,
276        value: impl Into<String>,
277    ) -> Self {
278        self.context.insert(key.into(), value.into());
279        self
280    }
281}
282
283/// Governance evaluation result.
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct GovernanceResult {
286    /// The decision.
287    pub decision: GovernanceDecision,
288
289    /// Rules that were evaluated.
290    pub evaluated_rules: Vec<String>,
291
292    /// The effect vector that was scored.
293    pub effect: EffectVector,
294
295    /// Whether the effect magnitude exceeded the threshold.
296    pub threshold_exceeded: bool,
297}
298
299/// Governance engine.
300///
301/// Evaluates actions against governance rules and the environment's
302/// risk threshold. Without the `governance` feature gate, all
303/// evaluations return `Permit`.
304pub struct GovernanceEngine {
305    rules: Vec<GovernanceRule>,
306    risk_threshold: f64,
307    human_approval_required: bool,
308}
309
310impl GovernanceEngine {
311    /// Create a governance engine with the given risk threshold.
312    pub fn new(risk_threshold: f64, human_approval_required: bool) -> Self {
313        Self {
314            rules: Vec::new(),
315            risk_threshold,
316            human_approval_required,
317        }
318    }
319
320    /// Create an open governance engine that permits everything.
321    pub fn open() -> Self {
322        Self {
323            rules: Vec::new(),
324            risk_threshold: 1.0,
325            human_approval_required: false,
326        }
327    }
328
329    /// Add a governance rule.
330    pub fn add_rule(&mut self, rule: GovernanceRule) {
331        debug!(rule_id = %rule.id, branch = %rule.branch, "adding governance rule");
332        self.rules.push(rule);
333    }
334
335    /// Get all active rules.
336    pub fn active_rules(&self) -> Vec<&GovernanceRule> {
337        self.rules.iter().filter(|r| r.active).collect()
338    }
339
340    /// Get rules by branch.
341    pub fn rules_by_branch(&self, branch: &GovernanceBranch) -> Vec<&GovernanceRule> {
342        self.rules
343            .iter()
344            .filter(|r| r.active && &r.branch == branch)
345            .collect()
346    }
347
348    /// Evaluate a governance request.
349    ///
350    /// Decision logic:
351    /// 1. If any blocking/critical rule applies, deny.
352    /// 2. If effect magnitude exceeds threshold:
353    ///    - If human_approval_required, escalate.
354    ///    - Otherwise deny.
355    /// 3. If any warning rule applies, permit with warning.
356    /// 4. Otherwise permit.
357    pub fn evaluate(&self, request: &GovernanceRequest) -> GovernanceResult {
358        let magnitude = request.effect.magnitude();
359        let threshold_exceeded = magnitude > self.risk_threshold;
360
361        let mut evaluated_rules = Vec::new();
362        let mut has_warning = false;
363        let mut has_blocking = false;
364        let mut blocking_reason = String::new();
365
366        for rule in self.active_rules() {
367            evaluated_rules.push(rule.id.clone());
368
369            match rule.severity {
370                RuleSeverity::Blocking | RuleSeverity::Critical => {
371                    if threshold_exceeded {
372                        has_blocking = true;
373                        blocking_reason = format!(
374                            "rule '{}': effect magnitude {magnitude:.2} > threshold {:.2}",
375                            rule.id, self.risk_threshold
376                        );
377                    }
378                }
379                RuleSeverity::Warning => {
380                    if threshold_exceeded {
381                        has_warning = true;
382                    }
383                }
384                RuleSeverity::Advisory => {}
385            }
386        }
387
388        let decision = if has_blocking {
389            if self.human_approval_required {
390                GovernanceDecision::EscalateToHuman(blocking_reason)
391            } else {
392                GovernanceDecision::Deny(blocking_reason)
393            }
394        } else if threshold_exceeded && has_warning {
395            GovernanceDecision::PermitWithWarning(format!(
396                "effect magnitude {magnitude:.2} approaches threshold {:.2}",
397                self.risk_threshold
398            ))
399        } else {
400            GovernanceDecision::Permit
401        };
402
403        GovernanceResult {
404            decision,
405            evaluated_rules,
406            effect: request.effect.clone(),
407            threshold_exceeded,
408        }
409    }
410
411    /// Get the configured risk threshold.
412    pub fn risk_threshold(&self) -> f64 {
413        self.risk_threshold
414    }
415
416    /// Get total rule count.
417    pub fn rule_count(&self) -> usize {
418        self.rules.len()
419    }
420
421    /// Evaluate a governance request in the context of a specific environment.
422    ///
423    /// Different environment classes apply different risk thresholds:
424    /// - **Development**: uses the environment's own `risk_threshold` (lenient, typically 0.9).
425    /// - **Staging**: uses the environment's own `risk_threshold` (moderate, typically 0.6).
426    /// - **Production**: uses half the environment's `risk_threshold` (strict, typically 0.15).
427    /// - **Custom**: uses the custom class's `risk_threshold` directly.
428    ///
429    /// After normal rule evaluation, an additional effect-magnitude check is
430    /// performed against the environment-adjusted threshold. If the magnitude
431    /// exceeds it, the decision is overridden to `Deny`.
432    pub fn evaluate_in_environment(
433        &self,
434        request: &GovernanceRequest,
435        env: &Environment,
436    ) -> GovernanceResult {
437        let adjusted_threshold = match &env.class {
438            EnvironmentClass::Development => {
439                // Dev: use the environment's risk threshold directly (lenient).
440                env.governance.risk_threshold
441            }
442            EnvironmentClass::Staging => {
443                // Staging: use the environment's risk threshold as-is.
444                env.governance.risk_threshold
445            }
446            EnvironmentClass::Production => {
447                // Production: halve the threshold for stricter gating.
448                env.governance.risk_threshold * 0.5
449            }
450            EnvironmentClass::Custom { risk_threshold, .. } => {
451                // Custom: use the class-level threshold.
452                *risk_threshold
453            }
454        };
455
456        // Run normal rule-based evaluation first.
457        let mut result = self.evaluate(request);
458
459        // Apply environment-scoped magnitude check on top.
460        let magnitude = request.effect.magnitude();
461        if magnitude > adjusted_threshold {
462            result.threshold_exceeded = true;
463            result.decision = GovernanceDecision::Deny(format!(
464                "effect magnitude {magnitude:.2} exceeds {} environment threshold {adjusted_threshold:.2}",
465                env.class,
466            ));
467        }
468
469        result
470    }
471
472    /// Evaluate a governance request and log the decision to the chain.
473    ///
474    /// This is the recommended entry point when a `ChainManager` is
475    /// available. It calls [`evaluate`](Self::evaluate) and records an
476    /// `ipc.dead_letter`-style audit event via [`ChainLoggable`].
477    ///
478    /// If no chain manager is provided, behaves identically to `evaluate`.
479    #[cfg(feature = "exochain")]
480    pub fn evaluate_logged(
481        &self,
482        request: &GovernanceRequest,
483        chain: Option<&crate::chain::ChainManager>,
484    ) -> GovernanceResult {
485        let result = self.evaluate(request);
486        if let Some(cm) = chain {
487            Self::chain_log_result(cm, request, &result);
488        }
489        result
490    }
491
492    /// Evaluate in an environment and log the decision to the chain.
493    #[cfg(feature = "exochain")]
494    pub fn evaluate_in_environment_logged(
495        &self,
496        request: &GovernanceRequest,
497        env: &Environment,
498        chain: Option<&crate::chain::ChainManager>,
499    ) -> GovernanceResult {
500        let result = self.evaluate_in_environment(request, env);
501        if let Some(cm) = chain {
502            Self::chain_log_result(cm, request, &result);
503        }
504        result
505    }
506
507    /// Log a governance result to the ExoChain.
508    ///
509    /// Can be called after any `evaluate` / `evaluate_in_environment`
510    /// call to record the decision in the audit trail.
511    #[cfg(feature = "exochain")]
512    pub fn chain_log_result(
513        cm: &crate::chain::ChainManager,
514        request: &GovernanceRequest,
515        result: &GovernanceResult,
516    ) {
517        use crate::chain::{ChainLoggable, GovernanceDecisionEvent};
518
519        let decision_str = match &result.decision {
520            GovernanceDecision::Permit => "Permit".to_owned(),
521            GovernanceDecision::PermitWithWarning(_) => "PermitWithWarning".to_owned(),
522            GovernanceDecision::EscalateToHuman(_) => "EscalateToHuman".to_owned(),
523            GovernanceDecision::Deny(_) => "Deny".to_owned(),
524        };
525
526        let event = GovernanceDecisionEvent {
527            agent_id: request.agent_id.clone(),
528            action: request.action.clone(),
529            decision: decision_str,
530            effect_magnitude: request.effect.magnitude(),
531            threshold_exceeded: result.threshold_exceeded,
532            evaluated_rules: result.evaluated_rules.clone(),
533            timestamp: chrono::Utc::now(),
534        };
535        cm.append_loggable(&event);
536    }
537}
538
539// ── Trajectory recording ─────────────────────────────────────
540//
541// Records agent decision points for learning, replay, and
542// pattern extraction. Lives alongside governance because every
543// trajectory point is a governed decision.
544
545/// Outcome of an agent decision for trajectory scoring.
546#[non_exhaustive]
547#[derive(Debug, Clone, Serialize, Deserialize)]
548pub enum TrajectoryOutcome {
549    /// Decision succeeded with a reward signal.
550    Success {
551        /// Reward value (higher is better).
552        reward: f64,
553    },
554    /// Decision failed.
555    Failure {
556        /// Reason for failure.
557        reason: String,
558    },
559    /// Outcome not yet known.
560    Pending,
561}
562
563/// A record of an agent's decision for learning and replay.
564#[derive(Debug, Clone, Serialize, Deserialize)]
565pub struct TrajectoryRecord {
566    /// Agent that made the decision.
567    pub agent_id: String,
568    /// What was decided (action name / tool call).
569    pub action: String,
570    /// Context at decision time (state snapshot).
571    pub context: serde_json::Value,
572    /// Outcome (success / failure / pending).
573    pub outcome: TrajectoryOutcome,
574    /// Timestamp of the decision.
575    pub timestamp: chrono::DateTime<chrono::Utc>,
576}
577
578/// Records agent trajectories for learning and pattern extraction.
579///
580/// Maintains a bounded FIFO buffer of [`TrajectoryRecord`]s. When the
581/// buffer is full the oldest record is evicted. Callers can query by
582/// agent and extract frequency patterns from successful actions.
583pub struct TrajectoryRecorder {
584    records: Vec<TrajectoryRecord>,
585    max_records: usize,
586}
587
588impl TrajectoryRecorder {
589    /// Create a recorder with the given capacity.
590    pub fn new(max_records: usize) -> Self {
591        Self {
592            records: Vec::new(),
593            max_records,
594        }
595    }
596
597    /// Record a trajectory point. Evicts the oldest record on overflow.
598    pub fn record(&mut self, record: TrajectoryRecord) {
599        if self.records.len() >= self.max_records {
600            self.records.remove(0); // FIFO eviction
601        }
602        self.records.push(record);
603    }
604
605    /// Get all records for a specific agent.
606    pub fn agent_trajectory(&self, agent_id: &str) -> Vec<&TrajectoryRecord> {
607        self.records
608            .iter()
609            .filter(|r| r.agent_id == agent_id)
610            .collect()
611    }
612
613    /// Extract patterns: returns `(action, count)` pairs for successful
614    /// actions, sorted by frequency descending.
615    pub fn extract_patterns(&self) -> Vec<(String, usize)> {
616        let mut action_counts: HashMap<String, usize> = HashMap::new();
617        for record in &self.records {
618            if matches!(record.outcome, TrajectoryOutcome::Success { .. }) {
619                *action_counts.entry(record.action.clone()).or_default() += 1;
620            }
621        }
622        let mut patterns: Vec<_> = action_counts.into_iter().collect();
623        patterns.sort_by(|a, b| b.1.cmp(&a.1));
624        patterns
625    }
626
627    /// Number of recorded trajectories.
628    pub fn len(&self) -> usize {
629        self.records.len()
630    }
631
632    /// Whether the recorder is empty.
633    pub fn is_empty(&self) -> bool {
634        self.records.is_empty()
635    }
636}
637
638// ── RVF governance bridge ────────────────────────────────────
639//
640// Behind `exochain` feature gate: bidirectional mapping between
641// WeftOS constitutional governance and RVF witness governance.
642//
643// WeftOS governance evaluates *whether* an action should proceed
644// (effect algebra, risk thresholds, branch-based rules).
645// RVF governance records *what happened* during execution
646// (witness bundles, tool call traces, cost budgets).
647
648#[cfg(feature = "exochain")]
649impl GovernanceDecision {
650    /// Map this decision to the equivalent RVF PolicyCheck.
651    ///
652    /// - `Permit` → `Allowed`
653    /// - `PermitWithWarning` / `EscalateToHuman` → `Confirmed`
654    /// - `Deny` → `Denied`
655    pub fn to_rvf_policy_check(&self) -> rvf_types::witness::PolicyCheck {
656        match self {
657            GovernanceDecision::Permit => rvf_types::witness::PolicyCheck::Allowed,
658            GovernanceDecision::PermitWithWarning(_) => rvf_types::witness::PolicyCheck::Confirmed,
659            GovernanceDecision::EscalateToHuman(_) => rvf_types::witness::PolicyCheck::Confirmed,
660            GovernanceDecision::Deny(_) => rvf_types::witness::PolicyCheck::Denied,
661        }
662    }
663}
664
665#[cfg(feature = "exochain")]
666impl GovernanceEngine {
667    /// Derive the equivalent RVF GovernanceMode from this engine's config.
668    ///
669    /// - `risk_threshold >= 1.0` (open) → `Autonomous`
670    /// - `human_approval_required` → `Approved`
671    /// - otherwise → `Restricted`
672    pub fn to_rvf_mode(&self) -> rvf_types::witness::GovernanceMode {
673        if self.risk_threshold >= 1.0 {
674            rvf_types::witness::GovernanceMode::Autonomous
675        } else if self.human_approval_required {
676            rvf_types::witness::GovernanceMode::Approved
677        } else {
678            rvf_types::witness::GovernanceMode::Restricted
679        }
680    }
681
682    /// Build an RVF GovernancePolicy from this engine's configuration.
683    ///
684    /// Uses the default tool lists and cost budgets for each mode.
685    /// Callers can customize the returned policy further if needed.
686    pub fn to_rvf_policy(&self) -> rvf_runtime::GovernancePolicy {
687        match self.to_rvf_mode() {
688            rvf_types::witness::GovernanceMode::Restricted => {
689                rvf_runtime::GovernancePolicy::restricted()
690            }
691            rvf_types::witness::GovernanceMode::Approved => {
692                rvf_runtime::GovernancePolicy::approved()
693            }
694            rvf_types::witness::GovernanceMode::Autonomous => {
695                rvf_runtime::GovernancePolicy::autonomous()
696            }
697        }
698    }
699}
700
701#[cfg(feature = "exochain")]
702impl GovernanceResult {
703    /// Map the decision to an RVF TaskOutcome.
704    ///
705    /// This is a convenience for recording the governance result in a
706    /// witness bundle. The caller should override based on actual execution.
707    pub fn to_rvf_task_outcome(&self) -> rvf_types::witness::TaskOutcome {
708        match &self.decision {
709            GovernanceDecision::Permit | GovernanceDecision::PermitWithWarning(_) => {
710                rvf_types::witness::TaskOutcome::Solved
711            }
712            GovernanceDecision::EscalateToHuman(_) => rvf_types::witness::TaskOutcome::Skipped,
713            GovernanceDecision::Deny(_) => rvf_types::witness::TaskOutcome::Failed,
714        }
715    }
716}
717
718#[cfg(test)]
719mod tests {
720    use super::*;
721
722    fn make_rule(id: &str, severity: RuleSeverity, branch: GovernanceBranch) -> GovernanceRule {
723        GovernanceRule {
724            id: id.into(),
725            description: format!("Test rule {id}"),
726            branch,
727            severity,
728            active: true,
729            reference_url: None,
730            sop_category: None,
731        }
732    }
733
734    #[test]
735    fn effect_vector_magnitude() {
736        let v = EffectVector {
737            risk: 0.3,
738            fairness: 0.4,
739            privacy: 0.0,
740            novelty: 0.0,
741            security: 0.0,
742        };
743        assert!((v.magnitude() - 0.5).abs() < 0.001);
744    }
745
746    #[test]
747    fn effect_vector_zero() {
748        let v = EffectVector::default();
749        assert!((v.magnitude() - 0.0).abs() < f64::EPSILON);
750    }
751
752    #[test]
753    fn effect_any_exceeds() {
754        let v = EffectVector {
755            risk: 0.8,
756            ..Default::default()
757        };
758        assert!(v.any_exceeds(0.5));
759        assert!(!v.any_exceeds(0.9));
760    }
761
762    #[test]
763    fn effect_max_dimension() {
764        let v = EffectVector {
765            risk: 0.2,
766            fairness: 0.5,
767            privacy: 0.3,
768            novelty: 0.1,
769            security: 0.4,
770        };
771        assert!((v.max_dimension() - 0.5).abs() < f64::EPSILON);
772    }
773
774    #[test]
775    fn governance_branch_display() {
776        assert_eq!(GovernanceBranch::Legislative.to_string(), "legislative");
777        assert_eq!(GovernanceBranch::Executive.to_string(), "executive");
778        assert_eq!(GovernanceBranch::Judicial.to_string(), "judicial");
779    }
780
781    #[test]
782    fn rule_severity_ordering() {
783        assert!(RuleSeverity::Advisory < RuleSeverity::Warning);
784        assert!(RuleSeverity::Warning < RuleSeverity::Blocking);
785        assert!(RuleSeverity::Blocking < RuleSeverity::Critical);
786    }
787
788    #[test]
789    fn governance_decision_display() {
790        assert_eq!(GovernanceDecision::Permit.to_string(), "permit");
791        assert!(
792            GovernanceDecision::Deny("too risky".into())
793                .to_string()
794                .contains("too risky")
795        );
796    }
797
798    #[test]
799    fn open_engine_permits_everything() {
800        let engine = GovernanceEngine::open();
801        let request = GovernanceRequest {
802            agent_id: "agent-1".into(),
803            action: "deploy".into(),
804            effect: EffectVector {
805                risk: 0.9,
806                security: 0.9,
807                ..Default::default()
808            },
809            context: Default::default(),
810            node_id: None,
811        };
812        let result = engine.evaluate(&request);
813        assert_eq!(result.decision, GovernanceDecision::Permit);
814    }
815
816    #[test]
817    fn blocking_rule_denies() {
818        let mut engine = GovernanceEngine::new(0.5, false);
819        engine.add_rule(make_rule(
820            "security-check",
821            RuleSeverity::Blocking,
822            GovernanceBranch::Judicial,
823        ));
824
825        let request = GovernanceRequest {
826            agent_id: "agent-1".into(),
827            action: "deploy".into(),
828            effect: EffectVector {
829                risk: 0.6,
830                ..Default::default()
831            },
832            context: Default::default(),
833            node_id: None,
834        };
835        let result = engine.evaluate(&request);
836        assert!(matches!(result.decision, GovernanceDecision::Deny(_)));
837        assert!(result.threshold_exceeded);
838    }
839
840    #[test]
841    fn blocking_with_human_approval_escalates() {
842        let mut engine = GovernanceEngine::new(0.5, true);
843        engine.add_rule(make_rule(
844            "security-check",
845            RuleSeverity::Blocking,
846            GovernanceBranch::Judicial,
847        ));
848
849        let request = GovernanceRequest {
850            agent_id: "agent-1".into(),
851            action: "deploy".into(),
852            effect: EffectVector {
853                risk: 0.6,
854                ..Default::default()
855            },
856            context: Default::default(),
857            node_id: None,
858        };
859        let result = engine.evaluate(&request);
860        assert!(matches!(
861            result.decision,
862            GovernanceDecision::EscalateToHuman(_)
863        ));
864    }
865
866    #[test]
867    fn warning_rule_permits_with_warning() {
868        let mut engine = GovernanceEngine::new(0.5, false);
869        engine.add_rule(make_rule(
870            "risk-check",
871            RuleSeverity::Warning,
872            GovernanceBranch::Executive,
873        ));
874
875        let request = GovernanceRequest {
876            agent_id: "agent-1".into(),
877            action: "deploy".into(),
878            effect: EffectVector {
879                risk: 0.6,
880                ..Default::default()
881            },
882            context: Default::default(),
883            node_id: None,
884        };
885        let result = engine.evaluate(&request);
886        assert!(matches!(
887            result.decision,
888            GovernanceDecision::PermitWithWarning(_)
889        ));
890    }
891
892    #[test]
893    fn below_threshold_permits() {
894        let mut engine = GovernanceEngine::new(0.5, false);
895        engine.add_rule(make_rule(
896            "security-check",
897            RuleSeverity::Blocking,
898            GovernanceBranch::Judicial,
899        ));
900
901        let request = GovernanceRequest {
902            agent_id: "agent-1".into(),
903            action: "read".into(),
904            effect: EffectVector {
905                risk: 0.1,
906                ..Default::default()
907            },
908            context: Default::default(),
909            node_id: None,
910        };
911        let result = engine.evaluate(&request);
912        assert_eq!(result.decision, GovernanceDecision::Permit);
913        assert!(!result.threshold_exceeded);
914    }
915
916    #[test]
917    fn rules_by_branch() {
918        let mut engine = GovernanceEngine::new(0.5, false);
919        engine.add_rule(make_rule(
920            "r1",
921            RuleSeverity::Warning,
922            GovernanceBranch::Legislative,
923        ));
924        engine.add_rule(make_rule(
925            "r2",
926            RuleSeverity::Blocking,
927            GovernanceBranch::Judicial,
928        ));
929        engine.add_rule(make_rule(
930            "r3",
931            RuleSeverity::Advisory,
932            GovernanceBranch::Judicial,
933        ));
934
935        let judicial = engine.rules_by_branch(&GovernanceBranch::Judicial);
936        assert_eq!(judicial.len(), 2);
937        let legislative = engine.rules_by_branch(&GovernanceBranch::Legislative);
938        assert_eq!(legislative.len(), 1);
939    }
940
941    #[test]
942    fn inactive_rules_excluded() {
943        let mut engine = GovernanceEngine::new(0.5, false);
944        engine.add_rule(GovernanceRule {
945            id: "disabled".into(),
946            description: "Disabled rule".into(),
947            branch: GovernanceBranch::Judicial,
948            severity: RuleSeverity::Blocking,
949            active: false,
950            reference_url: None,
951            sop_category: None,
952        });
953        assert_eq!(engine.active_rules().len(), 0);
954    }
955
956    #[test]
957    fn governance_rule_serde_roundtrip() {
958        let rule = make_rule("sec-1", RuleSeverity::Critical, GovernanceBranch::Judicial);
959        let json = serde_json::to_string(&rule).unwrap();
960        let restored: GovernanceRule = serde_json::from_str(&json).unwrap();
961        assert_eq!(restored.id, "sec-1");
962        assert!(restored.active);
963    }
964
965    #[test]
966    fn governance_request_serde_roundtrip() {
967        let request = GovernanceRequest {
968            agent_id: "agent-1".into(),
969            action: "deploy".into(),
970            effect: EffectVector {
971                risk: 0.5,
972                privacy: 0.3,
973                ..Default::default()
974            },
975            context: std::collections::HashMap::from([("env".into(), "prod".into())]),
976            node_id: None,
977        };
978        let json = serde_json::to_string(&request).unwrap();
979        let restored: GovernanceRequest = serde_json::from_str(&json).unwrap();
980        assert_eq!(restored.agent_id, "agent-1");
981        assert!((restored.effect.risk - 0.5).abs() < f64::EPSILON);
982        assert!(restored.node_id.is_none());
983    }
984
985    #[test]
986    fn governance_request_with_node_id() {
987        let request = GovernanceRequest::new("agent-1", "deploy")
988            .with_node_id("node-42")
989            .with_effect(EffectVector { risk: 0.1, ..Default::default() });
990
991        assert_eq!(request.node_id.as_deref(), Some("node-42"));
992        assert_eq!(request.agent_id, "agent-1");
993
994        // Serde roundtrip preserves node_id.
995        let json = serde_json::to_string(&request).unwrap();
996        assert!(json.contains("node-42"));
997        let restored: GovernanceRequest = serde_json::from_str(&json).unwrap();
998        assert_eq!(restored.node_id.as_deref(), Some("node-42"));
999    }
1000
1001    #[test]
1002    fn governance_request_without_node_id_deserializes() {
1003        // JSON without node_id should deserialize with node_id = None (backward compat).
1004        let json = r#"{"agent_id":"a","action":"b","effect":{},"context":{}}"#;
1005        let request: GovernanceRequest = serde_json::from_str(json).unwrap();
1006        assert!(request.node_id.is_none());
1007    }
1008
1009    #[test]
1010    fn governance_request_builder() {
1011        let request = GovernanceRequest::new("agent-1", "deploy");
1012        assert_eq!(request.agent_id, "agent-1");
1013        assert_eq!(request.action, "deploy");
1014        assert!(request.node_id.is_none());
1015        assert!((request.effect.magnitude() - 0.0).abs() < f64::EPSILON);
1016    }
1017
1018    #[test]
1019    fn effect_vector_serde_roundtrip() {
1020        let v = EffectVector {
1021            risk: 0.1,
1022            fairness: 0.2,
1023            privacy: 0.3,
1024            novelty: 0.4,
1025            security: 0.5,
1026        };
1027        let json = serde_json::to_string(&v).unwrap();
1028        let restored: EffectVector = serde_json::from_str(&json).unwrap();
1029        assert!((restored.security - 0.5).abs() < f64::EPSILON);
1030    }
1031
1032    #[test]
1033    fn filter_rules_by_sop_category() {
1034        let rules = vec![
1035            GovernanceRule {
1036                id: "SOP-L001".into(),
1037                description: "test".into(),
1038                branch: GovernanceBranch::Legislative,
1039                severity: RuleSeverity::Blocking,
1040                active: true,
1041                reference_url: Some("https://example.com".into()),
1042                sop_category: Some("governance".into()),
1043            },
1044            GovernanceRule {
1045                id: "SOP-J001".into(),
1046                description: "test".into(),
1047                branch: GovernanceBranch::Judicial,
1048                severity: RuleSeverity::Blocking,
1049                active: true,
1050                reference_url: Some("https://example.com".into()),
1051                sop_category: Some("ethics".into()),
1052            },
1053            GovernanceRule {
1054                id: "GOV-001".into(),
1055                description: "test".into(),
1056                branch: GovernanceBranch::Judicial,
1057                severity: RuleSeverity::Blocking,
1058                active: true,
1059                reference_url: None,
1060                sop_category: None,
1061            },
1062        ];
1063        let ethics = GovernanceRule::filter_by_category(&rules, "ethics");
1064        assert_eq!(ethics.len(), 1);
1065        assert_eq!(ethics[0].id, "SOP-J001");
1066
1067        let governance = GovernanceRule::filter_by_category(&rules, "governance");
1068        assert_eq!(governance.len(), 1);
1069
1070        let none = GovernanceRule::filter_by_category(&rules, "nonexistent");
1071        assert!(none.is_empty());
1072    }
1073
1074    #[test]
1075    fn governance_rule_with_sop_serde() {
1076        let rule = GovernanceRule {
1077            id: "SOP-L001".into(),
1078            description: "test".into(),
1079            branch: GovernanceBranch::Legislative,
1080            severity: RuleSeverity::Blocking,
1081            active: true,
1082            reference_url: Some("https://example.com/sop".into()),
1083            sop_category: Some("governance".into()),
1084        };
1085        let json = serde_json::to_string(&rule).unwrap();
1086        assert!(json.contains("reference_url"));
1087        assert!(json.contains("sop_category"));
1088        let restored: GovernanceRule = serde_json::from_str(&json).unwrap();
1089        assert_eq!(restored.reference_url, Some("https://example.com/sop".into()));
1090    }
1091
1092    #[test]
1093    fn governance_rule_without_sop_backward_compat() {
1094        // Old-format JSON without new fields should deserialize fine
1095        let json = r#"{"id":"GOV-001","description":"test","branch":"Judicial","severity":"Blocking","active":true}"#;
1096        let rule: GovernanceRule = serde_json::from_str(json).unwrap();
1097        assert!(rule.reference_url.is_none());
1098        assert!(rule.sop_category.is_none());
1099    }
1100
1101    // ── Genesis rule enforcement tests ──────────────────────────────
1102
1103    /// Helper to create a GovernanceRule with optional SOP category,
1104    /// matching the shape used in boot.rs genesis rules.
1105    fn make_sop_rule(
1106        id: &str,
1107        severity: RuleSeverity,
1108        branch: GovernanceBranch,
1109        category: Option<&str>,
1110    ) -> GovernanceRule {
1111        GovernanceRule {
1112            id: id.into(),
1113            description: format!("Genesis rule {id}"),
1114            branch,
1115            severity,
1116            active: true,
1117            reference_url: None,
1118            sop_category: category.map(|c| c.into()),
1119        }
1120    }
1121
1122    /// Build a GovernanceEngine with all 22 genesis rules matching boot.rs.
1123    fn genesis_engine() -> GovernanceEngine {
1124        let mut engine = GovernanceEngine::new(0.7, false);
1125
1126        // ── Core constitutional rules (GOV-001 .. GOV-007) ──────
1127        // Judicial blocking
1128        engine.add_rule(make_sop_rule("GOV-001", RuleSeverity::Blocking, GovernanceBranch::Judicial, None));
1129        engine.add_rule(make_sop_rule("GOV-002", RuleSeverity::Blocking, GovernanceBranch::Judicial, None));
1130        // Legislative warning
1131        engine.add_rule(make_sop_rule("GOV-003", RuleSeverity::Warning, GovernanceBranch::Legislative, None));
1132        // Executive advisory
1133        engine.add_rule(make_sop_rule("GOV-004", RuleSeverity::Advisory, GovernanceBranch::Executive, None));
1134        // Legislative warning
1135        engine.add_rule(make_sop_rule("GOV-005", RuleSeverity::Warning, GovernanceBranch::Legislative, None));
1136        // Executive blocking
1137        engine.add_rule(make_sop_rule("GOV-006", RuleSeverity::Blocking, GovernanceBranch::Executive, None));
1138        // Judicial advisory
1139        engine.add_rule(make_sop_rule("GOV-007", RuleSeverity::Advisory, GovernanceBranch::Judicial, None));
1140
1141        // ── AI-SDLC SOP rules: Legislative (6) ──────────────────
1142        engine.add_rule(make_sop_rule("SOP-L001", RuleSeverity::Blocking, GovernanceBranch::Legislative, Some("governance")));
1143        engine.add_rule(make_sop_rule("SOP-L002", RuleSeverity::Warning, GovernanceBranch::Legislative, Some("governance")));
1144        engine.add_rule(make_sop_rule("SOP-L003", RuleSeverity::Warning, GovernanceBranch::Legislative, Some("engineering")));
1145        engine.add_rule(make_sop_rule("SOP-L004", RuleSeverity::Advisory, GovernanceBranch::Legislative, Some("lifecycle")));
1146        engine.add_rule(make_sop_rule("SOP-L005", RuleSeverity::Blocking, GovernanceBranch::Legislative, Some("ethics")));
1147        engine.add_rule(make_sop_rule("SOP-L006", RuleSeverity::Warning, GovernanceBranch::Legislative, Some("governance")));
1148
1149        // ── AI-SDLC SOP rules: Executive (5) ────────────────────
1150        engine.add_rule(make_sop_rule("SOP-E001", RuleSeverity::Warning, GovernanceBranch::Executive, Some("engineering")));
1151        engine.add_rule(make_sop_rule("SOP-E002", RuleSeverity::Blocking, GovernanceBranch::Executive, Some("lifecycle")));
1152        engine.add_rule(make_sop_rule("SOP-E003", RuleSeverity::Warning, GovernanceBranch::Executive, Some("security")));
1153        engine.add_rule(make_sop_rule("SOP-E004", RuleSeverity::Advisory, GovernanceBranch::Executive, Some("lifecycle")));
1154        engine.add_rule(make_sop_rule("SOP-E005", RuleSeverity::Advisory, GovernanceBranch::Executive, Some("governance")));
1155
1156        // ── AI-SDLC SOP rules: Judicial (4) ─────────────────────
1157        engine.add_rule(make_sop_rule("SOP-J001", RuleSeverity::Blocking, GovernanceBranch::Judicial, Some("ethics")));
1158        engine.add_rule(make_sop_rule("SOP-J002", RuleSeverity::Warning, GovernanceBranch::Judicial, Some("ethics")));
1159        engine.add_rule(make_sop_rule("SOP-J003", RuleSeverity::Warning, GovernanceBranch::Judicial, Some("lifecycle")));
1160        engine.add_rule(make_sop_rule("SOP-J004", RuleSeverity::Advisory, GovernanceBranch::Judicial, Some("quality")));
1161
1162        engine
1163    }
1164
1165    #[test]
1166    fn genesis_has_22_rules() {
1167        let engine = genesis_engine();
1168        assert_eq!(engine.rule_count(), 22);
1169    }
1170
1171    #[test]
1172    fn genesis_high_risk_operation_blocked() {
1173        let engine = genesis_engine();
1174        let request = GovernanceRequest {
1175            agent_id: "agent-1".into(),
1176            action: "deploy-to-prod".into(),
1177            effect: EffectVector { risk: 0.9, ..Default::default() },
1178            context: Default::default(),
1179            node_id: None,
1180        };
1181        let result = engine.evaluate(&request);
1182        assert!(
1183            matches!(result.decision, GovernanceDecision::Deny(_)),
1184            "high-risk operation should be denied, got {:?}", result.decision,
1185        );
1186        assert!(result.threshold_exceeded);
1187    }
1188
1189    #[test]
1190    fn genesis_low_risk_operation_permitted() {
1191        let engine = genesis_engine();
1192        let request = GovernanceRequest {
1193            agent_id: "agent-1".into(),
1194            action: "read-file".into(),
1195            effect: EffectVector { risk: 0.1, ..Default::default() },
1196            context: Default::default(),
1197            node_id: None,
1198        };
1199        let result = engine.evaluate(&request);
1200        assert_eq!(result.decision, GovernanceDecision::Permit);
1201        assert!(!result.threshold_exceeded);
1202    }
1203
1204    #[test]
1205    fn genesis_privacy_violation_triggers_enforcement() {
1206        let engine = genesis_engine();
1207        let request = GovernanceRequest {
1208            agent_id: "agent-1".into(),
1209            action: "access-user-data".into(),
1210            effect: EffectVector { privacy: 0.8, ..Default::default() },
1211            context: Default::default(),
1212            node_id: None,
1213        };
1214        let result = engine.evaluate(&request);
1215        // magnitude = 0.8 > 0.7 threshold; blocking rules exist -> Deny
1216        assert!(
1217            matches!(result.decision, GovernanceDecision::Deny(_) | GovernanceDecision::PermitWithWarning(_)),
1218            "privacy violation should trigger warning or deny, got {:?}", result.decision,
1219        );
1220        assert!(result.threshold_exceeded);
1221    }
1222
1223    #[test]
1224    fn genesis_security_sensitive_blocked() {
1225        // GOV-002: security-sensitive actions blocked when threshold exceeded
1226        let engine = genesis_engine();
1227        let request = GovernanceRequest {
1228            agent_id: "agent-1".into(),
1229            action: "modify-firewall".into(),
1230            effect: EffectVector { security: 0.9, risk: 0.5, ..Default::default() },
1231            context: Default::default(),
1232            node_id: None,
1233        };
1234        let result = engine.evaluate(&request);
1235        // magnitude = sqrt(0.81 + 0.25) ~ 1.03 > 0.7
1236        assert!(matches!(result.decision, GovernanceDecision::Deny(_)));
1237    }
1238
1239    #[test]
1240    fn genesis_fairness_bias_blocked() {
1241        // SOP-J001: bias/fairness evaluation blocking
1242        let engine = genesis_engine();
1243        let request = GovernanceRequest {
1244            agent_id: "ml-agent".into(),
1245            action: "evaluate-candidate".into(),
1246            effect: EffectVector { fairness: 0.9, ..Default::default() },
1247            context: Default::default(),
1248            node_id: None,
1249        };
1250        let result = engine.evaluate(&request);
1251        assert!(
1252            matches!(result.decision, GovernanceDecision::Deny(_)),
1253            "fairness violation should be blocked by SOP-J001, got {:?}", result.decision,
1254        );
1255    }
1256
1257    #[test]
1258    fn genesis_agent_spawn_blocked_when_risky() {
1259        // GOV-006 + SOP-E002: agent spawn with high risk denied
1260        let engine = genesis_engine();
1261        let request = GovernanceRequest {
1262            agent_id: "orchestrator".into(),
1263            action: "agent.spawn".into(),
1264            effect: EffectVector { risk: 0.8, novelty: 0.5, ..Default::default() },
1265            context: Default::default(),
1266            node_id: None,
1267        };
1268        let result = engine.evaluate(&request);
1269        // magnitude = sqrt(0.64 + 0.25) ~ 0.94 > 0.7
1270        assert!(matches!(result.decision, GovernanceDecision::Deny(_)));
1271    }
1272
1273    #[test]
1274    fn genesis_agent_spawn_permitted_when_safe() {
1275        // GOV-006: spawn with low effect should be permitted
1276        let engine = genesis_engine();
1277        let request = GovernanceRequest {
1278            agent_id: "orchestrator".into(),
1279            action: "agent.spawn".into(),
1280            effect: EffectVector { risk: 0.1, ..Default::default() },
1281            context: Default::default(),
1282            node_id: None,
1283        };
1284        let result = engine.evaluate(&request);
1285        assert_eq!(result.decision, GovernanceDecision::Permit);
1286    }
1287
1288    #[test]
1289    fn genesis_data_protection_blocks_high_privacy() {
1290        // SOP-L005: data protection blocking
1291        let engine = genesis_engine();
1292        let request = GovernanceRequest {
1293            agent_id: "data-agent".into(),
1294            action: "export-pii".into(),
1295            effect: EffectVector { privacy: 0.9, risk: 0.3, ..Default::default() },
1296            context: Default::default(),
1297            node_id: None,
1298        };
1299        let result = engine.evaluate(&request);
1300        // magnitude = sqrt(0.81 + 0.09) ~ 0.95 > 0.7
1301        assert!(matches!(result.decision, GovernanceDecision::Deny(_)));
1302    }
1303
1304    #[test]
1305    fn genesis_with_human_approval_escalates() {
1306        // Same rules but with human_approval_required = true
1307        let mut engine = GovernanceEngine::new(0.7, true);
1308        engine.add_rule(make_sop_rule("GOV-001", RuleSeverity::Blocking, GovernanceBranch::Judicial, None));
1309
1310        let request = GovernanceRequest {
1311            agent_id: "agent-1".into(),
1312            action: "high-risk-op".into(),
1313            effect: EffectVector { risk: 0.9, ..Default::default() },
1314            context: Default::default(),
1315            node_id: None,
1316        };
1317        let result = engine.evaluate(&request);
1318        assert!(
1319            matches!(result.decision, GovernanceDecision::EscalateToHuman(_)),
1320            "with human_approval, blocking should escalate, got {:?}", result.decision,
1321        );
1322    }
1323
1324    #[test]
1325    fn genesis_all_branches_represented() {
1326        let engine = genesis_engine();
1327        let legislative = engine.rules_by_branch(&GovernanceBranch::Legislative);
1328        let executive = engine.rules_by_branch(&GovernanceBranch::Executive);
1329        let judicial = engine.rules_by_branch(&GovernanceBranch::Judicial);
1330
1331        // Legislative: GOV-003, GOV-005, SOP-L001..L006 = 8
1332        assert_eq!(legislative.len(), 8, "legislative should have 8 rules, got {}", legislative.len());
1333        // Executive: GOV-004, GOV-006, SOP-E001..E005 = 7
1334        assert_eq!(executive.len(), 7, "executive should have 7 rules, got {}", executive.len());
1335        // Judicial: GOV-001, GOV-002, GOV-007, SOP-J001..J004 = 7
1336        assert_eq!(judicial.len(), 7, "judicial should have 7 rules, got {}", judicial.len());
1337    }
1338
1339    #[test]
1340    fn genesis_blocking_rules_count() {
1341        let engine = genesis_engine();
1342        let blocking_count = engine.active_rules().iter()
1343            .filter(|r| matches!(r.severity, RuleSeverity::Blocking | RuleSeverity::Critical))
1344            .count();
1345        // GOV-001, GOV-002, GOV-006, SOP-L001, SOP-L005, SOP-E002, SOP-J001 = 7
1346        assert_eq!(blocking_count, 7, "should have exactly 7 blocking rules");
1347    }
1348
1349    #[test]
1350    fn genesis_warning_rules_count() {
1351        let engine = genesis_engine();
1352        let warning_count = engine.active_rules().iter()
1353            .filter(|r| matches!(r.severity, RuleSeverity::Warning))
1354            .count();
1355        // GOV-003, GOV-005, SOP-L002, SOP-L003, SOP-L006,
1356        // SOP-E001, SOP-E003, SOP-J002, SOP-J003 = 9
1357        assert_eq!(warning_count, 9, "should have exactly 9 warning rules");
1358    }
1359
1360    #[test]
1361    fn genesis_advisory_rules_count() {
1362        let engine = genesis_engine();
1363        let advisory_count = engine.active_rules().iter()
1364            .filter(|r| matches!(r.severity, RuleSeverity::Advisory))
1365            .count();
1366        // GOV-004, GOV-007, SOP-L004, SOP-E004, SOP-E005, SOP-J004 = 6
1367        assert_eq!(advisory_count, 6, "should have exactly 6 advisory rules");
1368    }
1369
1370    #[test]
1371    fn genesis_moderate_risk_with_only_warnings_permits_with_warning() {
1372        // Only warning-severity rules, no blocking: should PermitWithWarning
1373        let mut engine = GovernanceEngine::new(0.7, false);
1374        engine.add_rule(make_sop_rule("SOP-L002", RuleSeverity::Warning, GovernanceBranch::Legislative, Some("governance")));
1375        engine.add_rule(make_sop_rule("SOP-E001", RuleSeverity::Warning, GovernanceBranch::Executive, Some("engineering")));
1376
1377        let request = GovernanceRequest {
1378            agent_id: "dev-agent".into(),
1379            action: "write-code".into(),
1380            effect: EffectVector { risk: 0.5, novelty: 0.5, ..Default::default() },
1381            context: Default::default(),
1382            node_id: None,
1383        };
1384        let result = engine.evaluate(&request);
1385        // magnitude ~ 0.71 > 0.7, only warnings -> PermitWithWarning
1386        assert!(
1387            matches!(result.decision, GovernanceDecision::PermitWithWarning(_)),
1388            "warning-only rules above threshold should PermitWithWarning, got {:?}", result.decision,
1389        );
1390    }
1391
1392    #[test]
1393    fn genesis_advisory_only_permits_above_threshold() {
1394        // Advisory rules alone never block or warn -- action is permitted
1395        let mut engine = GovernanceEngine::new(0.7, false);
1396        engine.add_rule(make_sop_rule("GOV-004", RuleSeverity::Advisory, GovernanceBranch::Executive, None));
1397        engine.add_rule(make_sop_rule("GOV-007", RuleSeverity::Advisory, GovernanceBranch::Judicial, None));
1398
1399        let request = GovernanceRequest {
1400            agent_id: "agent-1".into(),
1401            action: "novel-action".into(),
1402            effect: EffectVector { novelty: 0.9, ..Default::default() },
1403            context: Default::default(),
1404            node_id: None,
1405        };
1406        let result = engine.evaluate(&request);
1407        assert_eq!(result.decision, GovernanceDecision::Permit,
1408            "advisory-only rules should still permit, got {:?}", result.decision);
1409        assert!(result.threshold_exceeded);
1410    }
1411
1412    #[test]
1413    fn genesis_evaluates_all_22_rules() {
1414        let engine = genesis_engine();
1415        let request = GovernanceRequest {
1416            agent_id: "agent-1".into(),
1417            action: "any-action".into(),
1418            effect: EffectVector { risk: 0.9, ..Default::default() },
1419            context: Default::default(),
1420            node_id: None,
1421        };
1422        let result = engine.evaluate(&request);
1423        assert_eq!(
1424            result.evaluated_rules.len(), 22,
1425            "all 22 rules should be evaluated, got {}", result.evaluated_rules.len(),
1426        );
1427    }
1428
1429    #[test]
1430    fn genesis_sop_categories_present() {
1431        let engine = genesis_engine();
1432        let all_rules = engine.active_rules();
1433        let categorized: Vec<_> = all_rules.iter()
1434            .filter(|r| r.sop_category.is_some())
1435            .collect();
1436        // 15 SOP rules have categories, 7 GOV rules do not
1437        assert_eq!(categorized.len(), 15, "15 SOP rules should have categories");
1438
1439        let categories: std::collections::HashSet<_> = categorized.iter()
1440            .map(|r| r.sop_category.as_deref().unwrap())
1441            .collect();
1442        assert!(categories.contains("governance"));
1443        assert!(categories.contains("ethics"));
1444        assert!(categories.contains("engineering"));
1445        assert!(categories.contains("lifecycle"));
1446        assert!(categories.contains("security"));
1447        assert!(categories.contains("quality"));
1448    }
1449
1450    #[test]
1451    fn genesis_multi_dimension_high_effect_denied() {
1452        // Multiple dimensions contributing to high magnitude
1453        let engine = genesis_engine();
1454        let request = GovernanceRequest {
1455            agent_id: "agent-1".into(),
1456            action: "risky-novel-private".into(),
1457            effect: EffectVector {
1458                risk: 0.4,
1459                privacy: 0.4,
1460                novelty: 0.4,
1461                security: 0.4,
1462                fairness: 0.0,
1463            },
1464            context: Default::default(),
1465            node_id: None,
1466        };
1467        let result = engine.evaluate(&request);
1468        // magnitude = sqrt(4 * 0.16) = sqrt(0.64) = 0.8 > 0.7
1469        assert!(
1470            matches!(result.decision, GovernanceDecision::Deny(_)),
1471            "combined multi-dimension effect should exceed threshold and deny, got {:?}",
1472            result.decision,
1473        );
1474        assert!(result.threshold_exceeded);
1475    }
1476
1477    // ── Environment-scoped governance tests ─────────────────────
1478
1479    fn make_env(class: crate::environment::EnvironmentClass, risk_threshold: f64) -> crate::environment::Environment {
1480        crate::environment::Environment {
1481            id: format!("{class}"),
1482            name: format!("{class}"),
1483            class,
1484            governance: crate::environment::GovernanceScope {
1485                risk_threshold,
1486                ..Default::default()
1487            },
1488            labels: std::collections::HashMap::new(),
1489        }
1490    }
1491
1492    #[test]
1493    fn dev_environment_allows_higher_risk() {
1494        use crate::environment::EnvironmentClass;
1495
1496        // Use an open engine (threshold 1.0) so the environment scope
1497        // is the controlling factor, not the engine's own threshold.
1498        let engine = GovernanceEngine::open();
1499
1500        // Dev environment with risk_threshold 0.9 -- lenient.
1501        let dev_env = make_env(EnvironmentClass::Development, 0.9);
1502
1503        // High-risk request: magnitude 0.8
1504        let request = GovernanceRequest::new("agent-1", "deploy")
1505            .with_effect(EffectVector { risk: 0.8, ..Default::default() });
1506
1507        let result = engine.evaluate_in_environment(&request, &dev_env);
1508        // 0.8 < 0.9 (dev threshold) => should be permitted.
1509        assert_eq!(
1510            result.decision,
1511            GovernanceDecision::Permit,
1512            "dev should allow risk 0.8 with threshold 0.9, got {:?}",
1513            result.decision,
1514        );
1515    }
1516
1517    #[test]
1518    fn prod_environment_blocks_high_risk() {
1519        use crate::environment::EnvironmentClass;
1520
1521        let mut engine = GovernanceEngine::new(1.0, false);
1522        engine.add_rule(make_rule(
1523            "sec-check",
1524            RuleSeverity::Blocking,
1525            GovernanceBranch::Judicial,
1526        ));
1527
1528        // Prod environment with risk_threshold 0.6 -- halved to 0.3.
1529        let prod_env = make_env(EnvironmentClass::Production, 0.6);
1530
1531        // Moderate request: magnitude 0.4
1532        let request = GovernanceRequest::new("agent-1", "deploy")
1533            .with_effect(EffectVector { risk: 0.4, ..Default::default() });
1534
1535        let result = engine.evaluate_in_environment(&request, &prod_env);
1536        // 0.4 > 0.3 (prod adjusted = 0.6 * 0.5) => denied.
1537        assert!(
1538            matches!(result.decision, GovernanceDecision::Deny(_)),
1539            "prod should deny risk 0.4 with adjusted threshold 0.3, got {:?}",
1540            result.decision,
1541        );
1542    }
1543
1544    #[test]
1545    fn staging_uses_normal_thresholds() {
1546        use crate::environment::EnvironmentClass;
1547
1548        let mut engine = GovernanceEngine::new(1.0, false);
1549        engine.add_rule(make_rule(
1550            "sec-check",
1551            RuleSeverity::Blocking,
1552            GovernanceBranch::Judicial,
1553        ));
1554
1555        // Staging environment with risk_threshold 0.6.
1556        let staging_env = make_env(EnvironmentClass::Staging, 0.6);
1557
1558        // Moderate request: magnitude 0.5
1559        let request = GovernanceRequest::new("agent-1", "test-deploy")
1560            .with_effect(EffectVector { risk: 0.5, ..Default::default() });
1561
1562        let result = engine.evaluate_in_environment(&request, &staging_env);
1563        // 0.5 < 0.6 => permitted.
1564        assert_eq!(
1565            result.decision,
1566            GovernanceDecision::Permit,
1567            "staging should allow risk 0.5 with threshold 0.6, got {:?}",
1568            result.decision,
1569        );
1570
1571        // Higher request: magnitude 0.7
1572        let request2 = GovernanceRequest::new("agent-1", "test-deploy")
1573            .with_effect(EffectVector { risk: 0.7, ..Default::default() });
1574        let result2 = engine.evaluate_in_environment(&request2, &staging_env);
1575        // 0.7 > 0.6 => denied.
1576        assert!(
1577            matches!(result2.decision, GovernanceDecision::Deny(_)),
1578            "staging should deny risk 0.7 with threshold 0.6, got {:?}",
1579            result2.decision,
1580        );
1581    }
1582
1583    #[test]
1584    fn environment_governance_same_request_different_envs() {
1585        use crate::environment::EnvironmentClass;
1586
1587        let engine = GovernanceEngine::open();
1588
1589        let dev = make_env(EnvironmentClass::Development, 0.9);
1590        let prod = make_env(EnvironmentClass::Production, 0.6);
1591
1592        // magnitude = 0.5
1593        let request = GovernanceRequest::new("agent-1", "write-file")
1594            .with_effect(EffectVector { risk: 0.5, ..Default::default() });
1595
1596        let dev_result = engine.evaluate_in_environment(&request, &dev);
1597        let prod_result = engine.evaluate_in_environment(&request, &prod);
1598
1599        // Dev: 0.5 < 0.9 => permit
1600        assert_eq!(dev_result.decision, GovernanceDecision::Permit);
1601        // Prod: 0.5 > 0.3 (0.6 * 0.5) => deny
1602        assert!(matches!(prod_result.decision, GovernanceDecision::Deny(_)));
1603    }
1604
1605    // ── Trajectory recorder tests ───────────────────────────────
1606
1607    #[test]
1608    fn trajectory_records_and_retrieves() {
1609        use chrono::Utc;
1610        let mut recorder = TrajectoryRecorder::new(100);
1611        recorder.record(TrajectoryRecord {
1612            agent_id: "agent-1".into(),
1613            action: "tool.execute".into(),
1614            context: serde_json::json!({"tool": "read_file"}),
1615            outcome: TrajectoryOutcome::Success { reward: 1.0 },
1616            timestamp: Utc::now(),
1617        });
1618        assert_eq!(recorder.len(), 1);
1619        assert!(!recorder.is_empty());
1620        assert_eq!(recorder.agent_trajectory("agent-1").len(), 1);
1621        assert_eq!(recorder.agent_trajectory("agent-2").len(), 0);
1622    }
1623
1624    #[test]
1625    fn trajectory_extracts_patterns() {
1626        use chrono::Utc;
1627        let mut recorder = TrajectoryRecorder::new(100);
1628
1629        for _ in 0..5 {
1630            recorder.record(TrajectoryRecord {
1631                agent_id: "a".into(),
1632                action: "tool.execute".into(),
1633                context: serde_json::json!({}),
1634                outcome: TrajectoryOutcome::Success { reward: 1.0 },
1635                timestamp: Utc::now(),
1636            });
1637        }
1638        for _ in 0..3 {
1639            recorder.record(TrajectoryRecord {
1640                agent_id: "a".into(),
1641                action: "tool.read".into(),
1642                context: serde_json::json!({}),
1643                outcome: TrajectoryOutcome::Success { reward: 0.5 },
1644                timestamp: Utc::now(),
1645            });
1646        }
1647        recorder.record(TrajectoryRecord {
1648            agent_id: "a".into(),
1649            action: "tool.fail".into(),
1650            context: serde_json::json!({}),
1651            outcome: TrajectoryOutcome::Failure {
1652                reason: "err".into(),
1653            },
1654            timestamp: Utc::now(),
1655        });
1656
1657        let patterns = recorder.extract_patterns();
1658        assert_eq!(patterns[0].0, "tool.execute");
1659        assert_eq!(patterns[0].1, 5);
1660        assert_eq!(patterns[1].0, "tool.read");
1661        assert_eq!(patterns[1].1, 3);
1662        // Failures are not in patterns.
1663        assert!(patterns.iter().all(|(a, _)| a != "tool.fail"));
1664    }
1665
1666    #[test]
1667    fn trajectory_max_records_eviction() {
1668        use chrono::Utc;
1669        let mut recorder = TrajectoryRecorder::new(3);
1670        for i in 0..5 {
1671            recorder.record(TrajectoryRecord {
1672                agent_id: format!("agent-{i}"),
1673                action: "act".into(),
1674                context: serde_json::json!({}),
1675                outcome: TrajectoryOutcome::Pending,
1676                timestamp: Utc::now(),
1677            });
1678        }
1679        assert_eq!(recorder.len(), 3);
1680        // Oldest two (agent-0, agent-1) should be evicted.
1681        assert!(recorder.agent_trajectory("agent-0").is_empty());
1682        assert!(recorder.agent_trajectory("agent-1").is_empty());
1683        assert_eq!(recorder.agent_trajectory("agent-2").len(), 1);
1684        assert_eq!(recorder.agent_trajectory("agent-4").len(), 1);
1685    }
1686
1687    #[test]
1688    fn trajectory_empty_patterns() {
1689        let recorder = TrajectoryRecorder::new(10);
1690        assert!(recorder.is_empty());
1691        assert!(recorder.extract_patterns().is_empty());
1692    }
1693
1694    // ── Sprint 09a: serde roundtrip + behavioral tests ─────────
1695
1696    #[test]
1697    fn governance_branch_serde_roundtrip() {
1698        for branch in [
1699            GovernanceBranch::Legislative,
1700            GovernanceBranch::Executive,
1701            GovernanceBranch::Judicial,
1702        ] {
1703            let json = serde_json::to_string(&branch).unwrap();
1704            let restored: GovernanceBranch = serde_json::from_str(&json).unwrap();
1705            assert_eq!(restored, branch);
1706        }
1707    }
1708
1709    #[test]
1710    fn rule_severity_serde_roundtrip() {
1711        for severity in [
1712            RuleSeverity::Advisory,
1713            RuleSeverity::Warning,
1714            RuleSeverity::Blocking,
1715            RuleSeverity::Critical,
1716        ] {
1717            let json = serde_json::to_string(&severity).unwrap();
1718            let restored: RuleSeverity = serde_json::from_str(&json).unwrap();
1719            assert_eq!(restored, severity);
1720        }
1721    }
1722
1723    #[test]
1724    fn rule_severity_display() {
1725        assert_eq!(RuleSeverity::Advisory.to_string(), "advisory");
1726        assert_eq!(RuleSeverity::Warning.to_string(), "warning");
1727        assert_eq!(RuleSeverity::Blocking.to_string(), "blocking");
1728        assert_eq!(RuleSeverity::Critical.to_string(), "critical");
1729    }
1730
1731    #[test]
1732    fn governance_decision_serde_roundtrip_all_variants() {
1733        let variants = vec![
1734            GovernanceDecision::Permit,
1735            GovernanceDecision::PermitWithWarning("low risk".into()),
1736            GovernanceDecision::EscalateToHuman("needs review".into()),
1737            GovernanceDecision::Deny("policy violation".into()),
1738        ];
1739        for decision in variants {
1740            let json = serde_json::to_string(&decision).unwrap();
1741            let restored: GovernanceDecision = serde_json::from_str(&json).unwrap();
1742            assert_eq!(restored, decision);
1743        }
1744    }
1745
1746    #[test]
1747    fn governance_result_serde_roundtrip() {
1748        let result = GovernanceResult {
1749            decision: GovernanceDecision::Permit,
1750            evaluated_rules: vec!["rule-1".into(), "rule-2".into()],
1751            effect: EffectVector {
1752                risk: 0.3,
1753                ..Default::default()
1754            },
1755            threshold_exceeded: false,
1756        };
1757        let json = serde_json::to_string(&result).unwrap();
1758        let restored: GovernanceResult = serde_json::from_str(&json).unwrap();
1759        assert_eq!(restored.decision, GovernanceDecision::Permit);
1760        assert_eq!(restored.evaluated_rules.len(), 2);
1761        assert!(!restored.threshold_exceeded);
1762    }
1763
1764    #[test]
1765    fn governance_result_deny_serde_roundtrip() {
1766        let result = GovernanceResult {
1767            decision: GovernanceDecision::Deny("threshold exceeded".into()),
1768            evaluated_rules: vec![],
1769            effect: EffectVector {
1770                risk: 0.9,
1771                security: 0.8,
1772                ..Default::default()
1773            },
1774            threshold_exceeded: true,
1775        };
1776        let json = serde_json::to_string(&result).unwrap();
1777        let restored: GovernanceResult = serde_json::from_str(&json).unwrap();
1778        assert!(restored.threshold_exceeded);
1779        assert!(matches!(restored.decision, GovernanceDecision::Deny(_)));
1780    }
1781
1782    #[test]
1783    fn trajectory_outcome_serde_roundtrip() {
1784        let variants: Vec<TrajectoryOutcome> = vec![
1785            TrajectoryOutcome::Success { reward: 1.5 },
1786            TrajectoryOutcome::Failure {
1787                reason: "timeout".into(),
1788            },
1789            TrajectoryOutcome::Pending,
1790        ];
1791        for outcome in variants {
1792            let json = serde_json::to_string(&outcome).unwrap();
1793            let _restored: TrajectoryOutcome = serde_json::from_str(&json).unwrap();
1794        }
1795    }
1796
1797    #[test]
1798    fn trajectory_record_serde_roundtrip() {
1799        use chrono::Utc;
1800        let record = TrajectoryRecord {
1801            agent_id: "agent-1".into(),
1802            action: "tool.execute".into(),
1803            context: serde_json::json!({"tool": "read_file", "path": "/etc/hosts"}),
1804            outcome: TrajectoryOutcome::Success { reward: 1.0 },
1805            timestamp: Utc::now(),
1806        };
1807        let json = serde_json::to_string(&record).unwrap();
1808        let restored: TrajectoryRecord = serde_json::from_str(&json).unwrap();
1809        assert_eq!(restored.agent_id, "agent-1");
1810        assert_eq!(restored.action, "tool.execute");
1811    }
1812
1813    #[test]
1814    fn governance_rule_with_sop_fields_roundtrip() {
1815        let rule = GovernanceRule {
1816            id: "SOP-001".into(),
1817            description: "Do not access prod data".into(),
1818            branch: GovernanceBranch::Legislative,
1819            severity: RuleSeverity::Critical,
1820            active: true,
1821            reference_url: Some("https://sops.example.com/001".into()),
1822            sop_category: Some("data-access".into()),
1823        };
1824        let json = serde_json::to_string(&rule).unwrap();
1825        let restored: GovernanceRule = serde_json::from_str(&json).unwrap();
1826        assert_eq!(restored.reference_url.unwrap(), "https://sops.example.com/001");
1827        assert_eq!(restored.sop_category.unwrap(), "data-access");
1828    }
1829
1830    #[test]
1831    fn effect_vector_default_is_zero() {
1832        let v = EffectVector::default();
1833        assert!((v.magnitude() - 0.0).abs() < f64::EPSILON);
1834        assert!(!v.any_exceeds(0.0));
1835    }
1836
1837    #[test]
1838    fn effect_vector_max_dimension() {
1839        let v = EffectVector {
1840            risk: 0.1,
1841            fairness: 0.5,
1842            privacy: 0.3,
1843            novelty: 0.9,
1844            security: 0.2,
1845        };
1846        assert!((v.max_dimension() - 0.9).abs() < f64::EPSILON);
1847    }
1848
1849    #[cfg(feature = "exochain")]
1850    mod rvf_bridge_tests {
1851        use super::*;
1852
1853        #[test]
1854        fn decision_to_policy_check() {
1855            use rvf_types::witness::PolicyCheck;
1856
1857            assert_eq!(
1858                GovernanceDecision::Permit.to_rvf_policy_check(),
1859                PolicyCheck::Allowed,
1860            );
1861            assert_eq!(
1862                GovernanceDecision::PermitWithWarning("low risk".into()).to_rvf_policy_check(),
1863                PolicyCheck::Confirmed,
1864            );
1865            assert_eq!(
1866                GovernanceDecision::EscalateToHuman("needs review".into()).to_rvf_policy_check(),
1867                PolicyCheck::Confirmed,
1868            );
1869            assert_eq!(
1870                GovernanceDecision::Deny("blocked".into()).to_rvf_policy_check(),
1871                PolicyCheck::Denied,
1872            );
1873        }
1874
1875        #[test]
1876        fn open_engine_maps_to_autonomous() {
1877            use rvf_types::witness::GovernanceMode;
1878
1879            let engine = GovernanceEngine::open();
1880            assert_eq!(engine.to_rvf_mode(), GovernanceMode::Autonomous);
1881        }
1882
1883        #[test]
1884        fn strict_engine_maps_to_restricted() {
1885            use rvf_types::witness::GovernanceMode;
1886
1887            let engine = GovernanceEngine::new(0.5, false);
1888            assert_eq!(engine.to_rvf_mode(), GovernanceMode::Restricted);
1889        }
1890
1891        #[test]
1892        fn human_approval_maps_to_approved() {
1893            use rvf_types::witness::GovernanceMode;
1894
1895            let engine = GovernanceEngine::new(0.5, true);
1896            assert_eq!(engine.to_rvf_mode(), GovernanceMode::Approved);
1897        }
1898
1899        #[test]
1900        fn to_rvf_policy_mode_matches() {
1901            use rvf_types::witness::GovernanceMode;
1902
1903            let open = GovernanceEngine::open();
1904            let policy = open.to_rvf_policy();
1905            assert_eq!(policy.mode, GovernanceMode::Autonomous);
1906
1907            let strict = GovernanceEngine::new(0.3, false);
1908            let policy = strict.to_rvf_policy();
1909            assert_eq!(policy.mode, GovernanceMode::Restricted);
1910
1911            let human = GovernanceEngine::new(0.3, true);
1912            let policy = human.to_rvf_policy();
1913            assert_eq!(policy.mode, GovernanceMode::Approved);
1914        }
1915
1916        #[test]
1917        fn governance_result_to_task_outcome() {
1918            use rvf_types::witness::TaskOutcome;
1919
1920            let permit_result = GovernanceResult {
1921                decision: GovernanceDecision::Permit,
1922                evaluated_rules: vec![],
1923                effect: EffectVector::default(),
1924                threshold_exceeded: false,
1925            };
1926            assert_eq!(permit_result.to_rvf_task_outcome() as u8, TaskOutcome::Solved as u8);
1927
1928            let deny_result = GovernanceResult {
1929                decision: GovernanceDecision::Deny("blocked".into()),
1930                evaluated_rules: vec![],
1931                effect: EffectVector::default(),
1932                threshold_exceeded: true,
1933            };
1934            assert_eq!(deny_result.to_rvf_task_outcome() as u8, TaskOutcome::Failed as u8);
1935
1936            let escalate_result = GovernanceResult {
1937                decision: GovernanceDecision::EscalateToHuman("review".into()),
1938                evaluated_rules: vec![],
1939                effect: EffectVector::default(),
1940                threshold_exceeded: true,
1941            };
1942            assert_eq!(escalate_result.to_rvf_task_outcome() as u8, TaskOutcome::Skipped as u8);
1943        }
1944    }
1945
1946    // ── ChainLoggable integration tests ────────────────────────
1947
1948    #[cfg(feature = "exochain")]
1949    mod chain_logging_tests {
1950        use super::*;
1951        use std::sync::Arc;
1952
1953        #[test]
1954        fn evaluate_logged_records_permit() {
1955            let cm = Arc::new(crate::chain::ChainManager::new(0, 100));
1956            let initial_len = cm.len();
1957
1958            let mut engine = GovernanceEngine::new(0.5, false);
1959            engine.add_rule(GovernanceRule {
1960                id: "sec".into(),
1961                description: "test".into(),
1962                branch: GovernanceBranch::Judicial,
1963                severity: RuleSeverity::Blocking,
1964                active: true,
1965                reference_url: None,
1966                sop_category: None,
1967            });
1968
1969            let request = GovernanceRequest {
1970                agent_id: "agent-1".into(),
1971                action: "tool.read".into(),
1972                effect: EffectVector { risk: 0.1, ..Default::default() },
1973                context: Default::default(),
1974                node_id: None,
1975            };
1976
1977            let result = engine.evaluate_logged(&request, Some(&cm));
1978            assert!(matches!(result.decision, GovernanceDecision::Permit));
1979            assert_eq!(cm.len(), initial_len + 1);
1980
1981            let events = cm.tail(1);
1982            assert_eq!(events[0].kind, "governance.permit");
1983            assert_eq!(events[0].source, "governance");
1984        }
1985
1986        #[test]
1987        fn evaluate_logged_records_deny() {
1988            let cm = Arc::new(crate::chain::ChainManager::new(0, 100));
1989
1990            let mut engine = GovernanceEngine::new(0.5, false);
1991            engine.add_rule(GovernanceRule {
1992                id: "sec".into(),
1993                description: "test".into(),
1994                branch: GovernanceBranch::Judicial,
1995                severity: RuleSeverity::Blocking,
1996                active: true,
1997                reference_url: None,
1998                sop_category: None,
1999            });
2000
2001            let request = GovernanceRequest {
2002                agent_id: "agent-1".into(),
2003                action: "tool.exec".into(),
2004                effect: EffectVector { risk: 0.9, ..Default::default() },
2005                context: Default::default(),
2006                node_id: None,
2007            };
2008
2009            let result = engine.evaluate_logged(&request, Some(&cm));
2010            assert!(matches!(result.decision, GovernanceDecision::Deny(_)));
2011
2012            let events = cm.tail(1);
2013            assert_eq!(events[0].kind, "governance.deny");
2014            let payload = events[0].payload.as_ref().unwrap();
2015            assert_eq!(payload["agent_id"], "agent-1");
2016            assert!(payload["threshold_exceeded"].as_bool().unwrap());
2017        }
2018
2019        #[test]
2020        fn evaluate_logged_without_chain_still_works() {
2021            let engine = GovernanceEngine::new(0.5, false);
2022            let request = GovernanceRequest {
2023                agent_id: "agent-1".into(),
2024                action: "tool.read".into(),
2025                effect: EffectVector::default(),
2026                context: Default::default(),
2027                node_id: None,
2028            };
2029
2030            // Passing None should not panic, just skip logging
2031            let result = engine.evaluate_logged(&request, None);
2032            assert!(matches!(result.decision, GovernanceDecision::Permit));
2033        }
2034
2035        #[test]
2036        fn chain_log_result_standalone() {
2037            let cm = crate::chain::ChainManager::new(0, 100);
2038            let initial_len = cm.len();
2039
2040            let mut engine = GovernanceEngine::new(0.5, true);
2041            engine.add_rule(GovernanceRule {
2042                id: "sec".into(),
2043                description: "test".into(),
2044                branch: GovernanceBranch::Judicial,
2045                severity: RuleSeverity::Blocking,
2046                active: true,
2047                reference_url: None,
2048                sop_category: None,
2049            });
2050
2051            let request = GovernanceRequest {
2052                agent_id: "agent-1".into(),
2053                action: "tool.exec".into(),
2054                effect: EffectVector { risk: 0.8, ..Default::default() },
2055                context: Default::default(),
2056                node_id: None,
2057            };
2058
2059            let result = engine.evaluate(&request);
2060            GovernanceEngine::chain_log_result(&cm, &request, &result);
2061            assert_eq!(cm.len(), initial_len + 1);
2062
2063            let events = cm.tail(1);
2064            assert_eq!(events[0].kind, "governance.defer");
2065        }
2066    }
2067}