Skip to main content

qualia_client_core/wellfair/
body_constitution.rs

1//! The person's **declared body constitution** — measurements, characteristics, attributes.
2//!
3//! Forum-internum / Sanctuary-class. Stored next to the physiological-state declaration.
4//! Absence means "not declared" — the caller uses an identity fit, never an assumed body.
5
6use std::path::Path;
7
8use wellfare_core::anatomy::{BodyConstitution, BodyFit, PhysiologicalState};
9
10/// Where the constitution lives (prefs-style, under the `wellfair/` prefix).
11pub const CONSTITUTION_FILE: &str = "wellfair/body_constitution.json";
12
13pub fn load(storage_root: impl AsRef<Path>) -> Option<BodyConstitution> {
14    let path = storage_root.as_ref().join(CONSTITUTION_FILE);
15    let bytes = std::fs::read(path).ok()?;
16    serde_json::from_slice(&bytes).ok()
17}
18
19pub fn save(storage_root: impl AsRef<Path>, body: &BodyConstitution) -> Result<(), String> {
20    body.validate()?;
21    let path = storage_root.as_ref().join(CONSTITUTION_FILE);
22    if let Some(parent) = path.parent() {
23        std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
24    }
25    let bytes = serde_json::to_vec_pretty(body).map_err(|e| e.to_string())?;
26    std::fs::write(path, bytes).map_err(|e| e.to_string())
27}
28
29pub fn clear(storage_root: impl AsRef<Path>) -> Result<(), String> {
30    let path = storage_root.as_ref().join(CONSTITUTION_FILE);
31    match std::fs::remove_file(path) {
32        Ok(()) => Ok(()),
33        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
34        Err(e) => Err(e.to_string()),
35    }
36}
37
38/// Fit using the constitution plus a physiological state so pregnancy is one source of truth.
39/// A declared pregnant physiological state wins; otherwise the constitution's own hint is used.
40pub fn fit_for(constitution: &BodyConstitution, phys: &PhysiologicalState) -> BodyFit {
41    let pregnancy = match phys {
42        PhysiologicalState::Reproductive(wellfare_core::anatomy::ReproductiveState::Pregnant(t)) => {
43            Some(*t)
44        }
45        _ => constitution.attributes.pregnancy,
46    };
47    constitution.fit_with_pregnancy(pregnancy)
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use wellfare_core::anatomy::{Karyotype, ReproductiveState};
54
55    #[test]
56    fn persists_and_clears() {
57        let dir = tempfile::tempdir().unwrap();
58        assert!(load(dir.path()).is_none());
59        let mut c = BodyConstitution::default();
60        c.measurements.stature_mm = Some(1720);
61        c.characteristics.karyotype = Some(Karyotype::Xx);
62        c.knowledge.ethnicities.push(
63            wellfare_core::anatomy::EthnicityAffiliation::declared("Ashkenazi").unwrap(),
64        );
65        c.attributes.eye_colour = Some("brown".into());
66        c.measurements.sleeve_mm = Some(610);
67        c.measurements.foot_left_mm = Some(265);
68        save(dir.path(), &c).unwrap();
69        let back = load(dir.path()).unwrap();
70        assert_eq!(back.measurements.stature_mm, Some(1720));
71        assert_eq!(back.knowledge.ethnicities.len(), 1);
72        assert_eq!(back.knowledge.ethnicities[0].token, "ashkenazi");
73        assert_eq!(back.attributes.eye_colour.as_deref(), Some("brown"));
74        assert_eq!(back.measurements.sleeve_mm, Some(610));
75        assert_eq!(back.measurements.foot_left_mm, Some(265));
76        clear(dir.path()).unwrap();
77        assert!(load(dir.path()).is_none());
78    }
79
80    #[test]
81    fn save_rejects_invalid() {
82        let dir = tempfile::tempdir().unwrap();
83        let mut c = BodyConstitution::default();
84        c.measurements.stature_mm = Some(10);
85        assert!(save(dir.path(), &c).is_err());
86    }
87
88    #[test]
89    fn phys_pregnancy_drives_fit() {
90        let c = BodyConstitution::default();
91        let phys = PhysiologicalState::Reproductive(ReproductiveState::Pregnant(
92            wellfare_core::anatomy::Trimester::Third,
93        ));
94        let fit = fit_for(&c, &phys);
95        assert!(fit.pregnancy_abdomen > 0.3);
96    }
97}