Skip to main content

oris_governor/
lib.rs

1//! Policy-only governor contracts for Oris EvoKernel.
2
3use serde::{Deserialize, Serialize};
4
5use oris_evolution::{AssetState, BlastRadius, CandidateSource};
6
7#[derive(Clone, Debug, Serialize, Deserialize)]
8pub struct GovernorConfig {
9    pub promote_after_successes: u64,
10    pub max_files_changed: usize,
11    pub max_lines_changed: usize,
12    pub cooldown_secs: u64,
13    pub revoke_after_replay_failures: u64,
14}
15
16impl Default for GovernorConfig {
17    fn default() -> Self {
18        Self {
19            promote_after_successes: 3,
20            max_files_changed: 5,
21            max_lines_changed: 300,
22            cooldown_secs: 30 * 60,
23            revoke_after_replay_failures: 2,
24        }
25    }
26}
27
28#[derive(Clone, Debug, Serialize, Deserialize)]
29pub struct CoolingWindow {
30    pub cooldown_secs: u64,
31}
32
33#[derive(Clone, Debug, Serialize, Deserialize)]
34pub enum RevocationReason {
35    ReplayRegression,
36    ValidationFailure,
37    Manual(String),
38}
39
40#[derive(Clone, Debug, Serialize, Deserialize)]
41pub struct GovernorInput {
42    pub candidate_source: CandidateSource,
43    pub success_count: u64,
44    pub blast_radius: BlastRadius,
45    pub replay_failures: u64,
46}
47
48#[derive(Clone, Debug, Serialize, Deserialize)]
49pub struct GovernorDecision {
50    pub target_state: AssetState,
51    pub reason: String,
52    pub cooling_window: Option<CoolingWindow>,
53    pub revocation_reason: Option<RevocationReason>,
54}
55
56pub trait Governor: Send + Sync {
57    fn evaluate(&self, input: GovernorInput) -> GovernorDecision;
58}
59
60#[derive(Clone, Debug, Default)]
61pub struct DefaultGovernor {
62    config: GovernorConfig,
63}
64
65impl DefaultGovernor {
66    pub fn new(config: GovernorConfig) -> Self {
67        Self { config }
68    }
69}
70
71impl Governor for DefaultGovernor {
72    fn evaluate(&self, input: GovernorInput) -> GovernorDecision {
73        if input.replay_failures >= self.config.revoke_after_replay_failures {
74            return GovernorDecision {
75                target_state: AssetState::Revoked,
76                reason: "replay validation failures exceeded threshold".into(),
77                cooling_window: None,
78                revocation_reason: Some(RevocationReason::ReplayRegression),
79            };
80        }
81
82        if input.blast_radius.files_changed > self.config.max_files_changed
83            || input.blast_radius.lines_changed > self.config.max_lines_changed
84        {
85            return GovernorDecision {
86                target_state: AssetState::Candidate,
87                reason: "blast radius exceeds promotion threshold".into(),
88                cooling_window: None,
89                revocation_reason: None,
90            };
91        }
92
93        if input.success_count >= self.config.promote_after_successes {
94            return GovernorDecision {
95                target_state: AssetState::Promoted,
96                reason: "success threshold reached".into(),
97                cooling_window: Some(CoolingWindow {
98                    cooldown_secs: self.config.cooldown_secs,
99                }),
100                revocation_reason: None,
101            };
102        }
103
104        GovernorDecision {
105            target_state: AssetState::Candidate,
106            reason: "collecting more successful executions".into(),
107            cooling_window: None,
108            revocation_reason: None,
109        }
110    }
111}