use crate::{
ArchetypeProfile, BreathAffinity, CosmicTier, GrowthDirection, ModuleEmphasis, TraitWeights,
error::AvataraError,
};
#[allow(clippy::missing_panics_doc)] pub fn compose(weighted: &[(ArchetypeProfile, f64)]) -> Result<ArchetypeProfile, AvataraError> {
if weighted.is_empty() {
return Err(AvataraError::InvalidParameter(
"cannot compose empty profile list".to_string(),
));
}
let total_weight: f64 = weighted.iter().map(|(_, w)| w).sum();
if total_weight <= 0.0 {
return Err(AvataraError::InvalidParameter(
"total weight must be positive".to_string(),
));
}
for (p, w) in weighted {
if *w < 0.0 {
return Err(AvataraError::InvalidParameter(format!(
"negative weight {} for profile '{}'",
w, p.name
)));
}
}
let traits = blend_traits(weighted, total_weight);
let emphasis = blend_emphasis(weighted, total_weight);
let breath = blend_breath(weighted, total_weight);
let growth = blend_growth(weighted);
let name = weighted
.iter()
.filter(|(_, w)| *w > 0.0)
.map(|(p, _)| p.name.as_str())
.collect::<Vec<_>>()
.join(" + ");
let traditions: Vec<&str> = {
let mut ts: Vec<&str> = weighted
.iter()
.filter(|(_, w)| *w > 0.0)
.map(|(p, _)| p.tradition.as_str())
.collect();
ts.dedup();
ts
};
let tradition = traditions.join(" + ");
let description = format!(
"Composite archetype blending {} tradition{}",
traditions.len(),
if traditions.len() == 1 { "" } else { "s" }
);
let dominant = weighted
.iter()
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(p, _)| p)
.expect("non-empty weighted slice (checked above)");
let element = dominant.element;
let polarity = dominant.polarity;
let tier = blend_tier(weighted);
Ok(ArchetypeProfile {
name,
tradition,
description,
traits,
emphasis,
breath,
growth,
element,
polarity,
tier,
soul_text: dominant.soul_text.clone(),
spirit_text: dominant.spirit_text.clone(),
})
}
fn blend_traits(weighted: &[(ArchetypeProfile, f64)], total: f64) -> TraitWeights {
let mut t = TraitWeights {
warmth: 0.0,
humor: 0.0,
empathy: 0.0,
patience: 0.0,
confidence: 0.0,
curiosity: 0.0,
creativity: 0.0,
directness: 0.0,
formality: 0.0,
verbosity: 0.0,
courage: 0.0,
precision: 0.0,
skepticism: 0.0,
autonomy: 0.0,
pedagogy: 0.0,
};
for (p, w) in weighted {
let f = w / total;
t.warmth += p.traits.warmth * f;
t.humor += p.traits.humor * f;
t.empathy += p.traits.empathy * f;
t.patience += p.traits.patience * f;
t.confidence += p.traits.confidence * f;
t.curiosity += p.traits.curiosity * f;
t.creativity += p.traits.creativity * f;
t.directness += p.traits.directness * f;
t.formality += p.traits.formality * f;
t.verbosity += p.traits.verbosity * f;
t.courage += p.traits.courage * f;
t.precision += p.traits.precision * f;
t.skepticism += p.traits.skepticism * f;
t.autonomy += p.traits.autonomy * f;
t.pedagogy += p.traits.pedagogy * f;
}
t
}
fn blend_emphasis(weighted: &[(ArchetypeProfile, f64)], total: f64) -> ModuleEmphasis {
let mut e = ModuleEmphasis {
mood: 0.0,
energy: 0.0,
stress: 0.0,
growth: 0.0,
spirit: 0.0,
reasoning: 0.0,
regulation: 0.0,
relationship: 0.0,
flow: 0.0,
belief: 0.0,
intuition: 0.0,
salience: 0.0,
appraisal: 0.0,
eq: 0.0,
};
for (p, w) in weighted {
let f = w / total;
e.mood += p.emphasis.mood * f;
e.energy += p.emphasis.energy * f;
e.stress += p.emphasis.stress * f;
e.growth += p.emphasis.growth * f;
e.spirit += p.emphasis.spirit * f;
e.reasoning += p.emphasis.reasoning * f;
e.regulation += p.emphasis.regulation * f;
e.relationship += p.emphasis.relationship * f;
e.flow += p.emphasis.flow * f;
e.belief += p.emphasis.belief * f;
e.intuition += p.emphasis.intuition * f;
e.salience += p.emphasis.salience * f;
e.appraisal += p.emphasis.appraisal * f;
e.eq += p.emphasis.eq * f;
}
e
}
fn blend_breath(weighted: &[(ArchetypeProfile, f64)], total: f64) -> BreathAffinity {
let avg_intensity: f64 = weighted
.iter()
.map(|(p, w)| p.breath.intensity() * (w / total))
.sum();
nearest_breath(avg_intensity)
}
fn nearest_breath(intensity: f64) -> BreathAffinity {
let candidates = [
BreathAffinity::Unity,
BreathAffinity::EarlyExhale,
BreathAffinity::MidExhale,
BreathAffinity::LateExhale,
BreathAffinity::EarlyInhale,
BreathAffinity::MidInhale,
BreathAffinity::LateInhale,
];
candidates
.into_iter()
.min_by(|a, b| {
let da = (a.intensity() - intensity).abs();
let db = (b.intensity() - intensity).abs();
da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap_or(BreathAffinity::LateExhale)
}
fn blend_growth(weighted: &[(ArchetypeProfile, f64)]) -> GrowthDirection {
use std::collections::HashMap;
let mut scores: HashMap<GrowthDirection, f64> = HashMap::new();
for (p, w) in weighted {
*scores.entry(p.growth).or_default() += w;
}
scores
.into_iter()
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(g, _)| g)
.unwrap_or_default()
}
fn blend_tier(weighted: &[(ArchetypeProfile, f64)]) -> CosmicTier {
use std::collections::HashMap;
let mut scores: HashMap<CosmicTier, f64> = HashMap::new();
for (p, w) in weighted {
*scores.entry(p.tier).or_default() += w;
}
scores
.into_iter()
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(t, _)| t)
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Archetype;
use crate::hindu::Trimurti;
use crate::kabbalah::Sephira;
use crate::olympian::Olympian;
#[test]
fn compose_single_profile_is_identity() {
let original = Sephira::Tiphareth.profile();
let composed = compose(&[(original.clone(), 1.0)]).unwrap();
assert_eq!(composed.name, "Tiphareth");
assert!((composed.traits.warmth - original.traits.warmth).abs() < 1e-10);
assert!((composed.traits.courage - original.traits.courage).abs() < 1e-10);
assert_eq!(composed.breath, original.breath);
assert_eq!(composed.growth, original.growth);
}
#[test]
fn compose_equal_weights_averages() {
let a = Sephira::Gevurah.profile(); let b = Sephira::Chesed.profile(); let composed = compose(&[(a, 1.0), (b, 1.0)]).unwrap();
assert!((composed.traits.directness - 0.7).abs() < 0.01);
}
#[test]
fn compose_cross_tradition() {
let result = compose(&[
(Sephira::Tiphareth.profile(), 1.0),
(Trimurti::Vishnu.profile(), 0.8),
(Olympian::Athena.profile(), 0.6),
])
.unwrap();
assert!(result.name.contains("Tiphareth"));
assert!(result.name.contains("Vishnu"));
assert!(result.name.contains("Athena"));
assert!(result.tradition.contains("Kabbalah"));
assert!(result.tradition.contains("Hindu"));
assert!(result.tradition.contains("Greek"));
}
#[test]
fn compose_empty_is_error() {
let result = compose(&[]);
assert!(result.is_err());
}
#[test]
fn compose_negative_weight_is_error() {
let p = Sephira::Kether.profile();
let result = compose(&[(p, -1.0)]);
assert!(result.is_err());
}
#[test]
fn compose_zero_weight_excluded_from_name() {
let a = Sephira::Kether.profile();
let b = Sephira::Malkuth.profile();
let composed = compose(&[(a, 1.0), (b, 0.0)]).unwrap();
assert!(composed.name.contains("Kether"));
assert!(!composed.name.contains("Malkuth"));
}
#[test]
fn compose_breath_blends_by_intensity() {
let unity = Sephira::Kether.profile();
let manifest = Sephira::Malkuth.profile();
let composed = compose(&[(unity, 1.0), (manifest, 1.0)]).unwrap();
assert_eq!(composed.breath, BreathAffinity::MidExhale);
}
#[test]
fn compose_growth_picks_dominant() {
let vishnu = Trimurti::Vishnu.profile(); let tiphareth = Sephira::Tiphareth.profile(); let shiva = Trimurti::Shiva.profile(); let composed = compose(&[(vishnu, 1.0), (tiphareth, 1.0), (shiva, 1.0)]).unwrap();
assert_eq!(composed.growth, GrowthDirection::Preserve);
}
#[test]
fn compose_traits_stay_in_range() {
let profiles: Vec<_> = Sephira::ALL.iter().map(|s| (s.profile(), 1.0)).collect();
let composed = compose(&profiles).unwrap();
assert!(composed.traits.warmth >= 0.0 && composed.traits.warmth <= 1.0);
assert!(composed.traits.courage >= 0.0 && composed.traits.courage <= 1.0);
}
}