1use serde::{Deserialize, Serialize};
8use std::fmt;
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub enum Resource {
13 Galaxy(String),
15 KarmaLedger,
17 DharmaRules,
19 SearchIndex,
21 VectorStore,
23 Network,
25 Filesystem,
27 Process,
29 Inference,
31 Session,
33 EventBus,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
39pub enum Capability {
40 MemoryRead,
42 MemoryWrite,
44 MemoryDelete,
46 Search,
48 VectorSearch,
50 Embed,
52 LlmInfer,
54 Delegate,
56 Execute,
58 NetworkRequest,
60 Dream,
62 CittaUpdate,
64}
65
66#[derive(Debug, Clone, Default, Serialize, Deserialize)]
68pub struct CostEstimate {
69 pub cpu_ns: u64,
71 pub memory_bytes: u64,
73 pub disk_bytes: u64,
75 pub network_bytes: u64,
77 pub expensive: bool,
79}
80
81#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91pub enum Sandbox {
92 #[default]
95 Inherit,
96 StoreScoped,
99 Subprocess,
105}
106
107#[derive(Debug, Clone, Default, Serialize, Deserialize)]
112pub struct EffectRow {
113 pub reads: Vec<Resource>,
115 pub writes: Vec<Resource>,
117 pub invokes: Vec<Capability>,
119 pub spawns: bool,
121 pub destructive: bool,
124 #[serde(default)]
126 pub sandbox: Sandbox,
127 pub cost: CostEstimate,
129}
130
131impl EffectRow {
132 #[must_use]
134 pub fn pure() -> Self {
135 Self::default()
136 }
137
138 #[must_use]
140 pub fn read_only(resources: Vec<Resource>) -> Self {
141 Self {
142 reads: resources,
143 writes: vec![],
144 invokes: vec![],
145 spawns: false,
146 destructive: false,
147 sandbox: Sandbox::Inherit,
148 cost: CostEstimate::default(),
149 }
150 }
151
152 #[must_use]
157 pub fn is_available_in(&self, brain_wave: crate::BrainWave) -> bool {
158 use crate::BrainWave::{Alpha, Beta, Delta, Gamma, Theta};
159 match brain_wave {
160 Gamma => true,
161 Beta => true,
162 Alpha => !self.cost.expensive && self.writes.is_empty(),
163 Theta => !self.cost.expensive && self.writes.is_empty() && !self.spawns,
164 Delta => false, }
166 }
167
168 #[must_use]
170 pub fn conflicts_with(&self, other: &Self) -> bool {
171 for w in &self.writes {
173 if other.writes.contains(w) || other.reads.contains(w) {
174 return true;
175 }
176 }
177 for w in &other.writes {
178 if self.reads.contains(w) {
179 return true;
180 }
181 }
182 if self.spawns && other.spawns {
184 return true;
185 }
186 false
187 }
188}
189
190impl fmt::Display for EffectRow {
191 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192 write!(
193 f,
194 "reads:{}, writes:{}, invokes:{}, spawns:{}",
195 self.reads.len(),
196 self.writes.len(),
197 self.invokes.len(),
198 self.spawns
199 )
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn pure_effect_has_no_side_effects() {
209 let e = EffectRow::pure();
210 assert!(e.reads.is_empty());
211 assert!(e.writes.is_empty());
212 assert!(!e.spawns);
213 }
214
215 #[test]
216 fn effect_conflict_detection() {
217 let writer = EffectRow {
218 writes: vec![Resource::Galaxy("citta".into())],
219 ..Default::default()
220 };
221 let reader = EffectRow {
222 reads: vec![Resource::Galaxy("citta".into())],
223 ..Default::default()
224 };
225 assert!(writer.conflicts_with(&reader));
226 assert!(reader.conflicts_with(&writer));
227
228 let other_reader = EffectRow {
229 reads: vec![Resource::Galaxy("codex".into())],
230 ..Default::default()
231 };
232 assert!(!reader.conflicts_with(&other_reader));
233 }
234
235 #[test]
236 fn brain_wave_filtering() {
237 use crate::BrainWave::*;
238 let expensive = EffectRow {
239 cost: CostEstimate {
240 expensive: true,
241 ..Default::default()
242 },
243 ..Default::default()
244 };
245 assert!(expensive.is_available_in(Gamma));
246 assert!(!expensive.is_available_in(Alpha));
247 assert!(!expensive.is_available_in(Delta));
248 }
249
250 #[test]
251 fn sandbox_seam_defaults_and_serializes() {
252 let row = EffectRow::pure();
253 assert_eq!(row.sandbox, Sandbox::Inherit);
254
255 let scoped = EffectRow {
256 sandbox: Sandbox::StoreScoped,
257 ..Default::default()
258 };
259 assert_eq!(scoped.sandbox, Sandbox::StoreScoped);
260
261 let json = serde_json::to_string(&scoped).expect("serialize");
264 assert!(json.contains("store_scoped"));
265 let back: EffectRow = serde_json::from_str(&json).expect("deserialize");
266 assert_eq!(back.sandbox, Sandbox::StoreScoped);
267 let legacy: EffectRow = serde_json::from_str(
268 "{\"reads\":[],\"writes\":[],\"invokes\":[],\"spawns\":false,\"destructive\":false,\
269 \"cost\":{\"cpu_ns\":0,\"memory_bytes\":0,\"disk_bytes\":0,\"network_bytes\":0,\"expensive\":false}}",
270 )
271 .expect("legacy payload without sandbox field");
272 assert_eq!(legacy.sandbox, Sandbox::Inherit);
273 }
274
275 use crate::BrainWave;
278 use proptest::prelude::*;
279
280 fn arb_resource() -> impl Strategy<Value = Resource> {
281 prop_oneof![
282 Just(Resource::Galaxy("codex".into())),
283 Just(Resource::Galaxy("citta".into())),
284 Just(Resource::Filesystem),
285 Just(Resource::Network),
286 Just(Resource::Process),
287 ]
288 }
289
290 fn arb_effect_row() -> impl Strategy<Value = EffectRow> {
291 (
292 proptest::collection::vec(arb_resource(), 0..6),
293 proptest::collection::vec(arb_resource(), 0..6),
294 any::<bool>(),
295 any::<bool>(),
296 )
297 .prop_map(|(reads, writes, spawns, expensive)| EffectRow {
298 reads,
299 writes,
300 spawns,
301 cost: CostEstimate {
302 expensive,
303 ..Default::default()
304 },
305 ..Default::default()
306 })
307 }
308
309 proptest! {
310 #[test]
312 fn delta_blocks_all(effects in arb_effect_row()) {
313 prop_assert!(!effects.is_available_in(BrainWave::Delta));
314 }
315
316 #[test]
318 fn gamma_allows_all(effects in arb_effect_row()) {
319 prop_assert!(effects.is_available_in(BrainWave::Gamma));
320 }
321
322 #[test]
324 fn beta_allows_all(effects in arb_effect_row()) {
325 prop_assert!(effects.is_available_in(BrainWave::Beta));
326 }
327
328 #[test]
330 fn alpha_blocks_writes_and_expensive(effects in arb_effect_row()) {
331 let result = effects.is_available_in(BrainWave::Alpha);
332 if !effects.writes.is_empty() || effects.cost.expensive {
333 prop_assert!(!result, "Alpha should block writes/expensive: {effects}");
334 } else {
335 prop_assert!(result, "Alpha should allow pure reads: {effects}");
336 }
337 }
338
339 #[test]
341 fn theta_blocks_writes_spawns_expensive(effects in arb_effect_row()) {
342 let result = effects.is_available_in(BrainWave::Theta);
343 if !effects.writes.is_empty() || effects.cost.expensive || effects.spawns {
344 prop_assert!(!result, "Theta should block: {effects}");
345 } else {
346 prop_assert!(result, "Theta should allow pure reads: {effects}");
347 }
348 }
349
350 #[test]
352 fn conflicts_symmetric(a in arb_effect_row(), b in arb_effect_row()) {
353 let ab = a.conflicts_with(&b);
354 let ba = b.conflicts_with(&a);
355 prop_assert_eq!(ab, ba, "conflicts_with must be symmetric");
356 }
357
358 #[test]
360 fn conflicts_self_with_writes_or_spawns(effects in arb_effect_row()) {
361 let self_conflict = effects.conflicts_with(&effects);
362 if !effects.writes.is_empty() || effects.spawns {
363 prop_assert!(self_conflict, "effect row with writes/spawns should conflict with itself");
364 } else {
365 prop_assert!(!self_conflict, "pure effect row should not conflict with itself");
366 }
367 }
368 }
369}