1use 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
25const 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub struct UnmappedRecord {
34 pub kind: String,
35 pub label: String,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct AnatomyViewReport {
41 pub view: AnatomyView,
43 pub burdens: Vec<SystemBurden>,
45 pub unmapped: Vec<UnmappedRecord>,
47 pub mapped_count: usize,
49 pub total_records: usize,
51 pub disclosure: String,
53}
54
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61pub struct SystemPercept {
62 pub system_id: String,
63 pub level: WellbeingLevel,
65 pub sigma: f32,
67 pub rgba: [f32; 4],
69 pub frequency_hz: f32,
71}
72
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77pub struct OrganPercept {
78 pub organ_key: String,
80 pub system_id: String,
82 pub percept: SystemPercept,
84}
85
86#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90pub struct OverlayPercept {
91 pub system_id: String,
92 pub percept: SystemPercept,
94 pub host_systems: Vec<String>,
96}
97
98impl AnatomyViewReport {
99 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 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 pub fn overlay_percepts(&self) -> Vec<OverlayPercept> {
146 overlay_percepts_from_burdens(&self.burdens)
147 }
148}
149
150fn 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
167fn 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
179fn 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
194pub 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
202pub 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 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
224pub 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
253pub 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
282pub 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
295const 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
303pub struct WellbeingScorecardReport {
304 pub scorecard: ScoreCard,
306 pub hypotheses: Vec<Hypothesis>,
309 pub forum_internum: bool,
311 pub sensitivity_class: String,
313 pub mapped_count: usize,
315 pub total_records: usize,
316 pub disclosure: String,
317}
318
319pub 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
346pub 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
362pub 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
375pub 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 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 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 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 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 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 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 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 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 assert_eq!(overlays.len(), 2);
545 assert!(overlays
546 .iter()
547 .all(|o| o.system_id == "glymphatic" || o.system_id == "ens"));
548 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 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 assert!(kb.get("cond:hypertension").is_some());
565 assert_eq!(
566 kb.get("cond:hypertension").unwrap().targets[0].system_id,
567 "circulatory"
568 );
569 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 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 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 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 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 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 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 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}