use crate::enums::{Direction, DirectionalPath, LifeDomain, RelPath, RelationshipSchema};
use crate::relationship::{
AntecedentDirection, DirectionalDimensions, InteractionPattern, PerceivedRisk,
RelationshipStage, SharedDimensions, TrustAntecedent, TrustContext, TrustDecision,
TrustworthinessFactors,
};
use crate::state::StateValue;
use crate::types::{Duration, EntityId, RelationshipId, Timestamp};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RelationshipError {
SelfRelationship,
InvalidStageTransition {
from: RelationshipStage,
to: RelationshipStage,
},
}
impl std::fmt::Display for RelationshipError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RelationshipError::SelfRelationship => {
write!(f, "Cannot create relationship between an entity and itself")
}
RelationshipError::InvalidStageTransition { from, to } => {
write!(f, "Invalid stage transition from {} to {}", from, to)
}
}
}
}
impl std::error::Error for RelationshipError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StageTransitionError {
pub from: RelationshipStage,
pub to: RelationshipStage,
}
impl StageTransitionError {
#[must_use]
pub fn new(from: RelationshipStage, to: RelationshipStage) -> Self {
StageTransitionError { from, to }
}
}
impl std::fmt::Display for StageTransitionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Invalid stage transition from {} to {}",
self.from, self.to
)
}
}
impl std::error::Error for StageTransitionError {}
const MAX_ANTECEDENT_HISTORY: usize = 100;
#[derive(Debug, Clone)]
pub struct Relationship {
id: RelationshipId,
entity_a: EntityId,
entity_b: EntityId,
schema: RelationshipSchema,
stage: RelationshipStage,
shared: SharedDimensions,
trustworthiness_a_to_b: TrustworthinessFactors,
trustworthiness_b_to_a: TrustworthinessFactors,
perceived_risk_a_to_b: PerceivedRisk,
perceived_risk_b_to_a: PerceivedRisk,
directional_a_to_b: DirectionalDimensions,
directional_b_to_a: DirectionalDimensions,
pattern: InteractionPattern,
antecedent_history_a_to_b: Vec<TrustAntecedent>,
antecedent_history_b_to_a: Vec<TrustAntecedent>,
last_negative_antecedent_a_to_b: Option<Timestamp>,
last_negative_antecedent_b_to_a: Option<Timestamp>,
}
impl Relationship {
pub fn try_between(entity_a: EntityId, entity_b: EntityId) -> Result<Self, RelationshipError> {
if entity_a == entity_b {
return Err(RelationshipError::SelfRelationship);
}
let rel_id = format!("rel_{}_{}", entity_a.as_str(), entity_b.as_str());
let id = RelationshipId::new(rel_id).unwrap();
Ok(Relationship {
id,
entity_a,
entity_b,
schema: RelationshipSchema::default(),
stage: RelationshipStage::default(),
shared: SharedDimensions::new(),
trustworthiness_a_to_b: TrustworthinessFactors::new(),
trustworthiness_b_to_a: TrustworthinessFactors::new(),
perceived_risk_a_to_b: PerceivedRisk::new(),
perceived_risk_b_to_a: PerceivedRisk::new(),
directional_a_to_b: DirectionalDimensions::new(),
directional_b_to_a: DirectionalDimensions::new(),
pattern: InteractionPattern::default(),
antecedent_history_a_to_b: Vec::new(),
antecedent_history_b_to_a: Vec::new(),
last_negative_antecedent_a_to_b: None,
last_negative_antecedent_b_to_a: None,
})
}
#[must_use]
pub fn with_schema(mut self, schema: RelationshipSchema) -> Self {
self.schema = schema;
self
}
#[must_use]
pub fn with_stage(mut self, stage: RelationshipStage) -> Self {
self.stage = stage;
self
}
#[must_use]
pub fn id(&self) -> &RelationshipId {
&self.id
}
#[must_use]
pub fn entity_a(&self) -> &EntityId {
&self.entity_a
}
#[must_use]
pub fn entity_b(&self) -> &EntityId {
&self.entity_b
}
#[must_use]
pub fn entities(&self) -> (&EntityId, &EntityId) {
(&self.entity_a, &self.entity_b)
}
#[must_use]
pub fn schema(&self) -> RelationshipSchema {
self.schema
}
#[must_use]
pub fn stage(&self) -> RelationshipStage {
self.stage
}
#[must_use]
pub fn shared(&self) -> &SharedDimensions {
&self.shared
}
pub fn shared_mut(&mut self) -> &mut SharedDimensions {
&mut self.shared
}
#[must_use]
pub fn pattern(&self) -> &InteractionPattern {
&self.pattern
}
pub fn pattern_mut(&mut self) -> &mut InteractionPattern {
&mut self.pattern
}
pub fn set_schema(&mut self, schema: RelationshipSchema) {
self.schema = schema;
}
pub fn set_stage(&mut self, stage: RelationshipStage) -> Result<(), StageTransitionError> {
self.stage = stage;
Ok(())
}
#[must_use]
pub fn trustworthiness(&self, direction: Direction) -> &TrustworthinessFactors {
match direction {
Direction::AToB => &self.trustworthiness_a_to_b,
Direction::BToA => &self.trustworthiness_b_to_a,
}
}
pub fn trustworthiness_mut(&mut self, direction: Direction) -> &mut TrustworthinessFactors {
match direction {
Direction::AToB => &mut self.trustworthiness_a_to_b,
Direction::BToA => &mut self.trustworthiness_b_to_a,
}
}
#[must_use]
pub fn perceived_risk(&self, direction: Direction) -> &PerceivedRisk {
match direction {
Direction::AToB => &self.perceived_risk_a_to_b,
Direction::BToA => &self.perceived_risk_b_to_a,
}
}
pub fn perceived_risk_mut(&mut self, direction: Direction) -> &mut PerceivedRisk {
match direction {
Direction::AToB => &mut self.perceived_risk_a_to_b,
Direction::BToA => &mut self.perceived_risk_b_to_a,
}
}
#[must_use]
pub fn compute_trust_decision(
&self,
direction: Direction,
trustor_propensity: f32,
context: &TrustContext,
life_domain: LifeDomain,
stakes: Option<crate::enums::ActionStakes>,
) -> TrustDecision {
const RISK_WEIGHT: f32 = 0.6;
let stage = self.stage;
let propensity = trustor_propensity.clamp(0.0, 1.0);
let propensity_weight = stage.propensity_weight();
let trustworthiness_weight = stage.trustworthiness_weight();
let trustworthiness = self.trustworthiness(direction);
let task_trustworthiness = trustworthiness.competence_in(life_domain);
let support_trustworthiness = trustworthiness.benevolence_effective();
let disclosure_trustworthiness = trustworthiness.integrity_effective();
let base_risk = self.perceived_risk(direction).effective();
let risk_multiplier = stakes.unwrap_or_default().risk_multiplier();
let adjusted_risk = (base_risk * risk_multiplier).clamp(0.0, 1.0);
let stage_risk = (adjusted_risk + stage.risk_modifier()).clamp(0.0, 1.0);
let context_multiplier = context.compute_multiplier();
let task_willingness = Self::compute_willingness(
propensity,
task_trustworthiness,
stage_risk,
propensity_weight,
trustworthiness_weight,
RISK_WEIGHT,
context_multiplier,
);
let support_willingness = Self::compute_willingness(
propensity,
support_trustworthiness,
stage_risk,
propensity_weight,
trustworthiness_weight,
RISK_WEIGHT,
context_multiplier,
);
let disclosure_willingness = Self::compute_willingness(
propensity,
disclosure_trustworthiness,
stage_risk,
propensity_weight,
trustworthiness_weight,
RISK_WEIGHT,
context_multiplier,
);
let stage_certainty = match stage {
RelationshipStage::Stranger => 0.2,
RelationshipStage::Acquaintance => 0.4,
RelationshipStage::Established => 0.7,
RelationshipStage::Intimate => 0.85,
RelationshipStage::Estranged => 0.5,
};
let decision_certainty = (stage_certainty + self.pattern.consistency * 0.3).clamp(0.0, 1.0);
let trustee_confidence = (trustworthiness.overall_for_domain(life_domain) * 0.7
+ self.pattern.consistency * 0.3)
.clamp(0.0, 1.0);
TrustDecision::new(
task_willingness,
support_willingness,
disclosure_willingness,
decision_certainty,
trustee_confidence,
)
}
fn compute_willingness(
propensity: f32,
trustworthiness: f32,
perceived_risk: f32,
propensity_weight: f32,
trustworthiness_weight: f32,
risk_weight: f32,
context_multiplier: f32,
) -> f32 {
let base = propensity_weight * propensity
+ trustworthiness_weight * trustworthiness
- risk_weight * perceived_risk;
(base * context_multiplier).clamp(0.0, 1.0)
}
#[must_use]
pub fn directional(&self, direction: Direction) -> &DirectionalDimensions {
match direction {
Direction::AToB => &self.directional_a_to_b,
Direction::BToA => &self.directional_b_to_a,
}
}
pub fn directional_mut(&mut self, direction: Direction) -> &mut DirectionalDimensions {
match direction {
Direction::AToB => &mut self.directional_a_to_b,
Direction::BToA => &mut self.directional_b_to_a,
}
}
pub fn append_antecedent(&mut self, direction: Direction, antecedent: TrustAntecedent) {
match direction {
Direction::AToB => {
Self::push_antecedent(
&mut self.antecedent_history_a_to_b,
antecedent,
&mut self.last_negative_antecedent_a_to_b,
);
}
Direction::BToA => {
Self::push_antecedent(
&mut self.antecedent_history_b_to_a,
antecedent,
&mut self.last_negative_antecedent_b_to_a,
);
}
}
}
#[must_use]
pub fn antecedent_history(&self, direction: Direction) -> &[TrustAntecedent] {
match direction {
Direction::AToB => &self.antecedent_history_a_to_b,
Direction::BToA => &self.antecedent_history_b_to_a,
}
}
#[must_use]
pub fn last_negative_antecedent(&self, direction: Direction) -> Option<Timestamp> {
match direction {
Direction::AToB => self.last_negative_antecedent_a_to_b,
Direction::BToA => self.last_negative_antecedent_b_to_a,
}
}
fn push_antecedent(
history: &mut Vec<TrustAntecedent>,
antecedent: TrustAntecedent,
last_negative: &mut Option<Timestamp>,
) {
if antecedent.direction() == AntecedentDirection::Negative {
*last_negative = Some(antecedent.timestamp());
}
history.push(antecedent);
if history.len() > MAX_ANTECEDENT_HISTORY {
history.sort_by_key(|entry| entry.timestamp());
let overflow = history.len().saturating_sub(MAX_ANTECEDENT_HISTORY);
history.drain(0..overflow);
}
}
#[must_use]
pub fn get(&self, path: RelPath) -> Option<&StateValue> {
match path {
RelPath::Shared(sp) => Some(self.shared.get(sp)),
RelPath::Directional(dir, dp) => self.get_directional(dir, dp),
RelPath::Stage => None, }
}
pub fn get_mut(&mut self, path: RelPath) -> Option<&mut StateValue> {
match path {
RelPath::Shared(sp) => Some(self.shared.get_mut(sp)),
RelPath::Directional(dir, dp) => self.get_directional_mut(dir, dp),
RelPath::Stage => None,
}
}
pub fn apply_delta(&mut self, path: RelPath, amount: f32) {
let consistency = self.pattern.consistency.clamp(0.0, 1.0);
let weight = 0.5 + (consistency * 0.5);
if let Some(value) = self.get_mut(path) { value.add_delta(amount * weight); }
}
fn get_directional(&self, direction: Direction, path: DirectionalPath) -> Option<&StateValue> {
let dims = self.directional(direction);
let trust = self.trustworthiness(direction);
let risk = self.perceived_risk(direction);
match path {
DirectionalPath::Trust(tp) => trust.get(tp),
DirectionalPath::Warmth => Some(dims.warmth()),
DirectionalPath::Resentment => Some(dims.resentment()),
DirectionalPath::Dependence => Some(dims.dependence()),
DirectionalPath::Attraction => Some(dims.attraction()),
DirectionalPath::Attachment => Some(dims.attachment()),
DirectionalPath::Jealousy => Some(dims.jealousy()),
DirectionalPath::Fear => Some(dims.fear()),
DirectionalPath::Obligation => Some(dims.obligation()),
DirectionalPath::PerceivedRisk => Some(risk.state_value()),
}
}
fn get_directional_mut(
&mut self,
direction: Direction,
path: DirectionalPath,
) -> Option<&mut StateValue> {
match path {
DirectionalPath::Trust(tp) => self.trustworthiness_mut(direction).get_mut(tp),
DirectionalPath::Warmth => Some(self.directional_mut(direction).warmth_mut()),
DirectionalPath::Resentment => Some(self.directional_mut(direction).resentment_mut()),
DirectionalPath::Dependence => Some(self.directional_mut(direction).dependence_mut()),
DirectionalPath::Attraction => Some(self.directional_mut(direction).attraction_mut()),
DirectionalPath::Attachment => Some(self.directional_mut(direction).attachment_mut()),
DirectionalPath::Jealousy => Some(self.directional_mut(direction).jealousy_mut()),
DirectionalPath::Fear => Some(self.directional_mut(direction).fear_mut()),
DirectionalPath::Obligation => Some(self.directional_mut(direction).obligation_mut()),
DirectionalPath::PerceivedRisk => {
Some(self.perceived_risk_mut(direction).state_value_mut())
}
}
}
pub fn apply_decay(&mut self, elapsed: Duration) {
self.shared.apply_decay(elapsed);
self.trustworthiness_a_to_b.apply_decay(elapsed);
self.trustworthiness_b_to_a.apply_decay(elapsed);
self.perceived_risk_a_to_b.apply_decay(elapsed);
self.perceived_risk_b_to_a.apply_decay(elapsed);
self.directional_a_to_b.apply_decay(elapsed);
self.directional_b_to_a.apply_decay(elapsed);
}
}
impl PartialEq for Relationship {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
&& self.schema == other.schema
&& self.stage == other.stage
&& self.shared == other.shared
&& self.trustworthiness_a_to_b == other.trustworthiness_a_to_b
&& self.trustworthiness_b_to_a == other.trustworthiness_b_to_a
&& self.perceived_risk_a_to_b == other.perceived_risk_a_to_b
&& self.perceived_risk_b_to_a == other.perceived_risk_b_to_a
&& self.directional_a_to_b == other.directional_a_to_b
&& self.directional_b_to_a == other.directional_b_to_a
&& self.pattern == other.pattern
&& self.antecedent_history_a_to_b == other.antecedent_history_a_to_b
&& self.antecedent_history_b_to_a == other.antecedent_history_b_to_a
&& self.last_negative_antecedent_a_to_b == other.last_negative_antecedent_a_to_b
&& self.last_negative_antecedent_b_to_a == other.last_negative_antecedent_b_to_a
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::enums::{DirectionalPath, SharedPath, TrustPath};
use crate::relationship::{AntecedentDirection, AntecedentType};
use crate::types::Duration;
fn alice() -> EntityId {
EntityId::new("alice").unwrap()
}
fn bob() -> EntityId {
EntityId::new("bob").unwrap()
}
fn carol() -> EntityId {
EntityId::new("carol").unwrap()
}
#[test]
fn append_antecedent_tracks_history_and_last_negative() {
let mut rel = Relationship::try_between(alice(), bob()).unwrap();
let ts_positive = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
let ts_negative = Timestamp::from_ymd_hms(2024, 2, 1, 0, 0, 0);
let positive = TrustAntecedent::new(
ts_positive,
AntecedentType::Benevolence,
AntecedentDirection::Positive,
0.4,
"support",
);
let negative = TrustAntecedent::new(
ts_negative,
AntecedentType::Integrity,
AntecedentDirection::Negative,
0.5,
"betrayal",
);
rel.append_antecedent(Direction::AToB, positive);
rel.append_antecedent(Direction::AToB, negative);
assert_eq!(rel.antecedent_history(Direction::AToB).len(), 2);
assert_eq!(rel.antecedent_history(Direction::BToA).len(), 0);
assert_eq!(
rel.last_negative_antecedent(Direction::AToB),
Some(ts_negative)
);
}
#[test]
fn last_negative_antecedent_tracks_b_to_a() {
let mut rel = Relationship::try_between(alice(), bob()).unwrap();
let ts_negative = Timestamp::from_ymd_hms(2024, 3, 1, 0, 0, 0);
let negative = TrustAntecedent::new(
ts_negative,
AntecedentType::Integrity,
AntecedentDirection::Negative,
0.4,
"betrayal",
);
rel.append_antecedent(Direction::BToA, negative);
assert_eq!(
rel.last_negative_antecedent(Direction::BToA),
Some(ts_negative)
);
}
#[test]
fn antecedent_history_caps_at_max_entries() {
let mut rel = Relationship::try_between(alice(), bob()).unwrap();
let base_time = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
for i in 0..(MAX_ANTECEDENT_HISTORY + 2) {
let ts = base_time + Duration::days(i as u64);
let antecedent = TrustAntecedent::new(
ts,
AntecedentType::Ability,
AntecedentDirection::Positive,
0.1,
"interaction",
);
rel.append_antecedent(Direction::AToB, antecedent);
}
assert_eq!(
rel.antecedent_history(Direction::AToB).len(),
MAX_ANTECEDENT_HISTORY
);
}
#[test]
fn try_between_creates_relationship() {
let rel = Relationship::try_between(alice(), bob()).unwrap();
assert_eq!(rel.entity_a(), &alice());
assert_eq!(rel.entity_b(), &bob());
}
#[test]
fn relationship_creation_with_ids() {
let rel = Relationship::try_between(alice(), bob()).unwrap();
assert!(!rel.id().as_str().is_empty());
assert!(rel.id().as_str().contains("alice"));
assert!(rel.id().as_str().contains("bob"));
}
#[test]
fn relationship_id_accessible() {
let rel = Relationship::try_between(alice(), bob()).unwrap();
let id = rel.id();
assert_eq!(id.as_str(), "rel_alice_bob");
}
#[test]
fn try_between_returns_error_on_self() {
let result = Relationship::try_between(alice(), alice());
assert_eq!(result, Err(RelationshipError::SelfRelationship));
}
#[test]
fn with_schema() {
let rel = Relationship::try_between(alice(), bob())
.unwrap()
.with_schema(RelationshipSchema::Mentor);
assert_eq!(rel.schema(), RelationshipSchema::Mentor);
}
#[test]
fn with_stage() {
let rel = Relationship::try_between(alice(), bob())
.unwrap()
.with_stage(RelationshipStage::Established);
assert_eq!(rel.stage(), RelationshipStage::Established);
}
#[test]
fn entities_accessor() {
let rel = Relationship::try_between(alice(), bob()).unwrap();
let (a, b) = rel.entities();
assert_eq!(a, &alice());
assert_eq!(b, &bob());
}
#[test]
fn set_schema() {
let mut rel = Relationship::try_between(alice(), bob()).unwrap();
rel.set_schema(RelationshipSchema::Romantic);
assert_eq!(rel.schema(), RelationshipSchema::Romantic);
}
#[test]
fn relationship_stage_manual_transition() {
let mut rel = Relationship::try_between(alice(), bob()).unwrap();
assert_eq!(rel.stage(), RelationshipStage::Stranger);
rel.set_stage(RelationshipStage::Acquaintance).unwrap();
assert_eq!(rel.stage(), RelationshipStage::Acquaintance);
rel.set_stage(RelationshipStage::Established).unwrap();
assert_eq!(rel.stage(), RelationshipStage::Established);
}
#[test]
fn directional_trust_asymmetry() {
let mut rel = Relationship::try_between(alice(), bob()).unwrap();
rel.trustworthiness_mut(Direction::AToB)
.add_competence_delta(0.3);
rel.trustworthiness_mut(Direction::BToA)
.add_competence_delta(0.1);
let a_of_b = rel.trustworthiness(Direction::AToB).competence_effective();
let b_of_a = rel.trustworthiness(Direction::BToA).competence_effective();
assert!(a_of_b > b_of_a);
}
#[test]
fn relationship_direction_semantics() {
let mut rel = Relationship::try_between(alice(), bob()).unwrap();
rel.directional_mut(Direction::AToB).add_warmth_delta(0.5);
rel.directional_mut(Direction::BToA).add_warmth_delta(0.1);
let alice_warmth_to_bob = rel.directional(Direction::AToB).warmth_effective();
let bob_warmth_to_alice = rel.directional(Direction::BToA).warmth_effective();
assert!(alice_warmth_to_bob > bob_warmth_to_alice);
}
#[test]
fn rel_path_shared_affinity() {
let rel = Relationship::try_between(alice(), bob()).unwrap();
let affinity = rel.get(RelPath::Shared(SharedPath::Affinity));
assert!(affinity.is_some());
}
#[test]
fn rel_path_directional_trust() {
let rel = Relationship::try_between(alice(), bob()).unwrap();
let competence = rel.get(RelPath::Directional(
Direction::AToB,
DirectionalPath::Trust(TrustPath::Competence),
));
assert!(competence.is_some());
}
#[test]
fn direction_a_to_b_semantics() {
let mut rel = Relationship::try_between(alice(), bob()).unwrap();
let sv = rel
.get_mut(RelPath::Directional(
Direction::AToB,
DirectionalPath::Trust(TrustPath::Benevolence),
))
.expect("directional trust path should be mutable");
sv.add_delta(0.2);
let benevolence = rel.trustworthiness(Direction::AToB).benevolence_effective();
assert!((benevolence - 0.5).abs() < f32::EPSILON); }
#[test]
fn get_returns_none_for_stage() {
let rel = Relationship::try_between(alice(), bob()).unwrap();
assert!(rel.get(RelPath::Stage).is_none());
}
#[test]
fn get_mut_returns_none_for_stage() {
let mut rel = Relationship::try_between(alice(), bob()).unwrap();
assert!(rel.get_mut(RelPath::Stage).is_none());
}
#[test]
fn get_mut_shared_paths_updates_values() {
let mut rel = Relationship::try_between(alice(), bob()).unwrap();
rel.get_mut(RelPath::Shared(SharedPath::Respect))
.unwrap()
.add_delta(0.2);
rel.get_mut(RelPath::Shared(SharedPath::Tension))
.unwrap()
.add_delta(0.3);
rel.get_mut(RelPath::Shared(SharedPath::Intimacy))
.unwrap()
.add_delta(0.4);
rel.get_mut(RelPath::Shared(SharedPath::History))
.unwrap()
.add_delta(0.5);
assert!((rel.shared().respect().delta() - 0.2).abs() < f32::EPSILON);
assert!((rel.shared().tension().delta() - 0.3).abs() < f32::EPSILON);
assert!((rel.shared().intimacy().delta() - 0.4).abs() < f32::EPSILON);
assert!((rel.shared().history().delta() - 0.5).abs() < f32::EPSILON);
}
#[test]
fn get_mut_directional_non_trust_paths() {
let mut rel = Relationship::try_between(alice(), bob()).unwrap();
let paths = [
(DirectionalPath::Warmth, 0.1),
(DirectionalPath::Resentment, 0.2),
(DirectionalPath::Dependence, 0.3),
(DirectionalPath::Attraction, 0.4),
(DirectionalPath::Attachment, 0.5),
(DirectionalPath::Jealousy, 0.6),
(DirectionalPath::Fear, 0.7),
(DirectionalPath::Obligation, 0.8),
];
for (path, delta) in paths {
rel.get_mut(RelPath::Directional(Direction::AToB, path))
.unwrap()
.add_delta(delta);
}
rel.get_mut(RelPath::Directional(
Direction::BToA,
DirectionalPath::PerceivedRisk,
))
.unwrap()
.add_delta(0.2);
let directional = rel.directional(Direction::AToB);
assert!((directional.warmth().delta() - 0.1).abs() < f32::EPSILON);
assert!((directional.resentment().delta() - 0.2).abs() < f32::EPSILON);
assert!((directional.dependence().delta() - 0.3).abs() < f32::EPSILON);
assert!((directional.attraction().delta() - 0.4).abs() < f32::EPSILON);
assert!((directional.attachment().delta() - 0.5).abs() < f32::EPSILON);
assert!((directional.jealousy().delta() - 0.6).abs() < f32::EPSILON);
assert!((directional.fear().delta() - 0.7).abs() < f32::EPSILON);
assert!((directional.obligation().delta() - 0.8).abs() < f32::EPSILON);
assert!((rel.perceived_risk(Direction::BToA).delta() - 0.2).abs() < f32::EPSILON);
}
#[test]
fn apply_delta_scales_by_consistency_for_shared() {
let mut rel = Relationship::try_between(alice(), bob()).unwrap();
rel.pattern_mut().consistency = 0.0;
rel.apply_delta(RelPath::Shared(SharedPath::Affinity), 0.4);
assert!((rel.shared().affinity().delta() - 0.2).abs() < 0.01);
}
#[test]
fn apply_delta_scales_by_consistency_for_directional() {
let mut rel = Relationship::try_between(alice(), bob()).unwrap();
rel.pattern_mut().consistency = 1.0;
rel.apply_delta(
RelPath::Directional(Direction::AToB, DirectionalPath::Warmth),
0.4,
);
assert!((rel.directional(Direction::AToB).warmth().delta() - 0.4).abs() < 0.01);
}
#[test]
fn apply_delta_ignores_stage_path() {
let mut rel = Relationship::try_between(alice(), bob()).unwrap();
let stage_before = rel.stage();
rel.apply_delta(RelPath::Stage, 0.4);
assert_eq!(rel.stage(), stage_before);
}
#[test]
fn trust_competence_update() {
let mut rel = Relationship::try_between(alice(), bob()).unwrap();
rel.trustworthiness_mut(Direction::AToB)
.add_competence_delta(0.3);
let competence = rel.trustworthiness(Direction::AToB).competence_effective();
assert!((competence - 0.6).abs() < f32::EPSILON); }
#[test]
fn trust_benevolence_update() {
let mut rel = Relationship::try_between(alice(), bob()).unwrap();
rel.trustworthiness_mut(Direction::AToB)
.add_benevolence_delta(0.2);
let benevolence = rel.trustworthiness(Direction::AToB).benevolence_effective();
assert!((benevolence - 0.5).abs() < f32::EPSILON);
}
#[test]
fn trust_integrity_update() {
let mut rel = Relationship::try_between(alice(), bob()).unwrap();
rel.trustworthiness_mut(Direction::AToB)
.add_integrity_delta(0.4);
let integrity = rel.trustworthiness(Direction::AToB).integrity_effective();
assert!((integrity - 0.7).abs() < f32::EPSILON);
}
#[test]
fn apply_decay() {
let mut rel = Relationship::try_between(alice(), bob()).unwrap();
rel.shared_mut().add_affinity_delta(0.4);
rel.trustworthiness_mut(Direction::AToB)
.add_competence_delta(0.4);
rel.apply_decay(Duration::days(14));
assert!((rel.shared().affinity().delta() - 0.2).abs() < 0.01);
}
#[test]
fn shared_accessor() {
let rel = Relationship::try_between(alice(), bob()).unwrap();
assert!(rel.shared().affinity_effective() > 0.0);
}
#[test]
fn shared_mut_accessor() {
let mut rel = Relationship::try_between(alice(), bob()).unwrap();
rel.shared_mut().add_affinity_delta(0.2);
assert!((rel.shared().affinity().delta() - 0.2).abs() < f32::EPSILON);
}
#[test]
fn pattern_accessor_returns_reference() {
let rel = Relationship::try_between(alice(), bob()).unwrap();
let pattern = rel.pattern();
assert!(pattern.consistency >= 0.0);
}
#[test]
fn relationship_error_display() {
let err = RelationshipError::SelfRelationship;
let display = format!("{}", err);
assert!(display.contains("self"));
let err2 = RelationshipError::InvalidStageTransition {
from: RelationshipStage::Stranger,
to: RelationshipStage::Intimate,
};
let display2 = format!("{}", err2);
assert!(display2.contains("Stranger"));
assert!(display2.contains("Intimate"));
}
#[test]
fn relationship_error_equality() {
let err1 = RelationshipError::SelfRelationship;
let err2 = RelationshipError::SelfRelationship;
assert_eq!(err1, err2);
let err3 = RelationshipError::InvalidStageTransition {
from: RelationshipStage::Stranger,
to: RelationshipStage::Intimate,
};
let err4 = RelationshipError::InvalidStageTransition {
from: RelationshipStage::Stranger,
to: RelationshipStage::Intimate,
};
assert_eq!(err3, err4);
}
#[test]
fn relationship_error_inequality() {
let err1 = RelationshipError::SelfRelationship;
let err2 = RelationshipError::InvalidStageTransition {
from: RelationshipStage::Stranger,
to: RelationshipStage::Intimate,
};
assert_ne!(err1, err2);
assert_ne!(err2, err1);
}
#[test]
fn relationship_error_inequality_different_fields() {
let err1 = RelationshipError::InvalidStageTransition {
from: RelationshipStage::Stranger,
to: RelationshipStage::Intimate,
};
let err2 = RelationshipError::InvalidStageTransition {
from: RelationshipStage::Acquaintance,
to: RelationshipStage::Intimate,
};
assert_ne!(err1, err2);
let err3 = RelationshipError::InvalidStageTransition {
from: RelationshipStage::Stranger,
to: RelationshipStage::Established,
};
assert_ne!(err1, err3);
}
#[test]
fn clone_and_equality() {
let rel1 = Relationship::try_between(alice(), bob()).unwrap();
let rel2 = rel1.clone();
assert_eq!(rel1, rel2);
}
#[test]
fn inequality_different_entities() {
let rel1 = Relationship::try_between(alice(), bob()).unwrap();
let carol = EntityId::new("carol").unwrap();
let rel2 = Relationship::try_between(alice(), carol).unwrap();
assert_ne!(rel1, rel2);
}
#[test]
fn inequality_modified_shared_dimension() {
let rel1 = Relationship::try_between(alice(), bob()).unwrap();
let mut rel2 = rel1.clone();
rel2.shared_mut().add_affinity_delta(0.5);
assert_ne!(rel1, rel2);
}
#[test]
fn inequality_different_schema() {
let rel1 = Relationship::try_between(alice(), bob()).unwrap();
let mut rel2 = rel1.clone();
rel2.set_schema(RelationshipSchema::Mentor);
assert_ne!(rel1, rel2);
}
#[test]
fn inequality_different_stage() {
let rel1 = Relationship::try_between(alice(), bob()).unwrap();
let mut rel2 = rel1.clone();
rel2.set_stage(RelationshipStage::Established).unwrap();
assert_ne!(rel1, rel2);
}
#[test]
fn inequality_different_trustworthiness() {
let rel1 = Relationship::try_between(alice(), bob()).unwrap();
let mut rel2 = rel1.clone();
rel2.trustworthiness_mut(Direction::AToB)
.add_competence_delta(0.5);
assert_ne!(rel1, rel2);
}
#[test]
fn inequality_different_perceived_risk() {
let rel1 = Relationship::try_between(alice(), bob()).unwrap();
let mut rel2 = rel1.clone();
rel2.perceived_risk_mut(Direction::BToA).add_delta(0.5);
assert_ne!(rel1, rel2);
}
#[test]
fn inequality_different_directional() {
let rel1 = Relationship::try_between(alice(), bob()).unwrap();
let mut rel2 = rel1.clone();
rel2.directional_mut(Direction::AToB).add_warmth_delta(0.5);
assert_ne!(rel1, rel2);
}
#[test]
fn inequality_different_trustworthiness_b_to_a() {
let rel1 = Relationship::try_between(alice(), bob()).unwrap();
let mut rel2 = rel1.clone();
rel2.trustworthiness_mut(Direction::BToA)
.add_benevolence_delta(0.5);
assert_ne!(rel1, rel2);
}
#[test]
fn inequality_different_perceived_risk_a_to_b() {
let rel1 = Relationship::try_between(alice(), bob()).unwrap();
let mut rel2 = rel1.clone();
rel2.perceived_risk_mut(Direction::AToB).add_delta(0.5);
assert_ne!(rel1, rel2);
}
#[test]
fn inequality_different_directional_b_to_a() {
let rel1 = Relationship::try_between(alice(), bob()).unwrap();
let mut rel2 = rel1.clone();
rel2.directional_mut(Direction::BToA)
.add_resentment_delta(0.5);
assert_ne!(rel1, rel2);
}
#[test]
fn debug_format() {
let rel = Relationship::try_between(alice(), bob()).unwrap();
let debug = format!("{:?}", rel);
assert!(debug.contains("Relationship"));
}
#[test]
fn default_stage_is_stranger() {
let rel = Relationship::try_between(alice(), bob()).unwrap();
assert_eq!(rel.stage(), RelationshipStage::Stranger);
}
#[test]
fn default_schema_is_peer() {
let rel = Relationship::try_between(alice(), bob()).unwrap();
assert_eq!(rel.schema(), RelationshipSchema::Peer);
}
#[test]
fn compute_trust_decision_propensity_differs() {
let mut rel = Relationship::try_between(alice(), carol()).unwrap();
rel.trustworthiness_mut(Direction::AToB)
.add_competence_delta(0.4);
rel.trustworthiness_mut(Direction::AToB)
.add_benevolence_delta(0.4);
rel.trustworthiness_mut(Direction::AToB)
.add_integrity_delta(0.4);
rel.perceived_risk_mut(Direction::AToB).set_base(0.4);
let context = TrustContext::new();
let high_prop =
rel.compute_trust_decision(Direction::AToB, 0.8, &context, LifeDomain::Work, None);
let low_prop =
rel.compute_trust_decision(Direction::AToB, 0.3, &context, LifeDomain::Work, None);
assert!(high_prop.task_willingness() > low_prop.task_willingness());
assert!(high_prop.support_willingness() > low_prop.support_willingness());
assert!(high_prop.disclosure_willingness() > low_prop.disclosure_willingness());
}
#[test]
fn compute_trust_decision_risk_suppresses_willingness() {
let mut rel = Relationship::try_between(alice(), carol()).unwrap();
rel.trustworthiness_mut(Direction::AToB)
.add_competence_delta(0.5);
rel.trustworthiness_mut(Direction::AToB)
.add_benevolence_delta(0.5);
rel.trustworthiness_mut(Direction::AToB)
.add_integrity_delta(0.5);
let context = TrustContext::new();
rel.perceived_risk_mut(Direction::AToB).set_base(0.1);
let low_risk =
rel.compute_trust_decision(Direction::AToB, 0.6, &context, LifeDomain::Work, None);
rel.perceived_risk_mut(Direction::AToB).set_base(0.8);
let high_risk =
rel.compute_trust_decision(Direction::AToB, 0.6, &context, LifeDomain::Work, None);
assert!(low_risk.task_willingness() > high_risk.task_willingness());
assert!(low_risk.support_willingness() > high_risk.support_willingness());
assert!(low_risk.disclosure_willingness() > high_risk.disclosure_willingness());
}
#[test]
fn compute_trust_decision_stakes_scale_risk() {
use crate::enums::ActionStakes;
let mut rel = Relationship::try_between(alice(), carol()).unwrap();
rel.trustworthiness_mut(Direction::AToB)
.add_competence_delta(0.5);
rel.trustworthiness_mut(Direction::AToB)
.add_benevolence_delta(0.5);
rel.trustworthiness_mut(Direction::AToB)
.add_integrity_delta(0.5);
rel.perceived_risk_mut(Direction::AToB).set_base(0.6);
let context = TrustContext::new();
let low_stakes = rel.compute_trust_decision(
Direction::AToB,
0.6,
&context,
LifeDomain::Work,
Some(ActionStakes::Low),
);
let high_stakes = rel.compute_trust_decision(
Direction::AToB,
0.6,
&context,
LifeDomain::Work,
Some(ActionStakes::High),
);
assert!(
low_stakes.support_willingness() > high_stakes.support_willingness() + 0.05
);
}
#[test]
fn compute_trust_decision_stage_modulates_weights() {
let mut rel = Relationship::try_between(alice(), carol()).unwrap();
rel.trustworthiness_mut(Direction::AToB)
.add_competence_delta(0.6);
rel.trustworthiness_mut(Direction::AToB)
.add_benevolence_delta(0.6);
rel.trustworthiness_mut(Direction::AToB)
.add_integrity_delta(0.6);
rel.perceived_risk_mut(Direction::AToB).set_base(0.2);
let context = TrustContext::new();
rel.set_stage(RelationshipStage::Stranger).unwrap();
let stranger =
rel.compute_trust_decision(Direction::AToB, 0.2, &context, LifeDomain::Work, None);
rel.set_stage(RelationshipStage::Intimate).unwrap();
let intimate =
rel.compute_trust_decision(Direction::AToB, 0.2, &context, LifeDomain::Work, None);
assert!(intimate.task_willingness() > stranger.task_willingness());
assert!(intimate.support_willingness() > stranger.support_willingness());
assert!(intimate.disclosure_willingness() > stranger.disclosure_willingness());
}
#[test]
fn compute_trust_decision_stage_certainty_mid_stages() {
let mut rel = Relationship::try_between(alice(), carol()).unwrap();
rel.pattern_mut().consistency = 0.0;
rel.trustworthiness_mut(Direction::AToB)
.add_competence_delta(0.6);
rel.trustworthiness_mut(Direction::AToB)
.add_benevolence_delta(0.6);
rel.trustworthiness_mut(Direction::AToB)
.add_integrity_delta(0.6);
rel.perceived_risk_mut(Direction::AToB).set_base(0.2);
let context = TrustContext::new();
rel.set_stage(RelationshipStage::Acquaintance).unwrap();
let acquaintance =
rel.compute_trust_decision(Direction::AToB, 0.4, &context, LifeDomain::Work, None);
rel.set_stage(RelationshipStage::Established).unwrap();
let established =
rel.compute_trust_decision(Direction::AToB, 0.4, &context, LifeDomain::Work, None);
rel.set_stage(RelationshipStage::Estranged).unwrap();
let estranged =
rel.compute_trust_decision(Direction::AToB, 0.4, &context, LifeDomain::Work, None);
assert!(established.decision_certainty() > estranged.decision_certainty());
assert!(estranged.decision_certainty() > acquaintance.decision_certainty());
}
#[test]
fn get_all_directional_paths() {
let rel = Relationship::try_between(alice(), bob()).unwrap();
assert!(rel
.get(RelPath::Directional(
Direction::AToB,
DirectionalPath::Warmth
))
.is_some());
assert!(rel
.get(RelPath::Directional(
Direction::AToB,
DirectionalPath::Resentment
))
.is_some());
assert!(rel
.get(RelPath::Directional(
Direction::AToB,
DirectionalPath::Dependence
))
.is_some());
assert!(rel
.get(RelPath::Directional(
Direction::AToB,
DirectionalPath::Attraction
))
.is_some());
assert!(rel
.get(RelPath::Directional(
Direction::AToB,
DirectionalPath::Attachment
))
.is_some());
assert!(rel
.get(RelPath::Directional(
Direction::AToB,
DirectionalPath::Jealousy
))
.is_some());
assert!(rel
.get(RelPath::Directional(Direction::AToB, DirectionalPath::Fear))
.is_some());
assert!(rel
.get(RelPath::Directional(
Direction::AToB,
DirectionalPath::Obligation
))
.is_some());
assert!(rel
.get(RelPath::Directional(
Direction::AToB,
DirectionalPath::PerceivedRisk
))
.is_some());
}
#[test]
fn get_all_shared_paths() {
let rel = Relationship::try_between(alice(), bob()).unwrap();
assert!(rel.get(RelPath::Shared(SharedPath::Affinity)).is_some());
assert!(rel.get(RelPath::Shared(SharedPath::Respect)).is_some());
assert!(rel.get(RelPath::Shared(SharedPath::Tension)).is_some());
assert!(rel.get(RelPath::Shared(SharedPath::Intimacy)).is_some());
assert!(rel.get(RelPath::Shared(SharedPath::History)).is_some());
}
#[test]
fn get_all_trust_paths() {
let rel = Relationship::try_between(alice(), bob()).unwrap();
assert!(rel
.get(RelPath::Directional(
Direction::AToB,
DirectionalPath::Trust(TrustPath::Competence)
))
.is_some());
assert!(rel
.get(RelPath::Directional(
Direction::AToB,
DirectionalPath::Trust(TrustPath::Benevolence)
))
.is_some());
assert!(rel
.get(RelPath::Directional(
Direction::AToB,
DirectionalPath::Trust(TrustPath::Integrity)
))
.is_some());
}
#[test]
fn get_support_willingness_returns_none() {
let rel = Relationship::try_between(alice(), bob()).unwrap();
assert!(rel
.get(RelPath::Directional(
Direction::AToB,
DirectionalPath::Trust(TrustPath::SupportWillingness)
))
.is_none());
}
#[test]
fn get_mut_support_willingness_returns_none() {
let mut rel = Relationship::try_between(alice(), bob()).unwrap();
assert!(rel
.get_mut(RelPath::Directional(
Direction::AToB,
DirectionalPath::Trust(TrustPath::SupportWillingness)
))
.is_none());
}
#[test]
fn stage_transition_error_new() {
let err =
StageTransitionError::new(RelationshipStage::Stranger, RelationshipStage::Intimate);
assert_eq!(err.from, RelationshipStage::Stranger);
assert_eq!(err.to, RelationshipStage::Intimate);
}
#[test]
fn stage_transition_error_display() {
let err =
StageTransitionError::new(RelationshipStage::Stranger, RelationshipStage::Intimate);
let display = format!("{}", err);
assert!(display.contains("Stranger"));
assert!(display.contains("Intimate"));
assert!(display.contains("Invalid stage transition"));
}
#[test]
fn stage_transition_error_debug() {
let err =
StageTransitionError::new(RelationshipStage::Stranger, RelationshipStage::Intimate);
let debug = format!("{:?}", err);
assert!(debug.contains("StageTransitionError"));
}
#[test]
fn stage_transition_error_clone_and_equality() {
let err1 = StageTransitionError::new(
RelationshipStage::Acquaintance,
RelationshipStage::Estranged,
);
let err2 = err1.clone();
assert_eq!(err1, err2);
}
#[test]
fn stage_transition_error_implements_error_trait() {
let err =
StageTransitionError::new(RelationshipStage::Stranger, RelationshipStage::Intimate);
let error: &dyn std::error::Error = &err;
assert!(error.to_string().contains("Invalid"));
}
#[test]
fn inequality_different_antecedent_history() {
let rel1 = Relationship::try_between(alice(), bob()).unwrap();
let mut rel2 = rel1.clone();
let ts = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
let antecedent = TrustAntecedent::new(
ts,
AntecedentType::Ability,
AntecedentDirection::Positive,
0.5,
"test",
);
rel2.append_antecedent(Direction::AToB, antecedent);
assert_ne!(rel1, rel2);
}
#[test]
fn inequality_different_antecedent_history_b_to_a() {
let rel1 = Relationship::try_between(alice(), bob()).unwrap();
let mut rel2 = rel1.clone();
let ts = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
let antecedent = TrustAntecedent::new(
ts,
AntecedentType::Ability,
AntecedentDirection::Positive,
0.5,
"test",
);
rel2.append_antecedent(Direction::BToA, antecedent);
assert_ne!(rel1, rel2);
}
#[test]
fn inequality_different_last_negative_antecedent() {
let rel1 = Relationship::try_between(alice(), bob()).unwrap();
let mut rel2 = rel1.clone();
let ts = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
let antecedent = TrustAntecedent::new(
ts,
AntecedentType::Integrity,
AntecedentDirection::Negative,
0.5,
"breach",
);
rel2.append_antecedent(Direction::AToB, antecedent);
assert_ne!(rel1, rel2);
}
#[test]
fn inequality_different_last_negative_antecedent_b_to_a() {
let rel1 = Relationship::try_between(alice(), bob()).unwrap();
let mut rel2 = rel1.clone();
let ts = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
let antecedent = TrustAntecedent::new(
ts,
AntecedentType::Integrity,
AntecedentDirection::Negative,
0.5,
"breach",
);
rel2.append_antecedent(Direction::BToA, antecedent);
assert_ne!(rel1, rel2);
}
#[test]
fn inequality_different_pattern() {
let rel1 = Relationship::try_between(alice(), bob()).unwrap();
let mut rel2 = rel1.clone();
rel2.pattern_mut().consistency = 0.9;
assert_ne!(rel1, rel2);
}
}