1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10#[serde(tag = "kind", rename_all = "snake_case")]
11pub enum Effect {
12 KnowledgeTransfer {
14 from: String,
16 to: String,
18 claim: String,
20 provenance: Option<String>,
22 initial_confidence: Option<f64>,
24 },
25 RelationshipDelta {
27 axis: String,
29 from: String,
31 to: String,
33 delta: f64,
35 },
36 EmotionalEvent {
38 target: String,
40 emotion: String,
42 intensity: f64,
44 },
45 MoodShift {
47 target: String,
49 axis: String,
51 delta: f64,
53 },
54 NeedSatisfaction {
56 target: String,
58 need: String,
60 amount: f64,
62 },
63 ValueShift {
65 target: String,
67 value: String,
69 delta: f64,
71 },
72 PracticeExit {
74 actor: String,
76 reason: Option<String>,
78 },
79}
80
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
83pub struct DriveAlignment {
84 pub kind: String,
86 pub strength: f64,
88}
89
90#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92pub struct ConsiderationSpec {
93 pub id: String,
95 pub curve: String,
97 pub weight: f64,
99 #[serde(default)]
101 pub threshold: Option<f64>,
102}
103
104#[derive(Debug, Clone, PartialEq, Serialize)]
109pub struct Beat {
110 pub actor: String,
112 pub action: String,
114 pub accepted: bool,
116 pub effects: Vec<Effect>,
118}
119
120#[derive(Debug, Clone, PartialEq, Serialize)]
126pub struct EncounterResult {
127 pub participants: Vec<String>,
129 pub practice: Option<String>,
131 pub beats: Vec<Beat>,
133 pub relationship_deltas: Vec<Effect>,
135 pub knowledge_transfers: Vec<Effect>,
137 pub emotional_events: Vec<Effect>,
139 pub mood_shifts: Vec<Effect>,
141 pub need_satisfactions: Vec<Effect>,
143 pub value_shifts: Vec<Effect>,
145 pub practice_exits: Vec<Effect>,
147 pub escalation_requested: bool,
149 pub escalation_requests: Vec<crate::escalation::EscalationRequest>,
151}
152
153impl EncounterResult {
154 pub fn new(participants: Vec<String>, practice: Option<String>) -> Self {
156 Self {
157 participants,
158 practice,
159 beats: Vec::new(),
160 relationship_deltas: Vec::new(),
161 knowledge_transfers: Vec::new(),
162 emotional_events: Vec::new(),
163 mood_shifts: Vec::new(),
164 need_satisfactions: Vec::new(),
165 value_shifts: Vec::new(),
166 practice_exits: Vec::new(),
167 escalation_requested: false,
168 escalation_requests: Vec::new(),
169 }
170 }
171
172 pub fn push_beat(&mut self, beat: Beat) {
177 for effect in &beat.effects {
178 match effect {
179 Effect::RelationshipDelta { .. } => {
180 self.relationship_deltas.push(effect.clone());
181 }
182 Effect::KnowledgeTransfer { .. } => {
183 self.knowledge_transfers.push(effect.clone());
184 }
185 Effect::EmotionalEvent { .. } => {
186 self.emotional_events.push(effect.clone());
187 }
188 Effect::MoodShift { .. } => {
189 self.mood_shifts.push(effect.clone());
190 }
191 Effect::NeedSatisfaction { .. } => {
192 self.need_satisfactions.push(effect.clone());
193 }
194 Effect::ValueShift { .. } => {
195 self.value_shifts.push(effect.clone());
196 }
197 Effect::PracticeExit { .. } => {
198 self.practice_exits.push(effect.clone());
199 }
200 }
201 }
202 self.beats.push(beat);
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn effect_deserializes_from_toml() {
212 let s = r#"
213 kind = "relationship_delta"
214 axis = "trust"
215 from = "alice"
216 to = "bob"
217 delta = 0.25
218 "#;
219 let effect: Effect = toml::from_str(s).expect("should deserialize");
220 match effect {
221 Effect::RelationshipDelta { delta, .. } => {
222 assert!((delta - 0.25).abs() < f64::EPSILON);
223 }
224 other => panic!("unexpected variant: {other:?}"),
225 }
226 }
227
228 #[test]
229 fn knowledge_transfer_deserializes_with_optional_fields() {
230 let s = r#"
231 kind = "knowledge_transfer"
232 from = "alice"
233 to = "bob"
234 claim = "the vault is open"
235 "#;
236 let effect: Effect = toml::from_str(s).expect("should deserialize");
237 match effect {
238 Effect::KnowledgeTransfer {
239 provenance,
240 initial_confidence,
241 ..
242 } => {
243 assert!(provenance.is_none());
244 assert!(initial_confidence.is_none());
245 }
246 other => panic!("unexpected variant: {other:?}"),
247 }
248 }
249
250 #[test]
251 fn drive_alignment_deserializes() {
252 let s = r#"
253 kind = "belonging"
254 strength = 0.8
255 "#;
256 let da: DriveAlignment = toml::from_str(s).expect("should deserialize");
257 assert_eq!(da.kind, "belonging");
258 assert!((da.strength - 0.8).abs() < f64::EPSILON);
259 }
260
261 #[test]
262 fn encounter_result_categorizes_effects() {
263 let mut result = EncounterResult::new(
264 vec!["alice".to_string(), "bob".to_string()],
265 Some("negotiation".to_string()),
266 );
267
268 let beat = Beat {
269 actor: "alice".to_string(),
270 action: "share_secret".to_string(),
271 accepted: true,
272 effects: vec![
273 Effect::KnowledgeTransfer {
274 from: "alice".to_string(),
275 to: "bob".to_string(),
276 claim: "the vault is open".to_string(),
277 provenance: None,
278 initial_confidence: None,
279 },
280 Effect::RelationshipDelta {
281 axis: "trust".to_string(),
282 from: "bob".to_string(),
283 to: "alice".to_string(),
284 delta: 0.1,
285 },
286 ],
287 };
288
289 result.push_beat(beat);
290
291 assert_eq!(result.beats.len(), 1);
292 assert_eq!(result.knowledge_transfers.len(), 1);
293 assert_eq!(result.relationship_deltas.len(), 1);
294 assert_eq!(result.emotional_events.len(), 0);
295 assert_eq!(result.value_shifts.len(), 0);
296 }
297
298 #[test]
299 fn encounter_result_categorizes_all_seven_variants() {
300 let mut result = EncounterResult::new(vec!["alice".into(), "bob".into()], None);
301
302 let beat = Beat {
303 actor: "alice".into(),
304 action: "complex_action".into(),
305 accepted: true,
306 effects: vec![
307 Effect::RelationshipDelta {
308 axis: "trust".into(),
309 from: "alice".into(),
310 to: "bob".into(),
311 delta: 0.1,
312 },
313 Effect::KnowledgeTransfer {
314 from: "alice".into(),
315 to: "bob".into(),
316 claim: "test".into(),
317 provenance: None,
318 initial_confidence: None,
319 },
320 Effect::EmotionalEvent {
321 target: "bob".into(),
322 emotion: "joy".into(),
323 intensity: 0.5,
324 },
325 Effect::MoodShift {
326 target: "bob".into(),
327 axis: "calm".into(),
328 delta: 0.2,
329 },
330 Effect::NeedSatisfaction {
331 target: "bob".into(),
332 need: "belonging".into(),
333 amount: 0.3,
334 },
335 Effect::ValueShift {
336 target: "bob".into(),
337 value: "honesty".into(),
338 delta: 0.05,
339 },
340 Effect::PracticeExit {
341 actor: "bob".into(),
342 reason: Some("satisfied".into()),
343 },
344 ],
345 };
346
347 result.push_beat(beat);
348
349 assert_eq!(result.relationship_deltas.len(), 1);
350 assert_eq!(result.knowledge_transfers.len(), 1);
351 assert_eq!(result.emotional_events.len(), 1);
352 assert_eq!(result.mood_shifts.len(), 1);
353 assert_eq!(result.need_satisfactions.len(), 1);
354 assert_eq!(result.value_shifts.len(), 1);
355 assert_eq!(result.practice_exits.len(), 1);
356 }
357}