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 CoordinationLease,
39 CoordinationRelease,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
47pub enum Capability {
48 MemoryRead,
50 MemoryWrite,
52 MemoryDelete,
54 Search,
56 VectorSearch,
58 Embed,
60 LlmInfer,
62 Delegate,
64 Execute,
66 NetworkRequest,
68 Dream,
70 CittaUpdate,
72}
73
74#[derive(Debug, Clone, Default, Serialize, Deserialize)]
76pub struct CostEstimate {
77 pub cpu_ns: u64,
79 pub memory_bytes: u64,
81 pub disk_bytes: u64,
83 pub network_bytes: u64,
85 pub expensive: bool,
87}
88
89#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
98#[serde(rename_all = "snake_case")]
99pub enum Sandbox {
100 #[default]
103 Inherit,
104 StoreScoped,
107 Subprocess,
113}
114
115#[derive(Debug, Clone, Default, Serialize, Deserialize)]
120pub struct EffectRow {
121 pub reads: Vec<Resource>,
123 pub writes: Vec<Resource>,
125 pub invokes: Vec<Capability>,
127 pub spawns: bool,
129 pub destructive: bool,
132 #[serde(default)]
134 pub sandbox: Sandbox,
135 pub cost: CostEstimate,
137}
138
139impl EffectRow {
140 #[must_use]
142 pub fn pure() -> Self {
143 Self::default()
144 }
145
146 #[must_use]
148 pub fn read_only(resources: Vec<Resource>) -> Self {
149 Self {
150 reads: resources,
151 writes: vec![],
152 invokes: vec![],
153 spawns: false,
154 destructive: false,
155 sandbox: Sandbox::Inherit,
156 cost: CostEstimate::default(),
157 }
158 }
159
160 #[must_use]
165 pub fn is_available_in(&self, brain_wave: crate::BrainWave) -> bool {
166 use crate::BrainWave::{Alpha, Beta, Delta, Gamma, Theta};
167 match brain_wave {
168 Gamma => true,
169 Beta => true,
170 Alpha => !self.cost.expensive && self.writes.is_empty(),
171 Theta => !self.cost.expensive && self.writes.is_empty() && !self.spawns,
172 Delta => false, }
174 }
175
176 #[must_use]
180 pub fn acquires_coordination_lease(&self) -> bool {
181 self.writes
182 .iter()
183 .any(|r| matches!(r, Resource::CoordinationLease))
184 }
185
186 #[must_use]
191 pub fn is_coordination_cleanup(&self) -> bool {
192 !self.spawns
193 && !self.destructive
194 && self.writes.len() == 1
195 && matches!(self.writes[0], Resource::CoordinationRelease)
196 }
197
198 #[must_use]
203 pub fn is_no_discovery_checkpoint(&self) -> bool {
204 !self.spawns
205 && !self.destructive
206 && self.writes.len() == 1
207 && matches!(&self.writes[0], Resource::Galaxy(g) if g == "sessions")
208 }
209
210 #[must_use]
212 pub fn conflicts_with(&self, other: &Self) -> bool {
213 for w in &self.writes {
215 if other.writes.contains(w) || other.reads.contains(w) {
216 return true;
217 }
218 }
219 for w in &other.writes {
220 if self.reads.contains(w) {
221 return true;
222 }
223 }
224 if self.spawns && other.spawns {
226 return true;
227 }
228 false
229 }
230}
231
232impl fmt::Display for EffectRow {
233 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234 write!(
235 f,
236 "reads:{}, writes:{}, invokes:{}, spawns:{}",
237 self.reads.len(),
238 self.writes.len(),
239 self.invokes.len(),
240 self.spawns
241 )
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn pure_effect_has_no_side_effects() {
251 let e = EffectRow::pure();
252 assert!(e.reads.is_empty());
253 assert!(e.writes.is_empty());
254 assert!(!e.spawns);
255 }
256
257 #[test]
258 fn effect_conflict_detection() {
259 let writer = EffectRow {
260 writes: vec![Resource::Galaxy("citta".into())],
261 ..Default::default()
262 };
263 let reader = EffectRow {
264 reads: vec![Resource::Galaxy("citta".into())],
265 ..Default::default()
266 };
267 assert!(writer.conflicts_with(&reader));
268 assert!(reader.conflicts_with(&writer));
269
270 let other_reader = EffectRow {
271 reads: vec![Resource::Galaxy("codex".into())],
272 ..Default::default()
273 };
274 assert!(!reader.conflicts_with(&other_reader));
275 }
276
277 #[test]
278 fn brain_wave_filtering() {
279 use crate::BrainWave::*;
280 let expensive = EffectRow {
281 cost: CostEstimate {
282 expensive: true,
283 ..Default::default()
284 },
285 ..Default::default()
286 };
287 assert!(expensive.is_available_in(Gamma));
288 assert!(!expensive.is_available_in(Alpha));
289 assert!(!expensive.is_available_in(Delta));
290 }
291
292 #[test]
293 fn sandbox_seam_defaults_and_serializes() {
294 let row = EffectRow::pure();
295 assert_eq!(row.sandbox, Sandbox::Inherit);
296
297 let scoped = EffectRow {
298 sandbox: Sandbox::StoreScoped,
299 ..Default::default()
300 };
301 assert_eq!(scoped.sandbox, Sandbox::StoreScoped);
302
303 let json = serde_json::to_string(&scoped).expect("serialize");
306 assert!(json.contains("store_scoped"));
307 let back: EffectRow = serde_json::from_str(&json).expect("deserialize");
308 assert_eq!(back.sandbox, Sandbox::StoreScoped);
309 let legacy: EffectRow = serde_json::from_str(
310 "{\"reads\":[],\"writes\":[],\"invokes\":[],\"spawns\":false,\"destructive\":false,\
311 \"cost\":{\"cpu_ns\":0,\"memory_bytes\":0,\"disk_bytes\":0,\"network_bytes\":0,\"expensive\":false}}",
312 )
313 .expect("legacy payload without sandbox field");
314 assert_eq!(legacy.sandbox, Sandbox::Inherit);
315 }
316
317 use crate::BrainWave;
320 use proptest::prelude::*;
321
322 fn arb_resource() -> impl Strategy<Value = Resource> {
323 prop_oneof![
324 Just(Resource::Galaxy("codex".into())),
325 Just(Resource::Galaxy("citta".into())),
326 Just(Resource::Filesystem),
327 Just(Resource::Network),
328 Just(Resource::Process),
329 ]
330 }
331
332 fn arb_effect_row() -> impl Strategy<Value = EffectRow> {
333 (
334 proptest::collection::vec(arb_resource(), 0..6),
335 proptest::collection::vec(arb_resource(), 0..6),
336 any::<bool>(),
337 any::<bool>(),
338 )
339 .prop_map(|(reads, writes, spawns, expensive)| EffectRow {
340 reads,
341 writes,
342 spawns,
343 cost: CostEstimate {
344 expensive,
345 ..Default::default()
346 },
347 ..Default::default()
348 })
349 }
350
351 proptest! {
352 #[test]
354 fn delta_blocks_all(effects in arb_effect_row()) {
355 prop_assert!(!effects.is_available_in(BrainWave::Delta));
356 }
357
358 #[test]
360 fn gamma_allows_all(effects in arb_effect_row()) {
361 prop_assert!(effects.is_available_in(BrainWave::Gamma));
362 }
363
364 #[test]
366 fn beta_allows_all(effects in arb_effect_row()) {
367 prop_assert!(effects.is_available_in(BrainWave::Beta));
368 }
369
370 #[test]
372 fn alpha_blocks_writes_and_expensive(effects in arb_effect_row()) {
373 let result = effects.is_available_in(BrainWave::Alpha);
374 if !effects.writes.is_empty() || effects.cost.expensive {
375 prop_assert!(!result, "Alpha should block writes/expensive: {effects}");
376 } else {
377 prop_assert!(result, "Alpha should allow pure reads: {effects}");
378 }
379 }
380
381 #[test]
383 fn theta_blocks_writes_spawns_expensive(effects in arb_effect_row()) {
384 let result = effects.is_available_in(BrainWave::Theta);
385 if !effects.writes.is_empty() || effects.cost.expensive || effects.spawns {
386 prop_assert!(!result, "Theta should block: {effects}");
387 } else {
388 prop_assert!(result, "Theta should allow pure reads: {effects}");
389 }
390 }
391
392 #[test]
394 fn conflicts_symmetric(a in arb_effect_row(), b in arb_effect_row()) {
395 let ab = a.conflicts_with(&b);
396 let ba = b.conflicts_with(&a);
397 prop_assert_eq!(ab, ba, "conflicts_with must be symmetric");
398 }
399
400 #[test]
402 fn conflicts_self_with_writes_or_spawns(effects in arb_effect_row()) {
403 let self_conflict = effects.conflicts_with(&effects);
404 if !effects.writes.is_empty() || effects.spawns {
405 prop_assert!(self_conflict, "effect row with writes/spawns should conflict with itself");
406 } else {
407 prop_assert!(!self_conflict, "pure effect row should not conflict with itself");
408 }
409 }
410 }
411}