Skip to main content

qualia_client_core/wellfair/
anatomy_view.rs

1//! S4b (host) — turn a person's WellFair records into an [`AnatomyViewReport`] for the Anatomy Qapp.
2//!
3//! Reads the person's condition / medication / diet journal entries, normalizes each to a
4//! [`RecordRef`] (extracting the human label from the entry's `summary` JSON projection), maps them to
5//! factors through the anatomy knowledge base, and returns a lens-shaped view plus the lens-independent
6//! per-system burden (which colours the 3D body in S5) and an honest account of what did **not** map.
7//!
8//! The host knowledge base is: the **bundled condition→system reference** (embedded via `include_str!`
9//! so conditions map regardless of the runtime file layout — offline, no fetch) **plus** the
10//! illustrative seed for food / herb / tea (pending Timothy's curated corpus). The `disclosure` field
11//! says exactly that, so the UI never passes seed data off as authoritative.
12
13use serde::{Deserialize, Serialize};
14
15use wellfare_core::anatomy::{
16    self, body_system_for_organ, burden_to_sigma, overlay_host_systems, system_representation,
17    AnatomyView, Hypothesis, KnowledgeBase, Lens, PhysiologicalState, Provenance, RecordRef,
18    ScoreCard, SystemBurden, SystemRepresentation, WellbeingLevel,
19};
20
21use qualia_core_db::render::{acoustic, spectral};
22
23use super::journal::JournalEntry;
24
25/// The bundled condition→primary-system map, embedded at compile time (offline, layout-independent).
26const BUNDLED_CONDITION_MAP: &str =
27    include_str!("../../../../bundled/qapps/Anatomy/Knowledge/condition-map.json");
28
29const DISCLOSURE: &str = "Conditions map via the bundled condition→system reference. Food, herb, and medication mappings currently use an illustrative seed set pending a curated knowledge corpus — treat those as examples, not authoritative. This is a general picture to explore with a clinician, not a diagnosis.";
30
31/// A record that carried no knowledge mapping — surfaced honestly, never silently dropped.
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub struct UnmappedRecord {
34    pub kind: String,
35    pub label: String,
36}
37
38/// The full report the host returns for one lens.
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct AnatomyViewReport {
41    /// The lens-shaped narrative (person wellbeing gist / clinician considerations).
42    pub view: AnatomyView,
43    /// Per-system burden, lens-independent — drives colour-by-load (S5).
44    pub burdens: Vec<SystemBurden>,
45    /// Records with no knowledge mapping yet.
46    pub unmapped: Vec<UnmappedRecord>,
47    /// How many records resolved to a factor.
48    pub mapped_count: usize,
49    /// How many records were considered in total.
50    pub total_records: usize,
51    /// Honest note on provenance/limits for the UI to show.
52    pub disclosure: String,
53}
54
55/// The dual-modality percept for one body system (S5.1 colour-by-load). The accumulated burden is
56/// encoded **once** to σ — a position on the shared EMF spectrum — and σ then drives *both* the visual
57/// spectrum (`rgba`) and the sonic spectrum (`frequency_hz`) via the engine's parity oracles. So an
58/// organ under strain is redder **and** lower-pitched: the 3D body can be seen and heard from one
59/// source of truth, rather than a hand-picked swatch that would discard the audio path.
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61pub struct SystemPercept {
62    pub system_id: String,
63    /// The coarse person-facing band (never a number to the person).
64    pub level: WellbeingLevel,
65    /// σ — the EMF spectrum position (0..1 over 400–700 nm) this burden encodes to. The one truth.
66    pub sigma: f32,
67    /// Visual encoding: normalized linear RGBA from `render::spectral`, ready for `upload_mesh_colored`.
68    pub rgba: [f32; 4],
69    /// Sonic encoding: centre frequency (Hz) from `render::acoustic` — settled/green higher, strain/red lower.
70    pub frequency_hz: f32,
71}
72
73/// One organ mesh's resolved paint: which body system it belongs to, and the σ-derived dual-modality
74/// [`SystemPercept`] (colour + pitch) for that system's current burden. This is what the renderer uses
75/// to colour (and can sonify) each organ of the 3D body.
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77pub struct OrganPercept {
78    /// The organ key as supplied (e.g. a CCF asset name / file path).
79    pub organ_key: String,
80    /// The body system this organ belongs to (one of the 17).
81    pub system_id: String,
82    /// The system's dual-modality percept — settled baseline if the system carries no recorded burden.
83    pub percept: SystemPercept,
84}
85
86/// A distributed-overlay system's paint (ECS / ENS / glymphatic) — a system with no standalone organ
87/// mesh, rendered as a highlight over its host structures. Carries the same σ percept as any system
88/// plus the host-system hints for where to place the overlay (empty = a whole-body cue).
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90pub struct OverlayPercept {
91    pub system_id: String,
92    /// The σ-derived colour + pitch for this network's burden.
93    pub percept: SystemPercept,
94    /// Discrete systems to highlight this overlay over (empty = whole-body).
95    pub host_systems: Vec<String>,
96}
97
98impl AnatomyViewReport {
99    /// The per-system dual-modality percepts. For each accumulated burden, encode `net_milli` → σ once
100    /// (`burden_to_sigma`), then derive the visual colour (`render::spectral`) and the sonic pitch
101    /// (`render::acoustic`) from that single σ — the modality-first parity that lets the same anatomy
102    /// state be rendered to sight or sound without re-deciding what it "means".
103    pub fn system_percepts(&self) -> Vec<SystemPercept> {
104        self.burdens
105            .iter()
106            .map(|b| system_percept(&b.system_id, b.net_milli))
107            .collect()
108    }
109
110    /// Resolve the dual-modality paint for a set of organ meshes — the organs of the selected anatomy
111    /// model (chosen from the user's XY/XX basis via `Karyotype::anatomy_model`; the model's file set is
112    /// supplied by the loader). Each organ's body system is looked up (`body_system_for_organ`), then
113    /// that system's percept. An organ on a system with **no** recorded burden gets the settled baseline
114    /// (calm green / higher pitch) so the whole body still renders; an organ **not** in the curated
115    /// organ→system map is returned in the second list — reported, never silently coloured.
116    pub fn paint_organs(&self, organ_keys: &[&str]) -> (Vec<OrganPercept>, Vec<String>) {
117        let percepts = self.system_percepts();
118        let mut painted = Vec::new();
119        let mut unmapped = Vec::new();
120        for &organ in organ_keys {
121            match body_system_for_organ(organ) {
122                Some(system_id) => {
123                    let percept = percepts
124                        .iter()
125                        .find(|p| p.system_id == system_id)
126                        .cloned()
127                        .unwrap_or_else(|| system_percept(system_id, 0));
128                    painted.push(OrganPercept {
129                        organ_key: organ.to_string(),
130                        system_id: system_id.to_string(),
131                        percept,
132                    });
133                }
134                None => unmapped.push(organ.to_string()),
135            }
136        }
137        (painted, unmapped)
138    }
139
140    /// The distributed-overlay systems' percepts (ECS / ENS / glymphatic) — the systems that have no
141    /// standalone organ mesh and so are omitted by [`paint_organs`]. Each is rendered as a highlight
142    /// over its host structures (see `host_systems`; empty = a whole-body cue). Together,
143    /// `paint_organs` (discrete organs) + `overlay_percepts` (distributed networks) cover the whole
144    /// body state — so nothing that carries burden is silently unrepresented.
145    pub fn overlay_percepts(&self) -> Vec<OverlayPercept> {
146        overlay_percepts_from_burdens(&self.burdens)
147    }
148}
149
150/// The overlay percepts for the distributed-network systems present in a burden set. Split from the
151/// method so it is testable with synthetic burdens.
152fn overlay_percepts_from_burdens(burdens: &[SystemBurden]) -> Vec<OverlayPercept> {
153    burdens
154        .iter()
155        .filter(|b| system_representation(&b.system_id) == SystemRepresentation::DistributedOverlay)
156        .map(|b| OverlayPercept {
157            host_systems: overlay_host_systems(&b.system_id)
158                .iter()
159                .map(|s| s.to_string())
160                .collect(),
161            percept: system_percept(&b.system_id, b.net_milli),
162            system_id: b.system_id.clone(),
163        })
164        .collect()
165}
166
167/// One system's percept from its burden — the shared σ → {colour, pitch} step.
168fn system_percept(system_id: &str, net_milli: u32) -> SystemPercept {
169    let sigma = burden_to_sigma(net_milli);
170    SystemPercept {
171        system_id: system_id.to_string(),
172        level: WellbeingLevel::from_net(net_milli),
173        sigma,
174        rgba: sigma_to_normalized_linear_rgba(sigma),
175        frequency_hz: acoustic::sigma_to_center_frequency_hz(sigma),
176    }
177}
178
179/// σ → normalized linear RGBA for the GPU mesh path. `render::spectral::sigma_to_linear_rgb` returns
180/// raw linear sRGB whose luminance varies by hue; we normalize by the peak channel (the same move the
181/// display oracle makes before gamma) so every hue reads at full strength as a categorical heat cue,
182/// then pin alpha opaque. Linear (not sRGB) because `upload_mesh_colored` expects linear vertex colour.
183fn sigma_to_normalized_linear_rgba(sigma: f32) -> [f32; 4] {
184    let lin = spectral::sigma_to_linear_rgb(sigma);
185    let scale = 1.0 / lin.iter().copied().fold(0.0_f32, f32::max).max(1e-6);
186    [
187        (lin[0] * scale).clamp(0.0, 1.0),
188        (lin[1] * scale).clamp(0.0, 1.0),
189        (lin[2] * scale).clamp(0.0, 1.0),
190        1.0,
191    ]
192}
193
194/// Parse a lens string (`"clinician"` → clinician; anything else → the safe person default).
195pub fn parse_lens(s: &str) -> Lens {
196    match s.trim().to_ascii_lowercase().as_str() {
197        "clinician" => Lens::Clinician,
198        _ => Lens::Person,
199    }
200}
201
202/// Build the host knowledge base: bundled conditions + the illustrative seed.
203pub fn host_knowledge_base() -> KnowledgeBase {
204    let mut kb = anatomy::seed_knowledge_base();
205    let prov = Provenance {
206        source_id: "clinical-reference".to_string(),
207        source_title: "Bundled condition→system reference map".to_string(),
208        citation: None,
209        imported_at: None,
210    };
211    // Resolve condition→system through the default (seeded 17) registry. When the app carries an
212    // extended taxonomy (ontology/pack-registered systems), pass that registry here instead so a
213    // condition mapping to an extension system is evaluated, not dropped.
214    if let Ok(res) =
215        anatomy::import_condition_map(BUNDLED_CONDITION_MAP, prov, anatomy::default_registry())
216    {
217        for entry in res.entries {
218            kb.insert(entry);
219        }
220    }
221    kb
222}
223
224/// Normalize condition / medication / diet journal entries into [`RecordRef`]s (ceased medications are
225/// skipped — they are not a current factor).
226pub fn record_refs_from_journal(
227    conditions: &[JournalEntry],
228    medications: &[JournalEntry],
229    diet: &[JournalEntry],
230) -> Vec<RecordRef> {
231    let mut refs = Vec::new();
232    for e in conditions {
233        if let Some(label) = summary_str(e, "label") {
234            refs.push(RecordRef::new(e.id.clone(), "condition", label));
235        }
236    }
237    for e in medications {
238        if summary_bool(e, "ceased") == Some(true) {
239            continue;
240        }
241        if let Some(name) = summary_str(e, "name") {
242            refs.push(RecordRef::new(e.id.clone(), "medication", name));
243        }
244    }
245    for e in diet {
246        if let Some(desc) = summary_str(e, "description") {
247            refs.push(RecordRef::new(e.id.clone(), "diet", desc));
248        }
249    }
250    refs
251}
252
253/// Build a report from already-normalized record refs and a lens, at a physiological state. The state
254/// modulator is applied to the per-system burdens so the colour-by-load reflects the person's current life
255/// stage (e.g. a nephrotoxic med is a bigger ask on the kidneys in the third trimester).
256pub fn build_report(
257    records: Vec<RecordRef>,
258    lens: Lens,
259    convergence_threshold: usize,
260    state: PhysiologicalState,
261) -> AnatomyViewReport {
262    let total_records = records.len();
263    let kb = host_knowledge_base();
264    let bridge = anatomy::records_to_factors(&records, &kb);
265    let raw_burdens = anatomy::accumulate(&bridge.factors);
266    let burdens = anatomy::state_modulator(state).apply_to_burdens(&raw_burdens);
267    let view = anatomy::build_view(&bridge.factors, lens, convergence_threshold);
268    AnatomyViewReport {
269        view,
270        burdens,
271        unmapped: bridge
272            .unmapped
273            .into_iter()
274            .map(|(kind, label)| UnmappedRecord { kind, label })
275            .collect(),
276        mapped_count: bridge.factors.len(),
277        total_records,
278        disclosure: DISCLOSURE.to_string(),
279    }
280}
281
282/// One-shot: journal entries → report for a lens, at a physiological state.
283pub fn build_report_from_journal(
284    conditions: &[JournalEntry],
285    medications: &[JournalEntry],
286    diet: &[JournalEntry],
287    lens: Lens,
288    convergence_threshold: usize,
289    state: PhysiologicalState,
290) -> AnatomyViewReport {
291    let refs = record_refs_from_journal(conditions, medications, diet);
292    build_report(refs, lens, convergence_threshold, state)
293}
294
295/// Honest note for the score-card surface: it is the person's inward, forum-internum self-assessment — a
296/// discussion aid + pathway start, never a diagnosis, never a rating.
297const SCORECARD_DISCLOSURE: &str = "This score-card is your own inward reading (forum-internum, Sanctuary-class) — a set of Hypotheses to explore, not a diagnosis and not a rating. Each aspect links to the underlying considerations, and each is a starting point toward knowing more (questions to ask, what could be tracked or tested, levers you control, when a clinician you choose could help).";
298
299/// The accumulative, **traceable** score-card + the investigable hypotheses it surfaces — the reading a
300/// person can act on. Companion to [`AnatomyViewReport`]; **forum-internum, `Sanctuary`-class** selfhood
301/// content (the storage/consent layer must honour that).
302#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
303pub struct WellbeingScorecardReport {
304    /// The score-card across health-relevant aspects, each score linked to its contributions.
305    pub scorecard: ScoreCard,
306    /// The investigable hypotheses (from converging systemic implications) — the seeds of the investigative
307    /// pathway (the step library that ranks next-steps by value-of-information is curation-grade, pending).
308    pub hypotheses: Vec<Hypothesis>,
309    /// Classification the storage/consent layer must honour: forum-internum (the person's inward domain).
310    pub forum_internum: bool,
311    /// The sensitivity class to store/handle this under (always `"Sanctuary"`, the most restrictive).
312    pub sensitivity_class: String,
313    /// How many records resolved to a factor / were considered.
314    pub mapped_count: usize,
315    pub total_records: usize,
316    pub disclosure: String,
317}
318
319/// Compute the score-card + investigable-hypotheses surface from records, using the **person's own weight
320/// model** — *their* authorship of how their body is read, not a lens the software imposes. The seed model is
321/// only a starting suggestion the person can edit or replace (see [`build_scorecard_report`]).
322pub fn build_scorecard_report_with_weights(
323    records: Vec<RecordRef>,
324    convergence_threshold: usize,
325    weight_model: &anatomy::WeightModel,
326    state: PhysiologicalState,
327) -> WellbeingScorecardReport {
328    let total_records = records.len();
329    let kb = host_knowledge_base();
330    let bridge = anatomy::records_to_factors(&records, &kb);
331    let scorecard =
332        anatomy::score_card(&bridge.factors, convergence_threshold, state, weight_model);
333    let implications = anatomy::systemic_implications(&bridge.factors, convergence_threshold);
334    let hypotheses = anatomy::hypotheses_from_implications(&implications);
335    WellbeingScorecardReport {
336        forum_internum: scorecard.forum_class() == anatomy::ForumClass::Internum,
337        sensitivity_class: scorecard.sensitivity_class().to_string(),
338        scorecard,
339        hypotheses,
340        mapped_count: bridge.factors.len(),
341        total_records,
342        disclosure: SCORECARD_DISCLOSURE.to_string(),
343    }
344}
345
346/// Compute the score-card with the **seed** weight model — the *suggested* starting interpretation, used when
347/// the person has not (yet) authored their own. The physiological state is passed through so the card reads
348/// the person at their current life stage.
349pub fn build_scorecard_report(
350    records: Vec<RecordRef>,
351    convergence_threshold: usize,
352    state: PhysiologicalState,
353) -> WellbeingScorecardReport {
354    build_scorecard_report_with_weights(
355        records,
356        convergence_threshold,
357        &anatomy::seed_weight_model(),
358        state,
359    )
360}
361
362/// One-shot: journal entries → score-card report, with the person's own weight model and declared state.
363pub fn build_scorecard_report_from_journal_with_weights(
364    conditions: &[JournalEntry],
365    medications: &[JournalEntry],
366    diet: &[JournalEntry],
367    convergence_threshold: usize,
368    weight_model: &anatomy::WeightModel,
369    state: PhysiologicalState,
370) -> WellbeingScorecardReport {
371    let refs = record_refs_from_journal(conditions, medications, diet);
372    build_scorecard_report_with_weights(refs, convergence_threshold, weight_model, state)
373}
374
375/// One-shot with the seed (suggested) weights.
376pub fn build_scorecard_report_from_journal(
377    conditions: &[JournalEntry],
378    medications: &[JournalEntry],
379    diet: &[JournalEntry],
380    convergence_threshold: usize,
381    state: PhysiologicalState,
382) -> WellbeingScorecardReport {
383    let refs = record_refs_from_journal(conditions, medications, diet);
384    build_scorecard_report(refs, convergence_threshold, state)
385}
386
387fn summary_value(entry: &JournalEntry) -> Option<serde_json::Value> {
388    serde_json::from_str(entry.summary.as_ref()?).ok()
389}
390
391fn summary_str(entry: &JournalEntry, field: &str) -> Option<String> {
392    summary_value(entry)?
393        .get(field)?
394        .as_str()
395        .map(|s| s.to_string())
396}
397
398fn summary_bool(entry: &JournalEntry, field: &str) -> Option<bool> {
399    summary_value(entry)?.get(field)?.as_bool()
400}
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405    use wellfare_core::anatomy::{Aspect, ReproductiveState, Trimester};
406
407    fn je(id: &str, kind: &str, summary: serde_json::Value) -> JournalEntry {
408        JournalEntry {
409            id: id.to_string(),
410            kind: kind.to_string(),
411            asserted_time_unix: 0,
412            evidence_type: "SelfReported".to_string(),
413            sensitivity: "Restricted".to_string(),
414            blob_hash: None,
415            source: "test".to_string(),
416            committed_unix: 0,
417            summary: Some(summary.to_string()),
418        }
419    }
420
421    #[test]
422    fn percept_parity_strain_is_redder_and_lower_pitched() {
423        // Two burdens through the one σ encoding, then both modalities derived from it.
424        let settled_sigma = burden_to_sigma(0);
425        let strained_sigma = burden_to_sigma(1000);
426        let settled = sigma_to_normalized_linear_rgba(settled_sigma);
427        let strained = sigma_to_normalized_linear_rgba(strained_sigma);
428        // Visual: strain is red-dominant, settled is green-leaning — the heat cue is spectral, not hex.
429        assert!(
430            strained[0] > strained[1] && strained[0] > strained[2],
431            "strain rgba={strained:?}"
432        );
433        assert!(settled[1] >= settled[0], "settled rgba={settled:?}");
434        assert_eq!(strained[3], 1.0, "opaque");
435        // Sonic parity from the SAME σ: red/strain folds to a lower pitch than green/settled.
436        let f_settled = acoustic::sigma_to_center_frequency_hz(settled_sigma);
437        let f_strained = acoustic::sigma_to_center_frequency_hz(strained_sigma);
438        assert!(
439            f_strained < f_settled,
440            "strain {f_strained}Hz should be below settled {f_settled}Hz"
441        );
442    }
443
444    #[test]
445    fn system_percepts_cover_every_burden_and_stay_in_the_emf_band() {
446        let conditions = vec![je(
447            "did:wf:me:condition:1",
448            "condition",
449            serde_json::json!({"label": "Hypertension"}),
450        )];
451        let report = build_report_from_journal(
452            &conditions,
453            &[],
454            &[],
455            Lens::Person,
456            2,
457            PhysiologicalState::Baseline,
458        );
459        let percepts = report.system_percepts();
460        // One percept per accumulated burden — nothing silently dropped.
461        assert_eq!(percepts.len(), report.burdens.len());
462        for p in &percepts {
463            assert!(
464                p.sigma >= 0.50 - 1e-6 && p.sigma <= 0.93 + 1e-6,
465                "σ in EMF band: {}",
466                p.sigma
467            );
468            assert_eq!(p.rgba[3], 1.0);
469            assert!(p.frequency_hz > 0.0);
470        }
471        // The hypertension load lands on the circulatory system.
472        assert!(percepts.iter().any(|p| p.system_id == "circulatory"));
473    }
474
475    #[test]
476    fn paint_organs_colours_by_system_and_reports_unknown_organs() {
477        let conditions = vec![je(
478            "did:wf:me:condition:1",
479            "condition",
480            serde_json::json!({"label": "Hypertension"}),
481        )];
482        let report = build_report_from_journal(
483            &conditions,
484            &[],
485            &[],
486            Lens::Person,
487            2,
488            PhysiologicalState::Baseline,
489        );
490        // A VH_Male organ set: a burdened organ (blood-vasculature → circulatory), an unburdened one
491        // (lung → respiratory), and one not in the curated map.
492        let (painted, unmapped) = report.paint_organs(&[
493            "3d-vh-m-blood-vasculature.glb",
494            "3d-vh-m-lung.glb",
495            "3d-vh-m-flux-capacitor.glb",
496        ]);
497        assert_eq!(painted.len(), 2);
498        assert_eq!(unmapped, vec!["3d-vh-m-flux-capacitor.glb".to_string()]);
499
500        let circ = painted
501            .iter()
502            .find(|o| o.system_id == "circulatory")
503            .unwrap();
504        let resp = painted
505            .iter()
506            .find(|o| o.system_id == "respiratory")
507            .unwrap();
508        // The hypertension load makes circulatory redder (higher σ) than the settled respiratory organ.
509        assert!(circ.percept.sigma >= resp.percept.sigma);
510        assert_eq!(
511            resp.percept.level,
512            WellbeingLevel::Settled,
513            "no respiratory load → settled baseline"
514        );
515        // Every painted organ has an opaque colour and an audible pitch (both encodings present).
516        for o in &painted {
517            assert_eq!(o.percept.rgba[3], 1.0);
518            assert!(o.percept.frequency_hz > 0.0);
519        }
520    }
521
522    #[test]
523    fn overlay_percepts_surface_only_distributed_networks_with_host_hints() {
524        let burdens = vec![
525            SystemBurden {
526                system_id: "glymphatic".to_string(),
527                net_milli: 400,
528                ..Default::default()
529            },
530            SystemBurden {
531                system_id: "circulatory".to_string(),
532                net_milli: 200,
533                ..Default::default()
534            },
535            SystemBurden {
536                system_id: "ens".to_string(),
537                net_milli: 150,
538                ..Default::default()
539            },
540        ];
541        let overlays = overlay_percepts_from_burdens(&burdens);
542        // Only the distributed networks appear — the discrete circulatory system is excluded (it
543        // paints its own organ mesh via paint_organs instead).
544        assert_eq!(overlays.len(), 2);
545        assert!(overlays
546            .iter()
547            .all(|o| o.system_id == "glymphatic" || o.system_id == "ens"));
548        // Anatomical host hints for overlay placement.
549        let ens = overlays.iter().find(|o| o.system_id == "ens").unwrap();
550        assert_eq!(ens.host_systems, vec!["digestive".to_string()]);
551        let gly = overlays
552            .iter()
553            .find(|o| o.system_id == "glymphatic")
554            .unwrap();
555        assert_eq!(gly.host_systems, vec!["nervous".to_string()]);
556        // Still a real σ percept — burdened → redder than settled and audible.
557        assert!(gly.percept.sigma > 0.5 && gly.percept.frequency_hz > 0.0);
558    }
559
560    #[test]
561    fn bundled_condition_map_is_embedded_and_parses() {
562        let kb = host_knowledge_base();
563        // A well-known bundled condition resolves to its primary system.
564        assert!(kb.get("cond:hypertension").is_some());
565        assert_eq!(
566            kb.get("cond:hypertension").unwrap().targets[0].system_id,
567            "circulatory"
568        );
569        // Integrity holds across the whole assembled base.
570        assert!(kb.verify_integrity().is_empty());
571    }
572
573    #[test]
574    fn real_conditions_map_and_unknown_records_are_reported() {
575        let conditions = vec![
576            je(
577                "did:wf:me:condition:1",
578                "condition",
579                serde_json::json!({"label": "Hypertension"}),
580            ),
581            je(
582                "did:wf:me:condition:2",
583                "condition",
584                serde_json::json!({"label": "Made-Up Disease"}),
585            ),
586        ];
587        let meds = vec![je(
588            "did:wf:me:medication:1",
589            "medication",
590            serde_json::json!({"name": "Warfarin", "ceased": false}),
591        )];
592        let diet = vec![je(
593            "did:wf:me:diet:1",
594            "diet",
595            serde_json::json!({"description": "Beer", "meal_type": "drink"}),
596        )];
597
598        let report = build_report_from_journal(
599            &conditions,
600            &meds,
601            &diet,
602            Lens::Person,
603            1,
604            PhysiologicalState::Baseline,
605        );
606        // Hypertension → circulatory; Beer → digestive+urinary (seed). Made-Up + Warfarin(no seed) unmapped.
607        assert!(report.burdens.iter().any(|b| b.system_id == "circulatory"));
608        assert!(report.burdens.iter().any(|b| b.system_id == "digestive"));
609        assert!(report.unmapped.iter().any(|u| u.label == "Made-Up Disease"));
610        assert!(report.unmapped.iter().any(|u| u.label == "Warfarin"));
611        assert_eq!(report.total_records, 4);
612        assert!(report.disclosure.contains("illustrative seed"));
613        // Person view carries the "not a diagnosis / not advice" boundary.
614        assert!(report.view.boundary.contains("not medical advice"));
615    }
616
617    #[test]
618    fn ceased_medications_are_skipped() {
619        let meds = vec![je(
620            "did:wf:me:medication:old",
621            "medication",
622            serde_json::json!({"name": "Warfarin", "ceased": true}),
623        )];
624        let refs = record_refs_from_journal(&[], &meds, &[]);
625        assert!(
626            refs.is_empty(),
627            "a ceased medication is not a current factor"
628        );
629    }
630
631    #[test]
632    fn clinician_lens_flags_the_herb_drug_style_convergence() {
633        // Two conditions on the circulatory system converge at threshold 2 → a clinician flag.
634        let conditions = vec![
635            je(
636                "c1",
637                "condition",
638                serde_json::json!({"label": "Hypertension"}),
639            ),
640            je(
641                "c2",
642                "condition",
643                serde_json::json!({"label": "Atrial Fibrillation"}),
644            ),
645        ];
646        let report = build_report_from_journal(
647            &conditions,
648            &[],
649            &[],
650            Lens::Clinician,
651            2,
652            PhysiologicalState::Baseline,
653        );
654        assert!(report
655            .view
656            .systems
657            .iter()
658            .any(|s| s.system_id == "circulatory"));
659        assert!(report.view.boundary.contains("not a diagnosis"));
660    }
661
662    #[test]
663    fn report_serde_round_trips() {
664        let report = build_report(vec![], Lens::Person, 2, PhysiologicalState::Baseline);
665        let json = serde_json::to_string(&report).unwrap();
666        let back: AnatomyViewReport = serde_json::from_str(&json).unwrap();
667        assert_eq!(report, back);
668    }
669
670    #[test]
671    fn scorecard_at_third_trimester_scales_adverse_load_higher_than_baseline() {
672        // Use a condition that maps to urinary (renal) in the seed KB — a known adverse renal load.
673        let refs = vec![RecordRef::new(
674            "r:renal-load",
675            "condition",
676            "Chronic Kidney Disease",
677        )];
678        let baseline_report = build_scorecard_report(refs.clone(), 1, PhysiologicalState::Baseline);
679        let preg_report = build_scorecard_report(
680            refs,
681            1,
682            PhysiologicalState::Reproductive(ReproductiveState::Pregnant(Trimester::Third)),
683        );
684        // The systemic-load aspect is computed over the state-modulated burdens. In the third trimester,
685        // the renal (urinary) engagement is 130% → the same adverse load lands harder.
686        let baseline_load = baseline_report
687            .scorecard
688            .aspect(Aspect::SystemicLoad)
689            .unwrap()
690            .score_milli;
691        let preg_load = preg_report
692            .scorecard
693            .aspect(Aspect::SystemicLoad)
694            .unwrap()
695            .score_milli;
696        assert!(
697            preg_load >= baseline_load,
698            "third-trimester renal engagement scales the load up or equal: {preg_load} vs {baseline_load}"
699        );
700        // The physiological-demand aspect is non-zero in pregnancy (whole-body engagement) and zero at baseline.
701        let baseline_demand = baseline_report
702            .scorecard
703            .aspect(Aspect::PhysiologicalDemand)
704            .unwrap()
705            .score_milli;
706        let preg_demand = preg_report
707            .scorecard
708            .aspect(Aspect::PhysiologicalDemand)
709            .unwrap()
710            .score_milli;
711        assert_eq!(baseline_demand, 0, "baseline has no physiological demand");
712        assert!(
713            preg_demand > 0,
714            "pregnancy engages the whole body → demand > 0"
715        );
716    }
717
718    #[test]
719    fn anatomy_view_at_third_trimester_scales_circulatory_burden() {
720        // Hypertension → circulatory burden. In the third trimester, circulatory engagement is 140%.
721        let conditions = vec![je(
722            "did:wf:me:condition:1",
723            "condition",
724            serde_json::json!({"label": "Hypertension"}),
725        )];
726        let baseline_report = build_report_from_journal(
727            &conditions,
728            &[],
729            &[],
730            Lens::Person,
731            2,
732            PhysiologicalState::Baseline,
733        );
734        let preg_report = build_report_from_journal(
735            &conditions,
736            &[],
737            &[],
738            Lens::Person,
739            2,
740            PhysiologicalState::Reproductive(ReproductiveState::Pregnant(Trimester::Third)),
741        );
742        let baseline_circ = baseline_report
743            .burdens
744            .iter()
745            .find(|b| b.system_id == "circulatory")
746            .unwrap()
747            .net_milli;
748        let preg_circ = preg_report
749            .burdens
750            .iter()
751            .find(|b| b.system_id == "circulatory")
752            .unwrap()
753            .net_milli;
754        assert!(
755            preg_circ > baseline_circ,
756            "third-trimester circulatory engagement (140%) scales hypertension higher: {preg_circ} > {baseline_circ}"
757        );
758    }
759}