1use 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}