writerslogic_warm/core/agency/
self_modify.rs1use crate::core::sync::RwLock;
11
12#[derive(Clone, Debug)]
14pub struct Proposal {
15 pub id: usize,
16 pub kind: ProposalKind,
17 pub reason: String,
18 pub status: ProposalStatus,
19}
20
21#[derive(Clone, Debug)]
23pub enum ProposalKind {
24 AddRule {
26 name: String,
27 input_relations: Vec<String>,
28 output_relation: String,
29 },
30 AdjustThreshold {
32 parameter: String,
33 current_value: f64,
34 proposed_value: f64,
35 },
36 AddPattern { description: String },
38 RemoveTriple {
40 subject: String,
41 relation: String,
42 object: String,
43 },
44}
45
46#[derive(Clone, Debug, PartialEq, Eq)]
48pub enum ProposalStatus {
49 Pending,
50 Approved,
51 Rejected,
52 Applied,
53}
54
55pub struct SelfModifier {
57 proposals: RwLock<Vec<Proposal>>,
58 audit_log: RwLock<Vec<AuditEntry>>,
59}
60
61#[derive(Clone, Debug)]
63pub struct AuditEntry {
64 pub proposal_id: usize,
65 pub action: String,
66 pub timestamp_ms: u64,
67}
68
69fn now_ms() -> u64 {
70 std::time::SystemTime::now()
71 .duration_since(std::time::UNIX_EPOCH)
72 .unwrap_or_default()
73 .as_millis() as u64
74}
75
76impl Default for SelfModifier {
77 fn default() -> Self {
78 Self::new()
79 }
80}
81
82impl SelfModifier {
83 pub fn new() -> Self {
84 Self {
85 proposals: RwLock::new(Vec::new()),
86 audit_log: RwLock::new(Vec::new()),
87 }
88 }
89
90 pub fn propose(&self, kind: ProposalKind, reason: String) -> usize {
92 let id = {
93 let mut proposals = self.proposals.write();
94 let id = proposals.len();
95 proposals.push(Proposal {
96 id,
97 kind,
98 reason,
99 status: ProposalStatus::Pending,
100 });
101 id
102 };
103 self.log_action(id, "proposed");
104 id
105 }
106
107 pub fn approve(&self, id: usize) -> bool {
109 let changed = {
110 let mut proposals = self.proposals.write();
111 if let Some(p) = proposals.get_mut(id) {
112 if p.status == ProposalStatus::Pending {
113 p.status = ProposalStatus::Approved;
114 true
115 } else {
116 false
117 }
118 } else {
119 false
120 }
121 };
122 if changed {
123 self.log_action(id, "approved");
124 }
125 changed
126 }
127
128 pub fn reject(&self, id: usize) -> bool {
130 let changed = {
131 let mut proposals = self.proposals.write();
132 if let Some(p) = proposals.get_mut(id) {
133 if p.status == ProposalStatus::Pending {
134 p.status = ProposalStatus::Rejected;
135 true
136 } else {
137 false
138 }
139 } else {
140 false
141 }
142 };
143 if changed {
144 self.log_action(id, "rejected");
145 }
146 changed
147 }
148
149 pub fn mark_applied(&self, id: usize) -> bool {
151 let changed = {
152 let mut proposals = self.proposals.write();
153 if let Some(p) = proposals.get_mut(id) {
154 if p.status == ProposalStatus::Approved {
155 p.status = ProposalStatus::Applied;
156 true
157 } else {
158 false
159 }
160 } else {
161 false
162 }
163 };
164 if changed {
165 self.log_action(id, "applied");
166 }
167 changed
168 }
169
170 fn log_action(&self, proposal_id: usize, action: &str) {
171 self.audit_log.write().push(AuditEntry {
172 proposal_id,
173 action: action.to_string(),
174 timestamp_ms: now_ms(),
175 });
176 }
177
178 pub fn pending_proposals(&self) -> Vec<Proposal> {
179 self.proposals
180 .read()
181 .iter()
182 .filter(|p| p.status == ProposalStatus::Pending)
183 .cloned()
184 .collect()
185 }
186
187 pub fn get(&self, id: usize) -> Option<Proposal> {
188 self.proposals.read().get(id).cloned()
189 }
190
191 pub fn proposal_count(&self) -> usize {
192 self.proposals.read().len()
193 }
194
195 pub fn pending_count(&self) -> usize {
196 self.proposals
197 .read()
198 .iter()
199 .filter(|p| p.status == ProposalStatus::Pending)
200 .count()
201 }
202
203 pub fn audit_entries(&self) -> Vec<AuditEntry> {
204 self.audit_log.read().clone()
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn test_propose_approve_apply() {
214 let sm = SelfModifier::new();
215 let id = sm.propose(
216 ProposalKind::AddRule {
217 name: "uncle".to_string(),
218 input_relations: vec!["parent".to_string(), "sibling".to_string()],
219 output_relation: "uncle".to_string(),
220 },
221 "Discovered uncle pattern in triples".to_string(),
222 );
223
224 assert_eq!(sm.pending_count(), 1);
225 assert!(sm.approve(id));
226 assert_eq!(sm.pending_count(), 0);
227 assert_eq!(sm.get(id).unwrap().status, ProposalStatus::Approved);
228
229 assert!(sm.mark_applied(id));
230 assert_eq!(sm.get(id).unwrap().status, ProposalStatus::Applied);
231 }
232
233 #[test]
234 fn test_reject() {
235 let sm = SelfModifier::new();
236 let id = sm.propose(
237 ProposalKind::AdjustThreshold {
238 parameter: "fanout_limit".to_string(),
239 current_value: 40.0,
240 proposed_value: 60.0,
241 },
242 "High fan-out queries are slow".to_string(),
243 );
244
245 assert!(sm.reject(id));
246 assert_eq!(sm.get(id).unwrap().status, ProposalStatus::Rejected);
247 assert!(!sm.approve(id)); }
249
250 #[test]
251 fn test_cannot_apply_without_approval() {
252 let sm = SelfModifier::new();
253 let id = sm.propose(
254 ProposalKind::RemoveTriple {
255 subject: "wrong".to_string(),
256 relation: "is".to_string(),
257 object: "right".to_string(),
258 },
259 "Incorrect triple".to_string(),
260 );
261
262 assert!(!sm.mark_applied(id)); }
264
265 #[test]
266 fn test_audit_trail() {
267 let sm = SelfModifier::new();
268 let id = sm.propose(
269 ProposalKind::AddPattern {
270 description: "passive voice pattern".to_string(),
271 },
272 "Improve decomposer".to_string(),
273 );
274 sm.approve(id);
275 sm.mark_applied(id);
276
277 let entries = sm.audit_entries();
278 assert_eq!(entries.len(), 3);
279 assert_eq!(entries[0].action, "proposed");
280 assert_eq!(entries[1].action, "approved");
281 assert_eq!(entries[2].action, "applied");
282 }
283
284 #[test]
285 fn test_multiple_proposals() {
286 let sm = SelfModifier::new();
287 sm.propose(
288 ProposalKind::AddRule {
289 name: "r1".to_string(),
290 input_relations: vec![],
291 output_relation: "r1".to_string(),
292 },
293 "reason 1".to_string(),
294 );
295 sm.propose(
296 ProposalKind::AddRule {
297 name: "r2".to_string(),
298 input_relations: vec![],
299 output_relation: "r2".to_string(),
300 },
301 "reason 2".to_string(),
302 );
303
304 assert_eq!(sm.proposal_count(), 2);
305 assert_eq!(sm.pending_count(), 2);
306
307 let pending = sm.pending_proposals();
308 assert_eq!(pending.len(), 2);
309 }
310
311 #[test]
312 fn test_empty() {
313 let sm = SelfModifier::new();
314 assert_eq!(sm.proposal_count(), 0);
315 assert_eq!(sm.pending_count(), 0);
316 assert!(sm.get(0).is_none());
317 assert!(sm.audit_entries().is_empty());
318 }
319}