Skip to main content

qualia_client_core/wellfair/
physiology_prefs.rs

1//! **The person's own physiological-state declaration** — where they are on the reproductive continuum.
2//!
3//! The reproductive-continuum layer (P1, `wellfare-core::anatomy::physiology`) models the continuum as
4//! whole-body physiological states. The `StateModulator` re-parameterises all body systems by the current
5//! state — but the person must be able to **declare** where they are on the continuum, so the score-card
6//! reads them at their current life stage, not a neutral baseline.
7//!
8//! This is the person's own, self-declared state — **forum-internum / Sanctuary-class** selfhood content
9//! (their inward knowledge of their own body), stored under the same prefs-style prefix as the weight model.
10//! Absence of a stored state means "the person has not declared one" — the caller falls back to
11//! [`PhysiologicalState::Baseline`], never an assumption about their body.
12
13use std::path::Path;
14
15use wellfare_core::anatomy::PhysiologicalState;
16
17/// Where the person's declared physiological state lives (prefs-style, under the `wellfair/` prefix).
18pub const PHYSIOLOGY_STATE_FILE: &str = "wellfair/physiology_state.json";
19
20/// Load the person's **declared** physiological state, or `None` if they have not declared one (→ the
21/// caller uses [`PhysiologicalState::Baseline`]).
22pub fn load(storage_root: impl AsRef<Path>) -> Option<PhysiologicalState> {
23    let path = storage_root.as_ref().join(PHYSIOLOGY_STATE_FILE);
24    let bytes = std::fs::read(path).ok()?;
25    serde_json::from_slice(&bytes).ok()
26}
27
28/// Persist the person's declared physiological state — their own statement of where they are on the
29/// reproductive continuum. Forum-internum / Sanctuary-class.
30pub fn save(storage_root: impl AsRef<Path>, state: &PhysiologicalState) -> Result<(), String> {
31    let path = storage_root.as_ref().join(PHYSIOLOGY_STATE_FILE);
32    if let Some(parent) = path.parent() {
33        std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
34    }
35    let bytes = serde_json::to_vec_pretty(state).map_err(|e| e.to_string())?;
36    std::fs::write(path, bytes).map_err(|e| e.to_string())
37}
38
39/// Clear the person's declared state — revert to the implicit [`PhysiologicalState::Baseline`]. Idempotent
40/// (no-op if none exists).
41pub fn clear(storage_root: impl AsRef<Path>) -> Result<(), String> {
42    let path = storage_root.as_ref().join(PHYSIOLOGY_STATE_FILE);
43    match std::fs::remove_file(path) {
44        Ok(()) => Ok(()),
45        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
46        Err(e) => Err(e.to_string()),
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use wellfare_core::anatomy::{CyclePhase, PhysiologicalState, ReproductiveState, Trimester};
54
55    #[test]
56    fn declared_state_persists_and_clears_back_to_none() {
57        let dir = tempfile::tempdir().unwrap();
58        // Nothing declared yet → None (caller uses Baseline).
59        assert!(load(dir.path()).is_none());
60
61        // The person declares they're in the third trimester.
62        let state = PhysiologicalState::Reproductive(ReproductiveState::Pregnant(Trimester::Third));
63        save(dir.path(), &state).unwrap();
64        assert_eq!(
65            load(dir.path()),
66            Some(state),
67            "the person's state is theirs, persisted"
68        );
69
70        // They can clear it back to the implicit baseline.
71        clear(dir.path()).unwrap();
72        assert!(
73            load(dir.path()).is_none(),
74            "cleared returns to the implicit baseline"
75        );
76    }
77
78    #[test]
79    fn every_continuum_state_round_trips_through_serde() {
80        let states = vec![
81            PhysiologicalState::Baseline,
82            PhysiologicalState::Reproductive(ReproductiveState::PreMenarche),
83            PhysiologicalState::Reproductive(ReproductiveState::Cycling(CyclePhase::Menstrual)),
84            PhysiologicalState::Reproductive(ReproductiveState::Cycling(CyclePhase::Follicular)),
85            PhysiologicalState::Reproductive(ReproductiveState::Cycling(CyclePhase::Ovulatory)),
86            PhysiologicalState::Reproductive(ReproductiveState::Cycling(CyclePhase::Luteal)),
87            PhysiologicalState::Reproductive(ReproductiveState::Pregnant(Trimester::First)),
88            PhysiologicalState::Reproductive(ReproductiveState::Pregnant(Trimester::Second)),
89            PhysiologicalState::Reproductive(ReproductiveState::Pregnant(Trimester::Third)),
90            PhysiologicalState::Reproductive(ReproductiveState::Postpartum),
91            PhysiologicalState::Reproductive(ReproductiveState::Lactating),
92            PhysiologicalState::Reproductive(ReproductiveState::Perimenopause),
93            PhysiologicalState::Reproductive(ReproductiveState::PostMenopause),
94        ];
95        let dir = tempfile::tempdir().unwrap();
96        for state in &states {
97            save(dir.path(), state).unwrap();
98            let loaded = load(dir.path()).unwrap();
99            assert_eq!(&loaded, state, "round-trip failed for {:?}", state);
100        }
101    }
102}