use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Effect {
KnowledgeTransfer {
from: String,
to: String,
claim: String,
provenance: Option<String>,
initial_confidence: Option<f64>,
},
RelationshipDelta {
axis: String,
from: String,
to: String,
delta: f64,
},
EmotionalEvent {
target: String,
emotion: String,
intensity: f64,
},
MoodShift {
target: String,
axis: String,
delta: f64,
},
NeedSatisfaction {
target: String,
need: String,
amount: f64,
},
ValueShift {
target: String,
value: String,
delta: f64,
},
PracticeExit {
actor: String,
reason: Option<String>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DriveAlignment {
pub kind: String,
pub strength: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ConsiderationSpec {
pub id: String,
pub curve: String,
pub weight: f64,
#[serde(default)]
pub threshold: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Beat {
pub actor: String,
pub action: String,
pub accepted: bool,
pub effects: Vec<Effect>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct EncounterResult {
pub participants: Vec<String>,
pub practice: Option<String>,
pub beats: Vec<Beat>,
pub relationship_deltas: Vec<Effect>,
pub knowledge_transfers: Vec<Effect>,
pub emotional_events: Vec<Effect>,
pub mood_shifts: Vec<Effect>,
pub need_satisfactions: Vec<Effect>,
pub value_shifts: Vec<Effect>,
pub practice_exits: Vec<Effect>,
pub escalation_requested: bool,
pub escalation_requests: Vec<crate::escalation::EscalationRequest>,
}
impl EncounterResult {
pub fn new(participants: Vec<String>, practice: Option<String>) -> Self {
Self {
participants,
practice,
beats: Vec::new(),
relationship_deltas: Vec::new(),
knowledge_transfers: Vec::new(),
emotional_events: Vec::new(),
mood_shifts: Vec::new(),
need_satisfactions: Vec::new(),
value_shifts: Vec::new(),
practice_exits: Vec::new(),
escalation_requested: false,
escalation_requests: Vec::new(),
}
}
pub fn push_beat(&mut self, beat: Beat) {
for effect in &beat.effects {
match effect {
Effect::RelationshipDelta { .. } => {
self.relationship_deltas.push(effect.clone());
}
Effect::KnowledgeTransfer { .. } => {
self.knowledge_transfers.push(effect.clone());
}
Effect::EmotionalEvent { .. } => {
self.emotional_events.push(effect.clone());
}
Effect::MoodShift { .. } => {
self.mood_shifts.push(effect.clone());
}
Effect::NeedSatisfaction { .. } => {
self.need_satisfactions.push(effect.clone());
}
Effect::ValueShift { .. } => {
self.value_shifts.push(effect.clone());
}
Effect::PracticeExit { .. } => {
self.practice_exits.push(effect.clone());
}
}
}
self.beats.push(beat);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn effect_deserializes_from_toml() {
let s = r#"
kind = "relationship_delta"
axis = "trust"
from = "alice"
to = "bob"
delta = 0.25
"#;
let effect: Effect = toml::from_str(s).expect("should deserialize");
match effect {
Effect::RelationshipDelta { delta, .. } => {
assert!((delta - 0.25).abs() < f64::EPSILON);
}
other => panic!("unexpected variant: {other:?}"),
}
}
#[test]
fn knowledge_transfer_deserializes_with_optional_fields() {
let s = r#"
kind = "knowledge_transfer"
from = "alice"
to = "bob"
claim = "the vault is open"
"#;
let effect: Effect = toml::from_str(s).expect("should deserialize");
match effect {
Effect::KnowledgeTransfer {
provenance,
initial_confidence,
..
} => {
assert!(provenance.is_none());
assert!(initial_confidence.is_none());
}
other => panic!("unexpected variant: {other:?}"),
}
}
#[test]
fn drive_alignment_deserializes() {
let s = r#"
kind = "belonging"
strength = 0.8
"#;
let da: DriveAlignment = toml::from_str(s).expect("should deserialize");
assert_eq!(da.kind, "belonging");
assert!((da.strength - 0.8).abs() < f64::EPSILON);
}
#[test]
fn encounter_result_categorizes_effects() {
let mut result = EncounterResult::new(
vec!["alice".to_string(), "bob".to_string()],
Some("negotiation".to_string()),
);
let beat = Beat {
actor: "alice".to_string(),
action: "share_secret".to_string(),
accepted: true,
effects: vec![
Effect::KnowledgeTransfer {
from: "alice".to_string(),
to: "bob".to_string(),
claim: "the vault is open".to_string(),
provenance: None,
initial_confidence: None,
},
Effect::RelationshipDelta {
axis: "trust".to_string(),
from: "bob".to_string(),
to: "alice".to_string(),
delta: 0.1,
},
],
};
result.push_beat(beat);
assert_eq!(result.beats.len(), 1);
assert_eq!(result.knowledge_transfers.len(), 1);
assert_eq!(result.relationship_deltas.len(), 1);
assert_eq!(result.emotional_events.len(), 0);
assert_eq!(result.value_shifts.len(), 0);
}
#[test]
fn encounter_result_categorizes_all_seven_variants() {
let mut result = EncounterResult::new(vec!["alice".into(), "bob".into()], None);
let beat = Beat {
actor: "alice".into(),
action: "complex_action".into(),
accepted: true,
effects: vec![
Effect::RelationshipDelta {
axis: "trust".into(),
from: "alice".into(),
to: "bob".into(),
delta: 0.1,
},
Effect::KnowledgeTransfer {
from: "alice".into(),
to: "bob".into(),
claim: "test".into(),
provenance: None,
initial_confidence: None,
},
Effect::EmotionalEvent {
target: "bob".into(),
emotion: "joy".into(),
intensity: 0.5,
},
Effect::MoodShift {
target: "bob".into(),
axis: "calm".into(),
delta: 0.2,
},
Effect::NeedSatisfaction {
target: "bob".into(),
need: "belonging".into(),
amount: 0.3,
},
Effect::ValueShift {
target: "bob".into(),
value: "honesty".into(),
delta: 0.05,
},
Effect::PracticeExit {
actor: "bob".into(),
reason: Some("satisfied".into()),
},
],
};
result.push_beat(beat);
assert_eq!(result.relationship_deltas.len(), 1);
assert_eq!(result.knowledge_transfers.len(), 1);
assert_eq!(result.emotional_events.len(), 1);
assert_eq!(result.mood_shifts.len(), 1);
assert_eq!(result.need_satisfactions.len(), 1);
assert_eq!(result.value_shifts.len(), 1);
assert_eq!(result.practice_exits.len(), 1);
}
}