ruvector_cognitive_container/
epoch.rs1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct ContainerEpochBudget {
6 pub total: u64,
8 pub ingest: u64,
10 pub mincut: u64,
12 pub spectral: u64,
14 pub evidence: u64,
16 pub witness: u64,
18}
19
20impl Default for ContainerEpochBudget {
21 fn default() -> Self {
22 Self {
23 total: 10_000,
24 ingest: 2_000,
25 mincut: 3_000,
26 spectral: 2_000,
27 evidence: 2_000,
28 witness: 1_000,
29 }
30 }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum Phase {
36 Ingest,
37 MinCut,
38 Spectral,
39 Evidence,
40 Witness,
41}
42
43pub struct EpochController {
45 budget: ContainerEpochBudget,
46 ticks_used: u64,
47 phase_used: [u64; 5],
48 current_phase: Phase,
49}
50
51impl EpochController {
52 pub fn new(budget: ContainerEpochBudget) -> Self {
54 Self {
55 budget,
56 ticks_used: 0,
57 phase_used: [0; 5],
58 current_phase: Phase::Ingest,
59 }
60 }
61
62 pub fn try_budget(&mut self, phase: Phase) -> bool {
65 let idx = Self::phase_index(phase);
66 let limit = self.phase_budget(phase);
67 if self.phase_used[idx] < limit && self.ticks_used < self.budget.total {
68 self.current_phase = phase;
69 true
70 } else {
71 false
72 }
73 }
74
75 pub fn consume(&mut self, ticks: u64) {
77 let idx = Self::phase_index(self.current_phase);
78 self.ticks_used += ticks;
79 self.phase_used[idx] += ticks;
80 }
81
82 pub fn remaining(&self) -> u64 {
84 self.budget.total.saturating_sub(self.ticks_used)
85 }
86
87 pub fn reset(&mut self) {
89 self.ticks_used = 0;
90 self.phase_used = [0; 5];
91 self.current_phase = Phase::Ingest;
92 }
93
94 pub fn phase_budget(&self, phase: Phase) -> u64 {
96 match phase {
97 Phase::Ingest => self.budget.ingest,
98 Phase::MinCut => self.budget.mincut,
99 Phase::Spectral => self.budget.spectral,
100 Phase::Evidence => self.budget.evidence,
101 Phase::Witness => self.budget.witness,
102 }
103 }
104
105 pub fn phase_used(&self, phase: Phase) -> u64 {
107 self.phase_used[Self::phase_index(phase)]
108 }
109
110 fn phase_index(phase: Phase) -> usize {
111 match phase {
112 Phase::Ingest => 0,
113 Phase::MinCut => 1,
114 Phase::Spectral => 2,
115 Phase::Evidence => 3,
116 Phase::Witness => 4,
117 }
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn test_epoch_budgeting() {
127 let budget = ContainerEpochBudget {
128 total: 100,
129 ingest: 30,
130 mincut: 25,
131 spectral: 20,
132 evidence: 15,
133 witness: 10,
134 };
135 let mut ctl = EpochController::new(budget);
136
137 assert!(ctl.try_budget(Phase::Ingest));
138 ctl.consume(30);
139 assert_eq!(ctl.phase_used(Phase::Ingest), 30);
140 assert!(!ctl.try_budget(Phase::Ingest));
142 assert_eq!(ctl.remaining(), 70);
143
144 assert!(ctl.try_budget(Phase::MinCut));
145 ctl.consume(25);
146 assert!(!ctl.try_budget(Phase::MinCut));
147 assert_eq!(ctl.remaining(), 45);
148
149 assert!(ctl.try_budget(Phase::Spectral));
150 ctl.consume(20);
151 assert!(ctl.try_budget(Phase::Evidence));
152 ctl.consume(15);
153 assert!(ctl.try_budget(Phase::Witness));
154 ctl.consume(10);
155
156 assert_eq!(ctl.remaining(), 0);
157 }
158
159 #[test]
160 fn test_epoch_reset() {
161 let mut ctl = EpochController::new(ContainerEpochBudget::default());
162 assert!(ctl.try_budget(Phase::Ingest));
163 ctl.consume(500);
164 assert_eq!(ctl.phase_used(Phase::Ingest), 500);
165
166 ctl.reset();
167 assert_eq!(ctl.phase_used(Phase::Ingest), 0);
168 assert_eq!(ctl.remaining(), 10_000);
169 }
170
171 #[test]
172 fn test_total_budget_caps_phase() {
173 let budget = ContainerEpochBudget {
174 total: 10,
175 ingest: 100,
176 mincut: 100,
177 spectral: 100,
178 evidence: 100,
179 witness: 100,
180 };
181 let mut ctl = EpochController::new(budget);
182 assert!(ctl.try_budget(Phase::Ingest));
183 ctl.consume(10);
184 assert!(!ctl.try_budget(Phase::MinCut));
186 }
187}