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}
100
101#[derive(Debug, Clone, Default, Serialize, Deserialize)]
106pub struct EffectRow {
107 pub reads: Vec<Resource>,
109 pub writes: Vec<Resource>,
111 pub invokes: Vec<Capability>,
113 pub spawns: bool,
115 pub destructive: bool,
118 #[serde(default)]
120 pub sandbox: Sandbox,
121 pub cost: CostEstimate,
123}
124
125impl EffectRow {
126 #[must_use]
128 pub fn pure() -> Self {
129 Self::default()
130 }
131
132 #[must_use]
134 pub fn read_only(resources: Vec<Resource>) -> Self {
135 Self {
136 reads: resources,
137 writes: vec![],
138 invokes: vec![],
139 spawns: false,
140 destructive: false,
141 sandbox: Sandbox::Inherit,
142 cost: CostEstimate::default(),
143 }
144 }
145
146 #[must_use]
151 pub fn is_available_in(&self, brain_wave: crate::BrainWave) -> bool {
152 use crate::BrainWave::{Alpha, Beta, Delta, Gamma, Theta};
153 match brain_wave {
154 Gamma => true,
155 Beta => true,
156 Alpha => !self.cost.expensive && self.writes.is_empty(),
157 Theta => !self.cost.expensive && self.writes.is_empty() && !self.spawns,
158 Delta => false, }
160 }
161
162 #[must_use]
164 pub fn conflicts_with(&self, other: &Self) -> bool {
165 for w in &self.writes {
167 if other.writes.contains(w) || other.reads.contains(w) {
168 return true;
169 }
170 }
171 for w in &other.writes {
172 if self.reads.contains(w) {
173 return true;
174 }
175 }
176 if self.spawns && other.spawns {
178 return true;
179 }
180 false
181 }
182}
183
184impl fmt::Display for EffectRow {
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 write!(
187 f,
188 "reads:{}, writes:{}, invokes:{}, spawns:{}",
189 self.reads.len(),
190 self.writes.len(),
191 self.invokes.len(),
192 self.spawns
193 )
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn pure_effect_has_no_side_effects() {
203 let e = EffectRow::pure();
204 assert!(e.reads.is_empty());
205 assert!(e.writes.is_empty());
206 assert!(!e.spawns);
207 }
208
209 #[test]
210 fn effect_conflict_detection() {
211 let writer = EffectRow {
212 writes: vec![Resource::Galaxy("citta".into())],
213 ..Default::default()
214 };
215 let reader = EffectRow {
216 reads: vec![Resource::Galaxy("citta".into())],
217 ..Default::default()
218 };
219 assert!(writer.conflicts_with(&reader));
220 assert!(reader.conflicts_with(&writer));
221
222 let other_reader = EffectRow {
223 reads: vec![Resource::Galaxy("codex".into())],
224 ..Default::default()
225 };
226 assert!(!reader.conflicts_with(&other_reader));
227 }
228
229 #[test]
230 fn brain_wave_filtering() {
231 use crate::BrainWave::*;
232 let expensive = EffectRow {
233 cost: CostEstimate {
234 expensive: true,
235 ..Default::default()
236 },
237 ..Default::default()
238 };
239 assert!(expensive.is_available_in(Gamma));
240 assert!(!expensive.is_available_in(Alpha));
241 assert!(!expensive.is_available_in(Delta));
242 }
243
244 #[test]
245 fn sandbox_seam_defaults_and_serializes() {
246 let row = EffectRow::pure();
247 assert_eq!(row.sandbox, Sandbox::Inherit);
248
249 let scoped = EffectRow {
250 sandbox: Sandbox::StoreScoped,
251 ..Default::default()
252 };
253 assert_eq!(scoped.sandbox, Sandbox::StoreScoped);
254
255 let json = serde_json::to_string(&scoped).expect("serialize");
258 assert!(json.contains("store_scoped"));
259 let back: EffectRow = serde_json::from_str(&json).expect("deserialize");
260 assert_eq!(back.sandbox, Sandbox::StoreScoped);
261 let legacy: EffectRow = serde_json::from_str(
262 "{\"reads\":[],\"writes\":[],\"invokes\":[],\"spawns\":false,\"destructive\":false,\
263 \"cost\":{\"cpu_ns\":0,\"memory_bytes\":0,\"disk_bytes\":0,\"network_bytes\":0,\"expensive\":false}}",
264 )
265 .expect("legacy payload without sandbox field");
266 assert_eq!(legacy.sandbox, Sandbox::Inherit);
267 }
268
269 use crate::BrainWave;
272 use proptest::prelude::*;
273
274 fn arb_resource() -> impl Strategy<Value = Resource> {
275 prop_oneof![
276 Just(Resource::Galaxy("codex".into())),
277 Just(Resource::Galaxy("citta".into())),
278 Just(Resource::Filesystem),
279 Just(Resource::Network),
280 Just(Resource::Process),
281 ]
282 }
283
284 fn arb_effect_row() -> impl Strategy<Value = EffectRow> {
285 (
286 proptest::collection::vec(arb_resource(), 0..6),
287 proptest::collection::vec(arb_resource(), 0..6),
288 any::<bool>(),
289 any::<bool>(),
290 )
291 .prop_map(|(reads, writes, spawns, expensive)| EffectRow {
292 reads,
293 writes,
294 spawns,
295 cost: CostEstimate {
296 expensive,
297 ..Default::default()
298 },
299 ..Default::default()
300 })
301 }
302
303 proptest! {
304 #[test]
306 fn delta_blocks_all(effects in arb_effect_row()) {
307 prop_assert!(!effects.is_available_in(BrainWave::Delta));
308 }
309
310 #[test]
312 fn gamma_allows_all(effects in arb_effect_row()) {
313 prop_assert!(effects.is_available_in(BrainWave::Gamma));
314 }
315
316 #[test]
318 fn beta_allows_all(effects in arb_effect_row()) {
319 prop_assert!(effects.is_available_in(BrainWave::Beta));
320 }
321
322 #[test]
324 fn alpha_blocks_writes_and_expensive(effects in arb_effect_row()) {
325 let result = effects.is_available_in(BrainWave::Alpha);
326 if !effects.writes.is_empty() || effects.cost.expensive {
327 prop_assert!(!result, "Alpha should block writes/expensive: {effects}");
328 } else {
329 prop_assert!(result, "Alpha should allow pure reads: {effects}");
330 }
331 }
332
333 #[test]
335 fn theta_blocks_writes_spawns_expensive(effects in arb_effect_row()) {
336 let result = effects.is_available_in(BrainWave::Theta);
337 if !effects.writes.is_empty() || effects.cost.expensive || effects.spawns {
338 prop_assert!(!result, "Theta should block: {effects}");
339 } else {
340 prop_assert!(result, "Theta should allow pure reads: {effects}");
341 }
342 }
343
344 #[test]
346 fn conflicts_symmetric(a in arb_effect_row(), b in arb_effect_row()) {
347 let ab = a.conflicts_with(&b);
348 let ba = b.conflicts_with(&a);
349 prop_assert_eq!(ab, ba, "conflicts_with must be symmetric");
350 }
351
352 #[test]
354 fn conflicts_self_with_writes_or_spawns(effects in arb_effect_row()) {
355 let self_conflict = effects.conflicts_with(&effects);
356 if !effects.writes.is_empty() || effects.spawns {
357 prop_assert!(self_conflict, "effect row with writes/spawns should conflict with itself");
358 } else {
359 prop_assert!(!self_conflict, "pure effect row should not conflict with itself");
360 }
361 }
362 }
363}