Skip to main content

qualia_client_core/wellfair/
anatomy_body.rs

1//! Compile a model's organ meshes into the `.10d` body — the ingestion entry point (S5.4).
2//!
3//! Given a set of `(organ_key, mesh_bytes)` for an [`AnatomyModel`], resolve each organ's body system
4//! and compile it to a sealed `.10d` whose q42 manifest carries `geo:bodySystem` + `geo:anatomyModel`.
5//! Organs with no system mapping, or bytes that fail to parse, are **reported** — never silently
6//! dropped. This layer owns the *compile*; the mesh **bytes** are supplied by the caller (the desktop
7//! `glb_ingest` loads them from the CCF/HRA set chosen by `AnatomyModel::asset_set()`), so the file I/O
8//! and the compile stay in their own lanes.
9
10use std::collections::HashMap;
11
12use qualia_core_db::container_10d::ProvenanceSidecar;
13use qualia_core_db::hypermedia::{
14    container_to_nquins, descriptors_to_nquins, AssetRef, AssetRole, Descriptors,
15    HypermediaContainer,
16};
17use qualia_core_db::render::compile_10d::{compile_organ_asset, CompiledAsset};
18use qualia_core_db::NQuin;
19use wellfare_core::anatomy::{body_system_for_organ, AnatomyModel};
20
21/// One organ compiled into the body: its resolved system and the sealed `.10d` asset.
22pub struct CompiledOrgan {
23    pub organ_key: String,
24    pub system_id: String,
25    pub asset: CompiledAsset,
26}
27
28/// The outcome of compiling a model's organ set — honest about what did and did not compile.
29pub struct BodyCompileResult {
30    pub model: AnatomyModel,
31    pub organs: Vec<CompiledOrgan>,
32    /// Organs with no body-system mapping (reported, not guessed onto a system).
33    pub unmapped: Vec<String>,
34    /// Organs whose bytes failed to import/compile, with the error text.
35    pub failed: Vec<(String, String)>,
36}
37
38impl BodyCompileResult {
39    /// How many organs compiled into the body.
40    pub fn compiled_count(&self) -> usize {
41        self.organs.len()
42    }
43}
44
45/// The source-format hint for an organ key, from its extension (default `glb` — the CCF asset format).
46fn format_of(organ_key: &str) -> &'static str {
47    match organ_key
48        .rsplit('.')
49        .next()
50        .unwrap_or("")
51        .to_ascii_lowercase()
52        .as_str()
53    {
54        "obj" => "obj",
55        "stl" => "stl",
56        "gltf" => "gltf",
57        _ => "glb",
58    }
59}
60
61/// Compile a model's organ meshes into its `.10d` body asset set.
62pub fn compile_body(model: AnatomyModel, organs: &[(String, Vec<u8>)]) -> BodyCompileResult {
63    let mut compiled = Vec::new();
64    let mut unmapped = Vec::new();
65    let mut failed = Vec::new();
66    for (organ_key, bytes) in organs {
67        let Some(system_id) = body_system_for_organ(organ_key) else {
68            unmapped.push(organ_key.clone());
69            continue;
70        };
71        let fmt = format_of(organ_key);
72        let uri = format!("urn:qualia:anatomy:{}:{organ_key}", model.as_str());
73        // Attest each organ with its HRA/CCF provenance (CC-BY-4.0) so the sealed `.10d` is citable and
74        // passes the renderer's fail-closed governance gate. A compact source reference identifies the CCF
75        // asset without embedding the multi-MB GLB into the container.
76        let provenance = ProvenanceSidecar::new(
77            format!("urn:hra:ccf:{organ_key}").into_bytes(),
78            "model/gltf-binary",
79            "CC-BY-4.0",
80        );
81        match compile_organ_asset(
82            bytes,
83            Some(fmt),
84            &uri,
85            fmt,
86            Some(system_id),
87            Some(model.as_str()),
88            Some(&provenance),
89        ) {
90            Ok(asset) => compiled.push(CompiledOrgan {
91                organ_key: organ_key.clone(),
92                system_id: system_id.to_string(),
93                asset,
94            }),
95            Err(e) => failed.push((organ_key.clone(), e.to_string())),
96        }
97    }
98    BodyCompileResult {
99        model,
100        organs: compiled,
101        unmapped,
102        failed,
103    }
104}
105
106// ── An organ / body as a HYPERMEDIA CONTAINER (not a bare .10d file) ─────────────────────────────
107//
108// P2 of the hypermedia semantic library: an anatomy asset becomes a semantic container — the sealed `.10d`
109// mesh (primary) ⊕ its source GLB (with CCF/HRA provenance + licence) ⊕ topic/depiction/system descriptors —
110// so it is *findable by meaning* (search by "respiratory", by the organ it depicts, by "3d-anatomy-model")
111// and carries its lineage, not a folder path. The container's primary subject shares the organ's own
112// geometry-manifest subject (one identity space), so the edges join to the mesh facts.
113
114/// An organ made real as a hypermedia container: the container model + its full semantic quin set (the
115/// organ's geometry manifest ⊕ the hypermedia edges ⊕ the descriptors) + the lexicon.
116pub struct OrganContainer {
117    pub container: HypermediaContainer,
118    pub quins: Vec<NQuin>,
119    pub lexicon: HashMap<u64, String>,
120}
121
122/// The stable URI a compiled organ is known by (matches `compile_body`'s compile URI, so subjects join).
123fn organ_uri(model: AnatomyModel, organ_key: &str) -> String {
124    format!("urn:qualia:anatomy:{}:{organ_key}", model.as_str())
125}
126
127/// Turn a [`CompiledOrgan`] into a [`HypermediaContainer`]. `source_url` is the CCF/HRA CDN URL the GLB was
128/// fetched from — recorded as the immutable source with its licence/creator; pass `None` if unknown.
129pub fn organ_container(
130    organ: &CompiledOrgan,
131    model: AnatomyModel,
132    source_url: Option<&str>,
133) -> OrganContainer {
134    let ouri = organ_uri(model, &organ.organ_key);
135    let source_uri = source_url
136        .map(str::to_string)
137        .unwrap_or_else(|| format!("urn:hra:ccf:{}", organ.organ_key));
138
139    let primary = AssetRef::new(
140        &ouri,
141        organ.asset.compiled_digest as u64,
142        "model/qualia-10d",
143        AssetRole::Primary,
144    )
145    .derived_from(&source_uri);
146    let mut source = AssetRef::new(
147        &source_uri,
148        organ.asset.source_digest as u64,
149        "model/gltf-binary",
150        AssetRole::Source,
151    )
152    .with_licence("CC-BY-4.0");
153    source.creator = Some("Human Reference Atlas (CCF), lod.humanatlas.io".into());
154    let provenance = AssetRef::new(
155        format!("{ouri}#provenance"),
156        0,
157        "application/ld+json",
158        AssetRole::Provenance,
159    );
160
161    let container = HypermediaContainer::new(
162        format!("urn:qualia:container:{}", organ.organ_key),
163        primary.clone(),
164    )
165    .with_related(source)
166    .with_related(provenance);
167
168    let (mut quins, mut lexicon) = container_to_nquins(&container);
169    // Join the organ's own geometry manifest (same identity space) so the container edges connect to the facts.
170    quins.extend(organ.asset.quins.iter().cloned());
171    for (k, v) in &organ.asset.lexicon {
172        lexicon.entry(*k).or_insert_with(|| v.clone());
173    }
174
175    // Descriptors that make the organ findable by meaning.
176    let label = organ
177        .organ_key
178        .rsplit('/')
179        .next()
180        .unwrap_or(&organ.organ_key)
181        .trim_end_matches(".glb")
182        .trim_end_matches(".obj")
183        .trim_end_matches(".gltf")
184        .to_string();
185    let descriptors = Descriptors {
186        topics: vec!["anatomy".into(), "biology".into(), organ.system_id.clone()],
187        depicts: vec![label],
188        document_type: Some("3d-anatomy-model".into()),
189        ..Default::default()
190    };
191    let (dq, dl) = descriptors_to_nquins(primary.subject(), &descriptors);
192    quins.extend(dq);
193    for (k, v) in dl {
194        lexicon.entry(k).or_insert(v);
195    }
196
197    OrganContainer {
198        container,
199        quins,
200        lexicon,
201    }
202}
203
204/// Bundle a model's organ containers into a single **body** container — one addressable unit whose members
205/// are the organs (so the body is a semantic bundle, not a folder of files). Returns the body container + the
206/// combined quin graph (every organ container's quins + the body's bundling edges + descriptors).
207pub fn body_container(model: AnatomyModel, organs: &[OrganContainer]) -> OrganContainer {
208    let body_uri = format!("urn:qualia:anatomy:{}:body", model.as_str());
209    let primary = AssetRef::new(&body_uri, 0, "application/qualia-body", AssetRole::Primary);
210    let mut container = HypermediaContainer::new(
211        format!("urn:qualia:container:{}:body", model.as_str()),
212        primary.clone(),
213    );
214    for oc in organs {
215        // Each organ's primary asset is a member of the body.
216        let p = &oc.container.primary;
217        container = container.with_related(AssetRef::new(
218            &p.uri,
219            p.digest,
220            &p.media_type,
221            AssetRole::Related,
222        ));
223    }
224
225    let (mut quins, mut lexicon) = container_to_nquins(&container);
226    for oc in organs {
227        quins.extend(oc.quins.iter().cloned());
228        for (k, v) in &oc.lexicon {
229            lexicon.entry(*k).or_insert_with(|| v.clone());
230        }
231    }
232    let descriptors = Descriptors {
233        topics: vec!["anatomy".into(), "biology".into()],
234        depicts: vec![format!("{} body", model.as_str())],
235        document_type: Some("3d-anatomy-body".into()),
236        ..Default::default()
237    };
238    let (dq, dl) = descriptors_to_nquins(primary.subject(), &descriptors);
239    quins.extend(dq);
240    for (k, v) in dl {
241        lexicon.entry(k).or_insert(v);
242    }
243    OrganContainer {
244        container,
245        quins,
246        lexicon,
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    // A single OBJ triangle standing in for an organ mesh (real assets are GLB; import_glb is proven
255    // separately in render::assets — this exercises the ingestion orchestration, not GLB parsing).
256    const TRI_OBJ: &[u8] = b"v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\n";
257
258    #[test]
259    fn compile_body_resolves_systems_binds_facts_and_reports_unmapped() {
260        let organs = vec![
261            ("3d-vh-m-lung.obj".to_string(), TRI_OBJ.to_vec()),
262            (
263                "3d-vh-m-blood-vasculature.obj".to_string(),
264                TRI_OBJ.to_vec(),
265            ),
266            ("3d-vh-m-flux-capacitor.obj".to_string(), TRI_OBJ.to_vec()),
267        ];
268        let result = compile_body(AnatomyModel::Male, &organs);
269        assert_eq!(result.compiled_count(), 2);
270        assert_eq!(
271            result.unmapped,
272            vec!["3d-vh-m-flux-capacitor.obj".to_string()]
273        );
274        assert!(result.failed.is_empty());
275
276        // Systems resolved from the organ keys.
277        let lung = result
278            .organs
279            .iter()
280            .find(|o| o.organ_key.contains("lung"))
281            .unwrap();
282        assert_eq!(lung.system_id, "respiratory");
283        let vasc = result
284            .organs
285            .iter()
286            .find(|o| o.organ_key.contains("vasculature"))
287            .unwrap();
288        assert_eq!(vasc.system_id, "circulatory");
289
290        // Each compiled organ carries its system + model facts and a sealed, larger-than-header .10d.
291        for organ in &result.organs {
292            let vals: Vec<&str> = organ.asset.lexicon.values().map(String::as_str).collect();
293            assert!(
294                vals.contains(&organ.system_id.as_str()),
295                "bodySystem fact present"
296            );
297            assert!(vals.contains(&"male"), "anatomyModel fact present");
298            assert!(
299                organ.asset.container_10d.len() > 64,
300                "sealed .10d container"
301            );
302        }
303    }
304
305    /// Real-asset harness: compile an actual CCF/HRA organ GLB end-to-end. Point `QUALIA_TEST_GLB` at a
306    /// `.glb` fetched from the HRA CDN (see `ccf_resolver`). Ignored by default (needs the file on disk).
307    #[test]
308    #[ignore = "requires a real GLB on disk via QUALIA_TEST_GLB"]
309    fn compile_real_ccf_organ_end_to_end() {
310        let path = std::env::var("QUALIA_TEST_GLB").expect("set QUALIA_TEST_GLB to a .glb path");
311        let bytes = std::fs::read(&path).expect("read glb");
312        let src_len = bytes.len();
313        let filename = std::path::Path::new(&path)
314            .file_name()
315            .unwrap()
316            .to_string_lossy()
317            .to_string();
318        let result = compile_body(AnatomyModel::Male, &[(filename.clone(), bytes)]);
319        assert_eq!(
320            result.compiled_count(),
321            1,
322            "unmapped={:?} failed={:?}",
323            result.unmapped,
324            result.failed
325        );
326        let organ = &result.organs[0];
327        // The .10d round-trips back to a mesh.
328        let mesh = qualia_core_db::render::compile_10d::decode_10d_mesh(&organ.asset.container_10d)
329            .unwrap();
330        eprintln!(
331            "REAL CCF ORGAN {filename} → system={} · {} verts / {} tris · GLB {src_len} B → .10d {} B ({:.2}x)",
332            organ.system_id,
333            mesh.vertex_count(),
334            mesh.triangle_count(),
335            organ.asset.container_10d.len(),
336            src_len as f64 / organ.asset.container_10d.len() as f64,
337        );
338    }
339
340    /// Whole-body harness: discover the male organ set from the HRA SPARQL endpoint, fetch each GLB
341    /// from its CDN URL, and compile the entire body. Live network + heavy — ignored by default.
342    #[test]
343    #[ignore = "live network: discovers + fetches + compiles a full model (QUALIA_TEST_MODEL=male|female)"]
344    fn compile_full_body_from_sparql() {
345        use crate::wellfair::ccf_resolver::{
346            discover_ref_organs, fetch_glb, organs_for_model, HRA_SPARQL_ENDPOINT,
347        };
348        let model = match std::env::var("QUALIA_TEST_MODEL").as_deref() {
349            Ok("female") => AnatomyModel::Female,
350            _ => AnatomyModel::Male,
351        };
352        let all = discover_ref_organs(HRA_SPARQL_ENDPOINT).expect("SPARQL discovery");
353        let set = organs_for_model(&all, model);
354        eprintln!(
355            "discovered {} total organs, {} {}",
356            all.len(),
357            set.len(),
358            model.as_str()
359        );
360        assert!(
361            set.len() > 20,
362            "expected a full {} set, got {}",
363            model.as_str(),
364            set.len()
365        );
366
367        let mut fetched = Vec::new();
368        let mut total_glb = 0usize;
369        for organ in &set {
370            match fetch_glb(&organ.glb_url) {
371                Ok(bytes) => {
372                    total_glb += bytes.len();
373                    fetched.push((organ.filename.clone(), bytes));
374                }
375                Err(e) => eprintln!("  fetch FAILED {}: {e}", organ.filename),
376            }
377        }
378
379        let result = compile_body(model, &fetched);
380        let total_10d: usize = result
381            .organs
382            .iter()
383            .map(|o| o.asset.container_10d.len())
384            .sum();
385        let mut systems: Vec<&str> = result.organs.iter().map(|o| o.system_id.as_str()).collect();
386        systems.sort();
387        systems.dedup();
388        eprintln!(
389            "{} BODY: {} / {} organs compiled · {} systems {:?} · unmapped={:?} failed={:?} · GLB {} B → .10d {} B ({:.2}x)",
390            model.as_str().to_uppercase(),
391            result.compiled_count(),
392            set.len(),
393            systems.len(),
394            systems,
395            result.unmapped,
396            result.failed.iter().map(|(k, _)| k).collect::<Vec<_>>(),
397            total_glb,
398            total_10d,
399            total_glb as f64 / total_10d.max(1) as f64,
400        );
401        // Every fetched male organ must resolve to a system — the map covers the real full set.
402        assert!(
403            result.unmapped.is_empty(),
404            "unmapped organs: {:?}",
405            result.unmapped
406        );
407        assert!(
408            result.failed.is_empty(),
409            "failed organs: {:?}",
410            result.failed
411        );
412    }
413
414    #[test]
415    fn bad_bytes_are_reported_not_silently_dropped() {
416        // A key that resolves to a system (lung → respiratory) but whose bytes are not a valid mesh.
417        let organs = vec![("3d-vh-f-lung.glb".to_string(), vec![0u8, 1, 2, 3])];
418        let result = compile_body(AnatomyModel::Female, &organs);
419        assert_eq!(result.compiled_count(), 0);
420        assert_eq!(result.failed.len(), 1);
421        assert_eq!(result.failed[0].0, "3d-vh-f-lung.glb");
422    }
423
424    #[test]
425    fn organ_becomes_a_searchable_provenance_carrying_container() {
426        use qualia_core_db::hypermedia::{by_topic, derived_from, provenance_of};
427        let organs = vec![("3d-vh-m-lung.obj".to_string(), TRI_OBJ.to_vec())];
428        let body = compile_body(AnatomyModel::Male, &organs);
429        let organ = &body.organs[0];
430        let oc = organ_container(
431            organ,
432            AnatomyModel::Male,
433            Some("https://cdn.humanatlas.io/hra/lung.glb"),
434        );
435
436        let primary = oc.container.primary.subject();
437        // Findable by MEANING (its system + generic topics), not by a folder path.
438        assert!(
439            by_topic(&oc.quins, "respiratory").contains(&primary),
440            "found by system topic"
441        );
442        assert!(by_topic(&oc.quins, "anatomy").contains(&primary));
443        // Lineage + provenance are edges on the asset.
444        assert!(
445            !derived_from(&oc.quins, primary).is_empty(),
446            "derived from its source GLB"
447        );
448        assert!(
449            provenance_of(&oc.quins, primary).is_some(),
450            "provenance record bound"
451        );
452        // The container joins to the organ's own geometry manifest (one identity space).
453        let vals: Vec<&str> = oc.lexicon.values().map(String::as_str).collect();
454        assert!(
455            vals.contains(&"respiratory"),
456            "manifest system fact present in the container"
457        );
458        assert!(
459            oc.quins.iter().all(|q| q.verify_ecc_parity()),
460            "all quins valid parity"
461        );
462    }
463
464    #[test]
465    fn body_container_bundles_the_organs_as_a_semantic_unit() {
466        use qualia_core_db::hypermedia::bundled;
467        let organs = vec![
468            ("3d-vh-m-lung.obj".to_string(), TRI_OBJ.to_vec()),
469            (
470                "3d-vh-m-blood-vasculature.obj".to_string(),
471                TRI_OBJ.to_vec(),
472            ),
473        ];
474        let result = compile_body(AnatomyModel::Male, &organs);
475        let ocs: Vec<_> = result
476            .organs
477            .iter()
478            .map(|o| organ_container(o, AnatomyModel::Male, None))
479            .collect();
480        let body = body_container(AnatomyModel::Male, &ocs);
481        // The body is a semantic bundle of its organ members, not a folder of files.
482        let members = bundled(&body.quins, body.container.subject());
483        assert!(members.len() >= 2, "body bundles its organ members");
484    }
485}