Skip to main content

qualia_core_db/render/
compile_10d.rs

1//! Compile an imported triangle [`Mesh`] into a sealed `.10d` container — the
2//! **dense compiled-geometry** half of a geometry asset (see
3//! `docs/manuals/standards/geometry-asset-ontology.md` §3). This is the
4//! "mesh → `.10d`" step of the 3-D-anatomy asset pipeline: the renderer and the
5//! anatomy layer read the `.10d` back with [`decode_10d_mesh`] instead of
6//! reparsing the source GLB, and the q42 semantic manifest cites the container's
7//! [`compiled_digest`].
8//!
9//! **Scope (honest):** emits a `QuantizedMesh` section (u16-quantized
10//! vertices in the bbox + u16/u32 indices). Optional `Tensor10DNodes` (D1),
11//! provenance sidecars, and on native / `wasm-scientific` builds optional
12//! `Topology` + `SpatialIndex` sections (C3) for scan-free picking. LOD chain
13//! from `decimate_3` remains a separate pre-compile step.
14
15use crate::container_10d::crc32c::crc32c;
16use crate::container_10d::header::Container10dHeader;
17use crate::container_10d::integrity::{compute_whole_file_crc32c, seal_whole_file_crc32c};
18use crate::container_10d::mesh_section::{
19    decode_mesh_section, encode_mesh_section, encoded_len, MeshSectionError,
20};
21use crate::container_10d::node_section::{
22    parse_node_header, read_node, write_node_section_aos, NodeMiniHeader, NodeSectionError,
23};
24use crate::container_10d::provenance_section::{
25    encode_provenance_section, encoded_len as provenance_encoded_len, ProvenanceSectionError,
26    ProvenanceSidecar,
27};
28use crate::container_10d::section::{
29    encode_container, parse_section_table, AlignmentTier, SectionInput, SectionTableError,
30    SectionType,
31};
32use crate::render::assets::{
33    import_asset, mesh_to_nquins_with_dev, mesh_to_nquins_with_meta, AssetError, Mesh,
34};
35use crate::tensor::Tensor10D;
36use crate::NQuin;
37use std::collections::HashMap;
38
39/// Optional extra sections for vision / recon seals (programme C3).
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub struct Compile10dExtras {
42    /// Build half-edge Topology section from mesh triangles.
43    pub topology: bool,
44    /// Build BVH + kd-tree SpatialIndex over triangle AABBs / vertices.
45    pub spatial_index: bool,
46}
47
48impl Compile10dExtras {
49    /// Full vision recon package: topology + spatial index (when CG available).
50    pub const VISION: Self = Self {
51        topology: true,
52        spatial_index: true,
53    };
54}
55
56/// Failure modes for `.10d` compilation and read-back.
57///
58/// Not `Clone` — it wraps [`AssetError`], which is not `Clone`. Errors are consumed
59/// on the failure path, not duplicated.
60#[derive(Debug, PartialEq, Eq)]
61pub enum Compile10dError {
62    /// Decoding the source asset bytes (OBJ/STL/GLB) failed.
63    Import(AssetError),
64    /// The QuantizedMesh section encode/decode failed.
65    Mesh(MeshSectionError),
66    /// The container section table encode/decode failed.
67    Section(SectionTableError),
68    /// Encoding the provenance sidecar section failed.
69    Provenance(ProvenanceSectionError),
70    /// Encoding or reading the Tensor10DNodes section failed.
71    Nodes(NodeSectionError),
72    /// Topology / spatial-index extra section failed (C3).
73    ExtraSection { kind: &'static str },
74    /// The container parsed but held no `QuantizedMesh` section.
75    NoMeshSection,
76    /// The container parsed but held no `Tensor10DNodes` section.
77    NoNodesSection,
78    /// The 64-byte container header failed to parse on read-back.
79    BadHeader,
80    /// A section descriptor's byte range fell outside the container bytes.
81    SectionOutOfBounds,
82}
83
84impl std::fmt::Display for Compile10dError {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        match self {
87            Self::Import(e) => write!(f, ".10d compile: source import: {e}"),
88            Self::Mesh(e) => write!(f, ".10d compile: mesh section: {e}"),
89            Self::Section(e) => write!(f, ".10d compile: section table: {e:?}"),
90            Self::Provenance(e) => write!(f, ".10d compile: provenance section: {e}"),
91            Self::Nodes(e) => write!(f, ".10d compile: Tensor10DNodes section: {e}"),
92            Self::ExtraSection { kind } => write!(f, ".10d compile: extra section {kind} failed"),
93            Self::NoMeshSection => write!(f, ".10d: no QuantizedMesh section in container"),
94            Self::NoNodesSection => write!(f, ".10d: no Tensor10DNodes section in container"),
95            Self::BadHeader => write!(f, ".10d: container header failed to parse"),
96            Self::SectionOutOfBounds => write!(f, ".10d: section byte range outside container"),
97        }
98    }
99}
100
101impl std::error::Error for Compile10dError {}
102
103/// Compile a mesh into a **sealed** `.10d` container holding one `QuantizedMesh`
104/// section. The whole-file CRC-32C is written (the container is self-verifying).
105///
106/// Cold path (one-shot asset compilation) — allocates the output `Vec`; the hot
107/// render/query paths operate zero-heap on the decoded section, not here.
108/// Deterministic: identical input → byte-identical container (attestable).
109pub fn compile_mesh_to_10d(mesh: &Mesh) -> Result<Vec<u8>, Compile10dError> {
110    compile_mesh_to_10d_with_provenance(mesh, None)
111}
112
113/// Compile a mesh into a sealed `.10d`, optionally **bundling a provenance sidecar
114/// physically inside the container** (P1) — the immutable source bytes + licence
115/// (+ optional VC) the asset was derived from, so context is byte-inseparable: a
116/// `.10d` copied on its own still carries what it came from and under what licence.
117/// The renderer's governance path treats the presence of this section as the
118/// attestation that makes the mesh *citable* (see `render/portal/mod.rs`).
119pub fn compile_mesh_to_10d_with_provenance(
120    mesh: &Mesh,
121    provenance: Option<&ProvenanceSidecar>,
122) -> Result<Vec<u8>, Compile10dError> {
123    compile_mesh_to_10d_with_nodes_and_provenance(mesh, &[], provenance)
124}
125
126/// Compile mesh + optional Tensor10D nodes (vision detections / σ paint fuel).
127///
128/// Nodes are encoded as `SectionType::Tensor10DNodes` (AoS). Empty `nodes` is
129/// equivalent to [`compile_mesh_to_10d`].
130pub fn compile_mesh_to_10d_with_nodes(
131    mesh: &Mesh,
132    nodes: &[Tensor10D],
133) -> Result<Vec<u8>, Compile10dError> {
134    compile_mesh_to_10d_with_nodes_and_provenance(mesh, nodes, None)
135}
136
137/// Full vision/recon seal: mesh + nodes + optional provenance (no topology extras).
138pub fn compile_mesh_to_10d_with_nodes_and_provenance(
139    mesh: &Mesh,
140    nodes: &[Tensor10D],
141    provenance: Option<&ProvenanceSidecar>,
142) -> Result<Vec<u8>, Compile10dError> {
143    compile_mesh_to_10d_with_extras(mesh, nodes, provenance, Compile10dExtras::default())
144}
145
146/// Vision recon seal: mesh + nodes + Topology + SpatialIndex when CG is linked (C3).
147///
148/// On slim WASM portal builds without `wasm-scientific`, extras are silently
149/// skipped (mesh+nodes still seal) so product paths stay portable.
150pub fn compile_mesh_to_10d_vision(
151    mesh: &Mesh,
152    nodes: &[Tensor10D],
153) -> Result<Vec<u8>, Compile10dError> {
154    compile_mesh_to_10d_with_extras(mesh, nodes, None, Compile10dExtras::VISION)
155}
156
157/// Vision recon seal + in-envelope provenance (D4).
158pub fn compile_mesh_to_10d_vision_with_provenance(
159    mesh: &Mesh,
160    nodes: &[Tensor10D],
161    provenance: &ProvenanceSidecar,
162) -> Result<Vec<u8>, Compile10dError> {
163    compile_mesh_to_10d_with_extras(mesh, nodes, Some(provenance), Compile10dExtras::VISION)
164}
165
166/// Full seal with explicit extras (topology / spatial index).
167pub fn compile_mesh_to_10d_with_extras(
168    mesh: &Mesh,
169    nodes: &[Tensor10D],
170    provenance: Option<&ProvenanceSidecar>,
171    extras: Compile10dExtras,
172) -> Result<Vec<u8>, Compile10dError> {
173    // 1. Encode the QuantizedMesh section payload.
174    let mut payload = vec![0u8; encoded_len(mesh.vertex_count(), mesh.triangle_count())];
175    let written = encode_mesh_section(mesh, &mut payload).map_err(Compile10dError::Mesh)?;
176    payload.truncate(written);
177
178    // 1b. Encode Tensor10DNodes (AoS) when present.
179    let node_payload: Option<Vec<u8>> = if nodes.is_empty() {
180        None
181    } else {
182        let mut buf = vec![0u8; NodeMiniHeader::payload_bytes(nodes.len())];
183        let n = write_node_section_aos(nodes, &mut buf).map_err(Compile10dError::Nodes)?;
184        buf.truncate(n);
185        Some(buf)
186    };
187
188    // 1c. Encode the provenance sidecar payload, if bundling one.
189    let prov_payload: Option<Vec<u8>> = match provenance {
190        Some(p) => {
191            let mut buf = vec![0u8; provenance_encoded_len(p)];
192            let n = encode_provenance_section(p, &mut buf).map_err(Compile10dError::Provenance)?;
193            buf.truncate(n);
194            Some(buf)
195        }
196        None => None,
197    };
198
199    // 1d. Optional Topology + SpatialIndex (native / wasm-scientific only).
200    let topo_payload = if extras.topology {
201        encode_topology_for_mesh(mesh)?
202    } else {
203        None
204    };
205    let spatial_payload = if extras.spatial_index {
206        encode_spatial_index_for_mesh(mesh)?
207    } else {
208        None
209    };
210
211    // 2. Assemble the container. Writer canonical-orders by section type.
212    let header = Container10dHeader::proposed();
213    let mut inputs = vec![SectionInput {
214        section_type: SectionType::QuantizedMesh,
215        alignment_tier: AlignmentTier::Page,
216        stride: 0,
217        element_count: 0,
218        payload: &payload,
219    }];
220    if let Some(np) = &node_payload {
221        // stride/element_count stay 0: payload includes 16-byte NodeMiniHeader +
222        // N×40 tensors (same contract as container_10d conformance golden).
223        inputs.push(SectionInput {
224            section_type: SectionType::Tensor10DNodes,
225            alignment_tier: AlignmentTier::CacheLine,
226            stride: 0,
227            element_count: 0,
228            payload: np,
229        });
230    }
231    if let Some(pp) = &prov_payload {
232        inputs.push(SectionInput {
233            section_type: SectionType::ProvenanceSidecar,
234            alignment_tier: AlignmentTier::Word,
235            stride: 0,
236            element_count: 0,
237            payload: pp,
238        });
239    }
240    if let Some(tp) = &topo_payload {
241        inputs.push(SectionInput {
242            section_type: SectionType::Topology,
243            alignment_tier: AlignmentTier::Word,
244            stride: 0,
245            element_count: 0,
246            payload: tp,
247        });
248    }
249    if let Some(sp) = &spatial_payload {
250        inputs.push(SectionInput {
251            section_type: SectionType::SpatialIndex,
252            alignment_tier: AlignmentTier::Page,
253            stride: 0,
254            element_count: 0,
255            payload: sp,
256        });
257    }
258    // Dry-run against an empty buffer to size the output exactly.
259    let needed = match encode_container(&header, &inputs, &mut []) {
260        Err(SectionTableError::OutputBufferTooSmall { needed, .. }) => needed,
261        Ok(n) => n,
262        Err(e) => return Err(Compile10dError::Section(e)),
263    };
264    let mut out = vec![0u8; needed];
265    let total = encode_container(&header, &inputs, &mut out).map_err(Compile10dError::Section)?;
266    out.truncate(total);
267
268    // 3. Seal the whole-file CRC-32C (the `compiledDigest` source).
269    seal_whole_file_crc32c(&mut out);
270    Ok(out)
271}
272
273#[cfg(any(not(target_arch = "wasm32"), feature = "wasm-scientific"))]
274fn encode_topology_for_mesh(mesh: &Mesh) -> Result<Option<Vec<u8>>, Compile10dError> {
275    use crate::container_10d::topology_section::{
276        encode_topology_section, encoded_len as topo_len,
277    };
278    use crate::specialized_libs::computational_geometry::{
279        build_triangle_half_edges, required_edge_slots, EdgeSlot, HalfEdge,
280    };
281
282    if mesh.triangle_count() == 0 || mesh.vertex_count() == 0 {
283        return Ok(None);
284    }
285    let vc = mesh.vertex_count() as u32;
286    let fc = mesh.triangle_count() as u32;
287    let mut edges = vec![HalfEdge::default(); mesh.triangles.len().saturating_mul(3)];
288    let mut slots = vec![EdgeSlot::default(); required_edge_slots(mesh.triangles.len())];
289    build_triangle_half_edges(vc, &mesh.triangles, &mut edges, &mut slots).map_err(|_| {
290        Compile10dError::ExtraSection {
291            kind: "topology_half_edges",
292        }
293    })?;
294    let need = topo_len(vc, fc, edges.len() as u32);
295    let mut buf = vec![0u8; need];
296    let n = encode_topology_section(vc, fc, &edges, &mut buf).map_err(|_| {
297        Compile10dError::ExtraSection {
298            kind: "topology_encode",
299        }
300    })?;
301    buf.truncate(n);
302    Ok(Some(buf))
303}
304
305#[cfg(not(any(not(target_arch = "wasm32"), feature = "wasm-scientific")))]
306fn encode_topology_for_mesh(_mesh: &Mesh) -> Result<Option<Vec<u8>>, Compile10dError> {
307    Ok(None)
308}
309
310#[cfg(any(not(target_arch = "wasm32"), feature = "wasm-scientific"))]
311fn encode_spatial_index_for_mesh(mesh: &Mesh) -> Result<Option<Vec<u8>>, Compile10dError> {
312    use crate::container_10d::spatial_index_section::{
313        encode_spatial_index_section, encoded_len as spatial_len,
314    };
315    use crate::specialized_libs::computational_geometry::{
316        build_bvh_recursive, build_kd_tree_3d, Aabb, BvhNode, KdNode, Point3,
317    };
318
319    if mesh.triangle_count() == 0 || mesh.vertex_count() == 0 {
320        return Ok(None);
321    }
322
323    let mut aabbs = Vec::with_capacity(mesh.triangles.len());
324    for tri in &mesh.triangles {
325        let p0 = mesh.positions[tri[0] as usize];
326        let p1 = mesh.positions[tri[1] as usize];
327        let p2 = mesh.positions[tri[2] as usize];
328        let min = Point3::new(
329            p0[0].min(p1[0]).min(p2[0]) as f64,
330            p0[1].min(p1[1]).min(p2[1]) as f64,
331            p0[2].min(p1[2]).min(p2[2]) as f64,
332        );
333        let max = Point3::new(
334            p0[0].max(p1[0]).max(p2[0]) as f64,
335            p0[1].max(p1[1]).max(p2[1]) as f64,
336            p0[2].max(p1[2]).max(p2[2]) as f64,
337        );
338        aabbs.push(Aabb::new(min, max));
339    }
340    let n = aabbs.len();
341    let mut bvh_nodes = vec![BvhNode::default(); 2 * n];
342    let mut bvh_indices = vec![0u32; n];
343    let mut bvh_codes = vec![0u64; n];
344    let mut bvh_sort = vec![0u32; n];
345    let (bvh_count, bvh_root) = build_bvh_recursive(
346        &aabbs,
347        &mut bvh_nodes,
348        &mut bvh_indices,
349        &mut bvh_codes,
350        &mut bvh_sort,
351    )
352    .map_err(|_| Compile10dError::ExtraSection { kind: "bvh_build" })?;
353
354    let points: Vec<[f64; 3]> = mesh
355        .positions
356        .iter()
357        .map(|p| [p[0] as f64, p[1] as f64, p[2] as f64])
358        .collect();
359    let np = points.len();
360    let mut kd_nodes = vec![KdNode::default(); np];
361    let mut kd_indices = vec![0u32; np];
362    let mut kd_codes = vec![0u64; np];
363    let mut kd_sort = vec![0u32; np];
364    let (kd_count, kd_root) = build_kd_tree_3d(
365        &points,
366        &mut kd_nodes,
367        &mut kd_indices,
368        &mut kd_codes,
369        &mut kd_sort,
370    )
371    .map_err(|_| Compile10dError::ExtraSection { kind: "kd_build" })?;
372
373    let need = spatial_len(bvh_count as u32, kd_count as u32, n as u32, np as u32);
374    let mut buf = vec![0u8; need];
375    encode_spatial_index_section(
376        &bvh_nodes[..bvh_count],
377        &bvh_indices,
378        bvh_root as u32,
379        &kd_nodes[..kd_count],
380        &kd_indices,
381        kd_root as u32,
382        &mut buf,
383    )
384    .map_err(|_| Compile10dError::ExtraSection {
385        kind: "spatial_encode",
386    })?;
387    Ok(Some(buf))
388}
389
390#[cfg(not(any(not(target_arch = "wasm32"), feature = "wasm-scientific")))]
391fn encode_spatial_index_for_mesh(_mesh: &Mesh) -> Result<Option<Vec<u8>>, Compile10dError> {
392    Ok(None)
393}
394
395/// The `compiledDigest` a q42 asset manifest cites: the container's whole-file
396/// CRC-32C (computed with the header's own CRC field zeroed, so it equals the
397/// sealed header value). Deterministic; changes on any geometry-byte change.
398#[inline]
399pub fn compiled_digest(container_10d: &[u8]) -> u32 {
400    compute_whole_file_crc32c(container_10d)
401}
402
403/// Read a `.10d` container back into a dequantized [`Mesh`] — the renderer /
404/// anatomy path that avoids reparsing the source GLB. Extracts the first
405/// `QuantizedMesh` section.
406pub fn decode_10d_mesh(container_10d: &[u8]) -> Result<Mesh, Compile10dError> {
407    let header =
408        Container10dHeader::parse(container_10d).map_err(|_| Compile10dError::BadHeader)?;
409    let descs = parse_section_table(container_10d, &header).map_err(Compile10dError::Section)?;
410    for d in descs {
411        if d.typ() == Some(SectionType::QuantizedMesh) {
412            let start = d.byte_offset as usize;
413            let end = start
414                .checked_add(d.byte_length as usize)
415                .ok_or(Compile10dError::SectionOutOfBounds)?;
416            let payload = container_10d
417                .get(start..end)
418                .ok_or(Compile10dError::SectionOutOfBounds)?;
419            return decode_mesh_section(payload).map_err(Compile10dError::Mesh);
420        }
421    }
422    Err(Compile10dError::NoMeshSection)
423}
424
425/// Read Tensor10D nodes from a sealed `.10d` (first Tensor10DNodes section).
426///
427/// Returns the node count written into `out` (caller buffer; truncated to
428/// `out.len()`). Fail-closed if the section is missing or malformed.
429pub fn decode_10d_nodes(
430    container_10d: &[u8],
431    out: &mut [Tensor10D],
432) -> Result<usize, Compile10dError> {
433    let header =
434        Container10dHeader::parse(container_10d).map_err(|_| Compile10dError::BadHeader)?;
435    let descs = parse_section_table(container_10d, &header).map_err(Compile10dError::Section)?;
436    for d in descs {
437        if d.typ() == Some(SectionType::Tensor10DNodes) {
438            let start = d.byte_offset as usize;
439            let end = start
440                .checked_add(d.byte_length as usize)
441                .ok_or(Compile10dError::SectionOutOfBounds)?;
442            let payload = container_10d
443                .get(start..end)
444                .ok_or(Compile10dError::SectionOutOfBounds)?;
445            let (nh, _) = parse_node_header(payload).map_err(Compile10dError::Nodes)?;
446            let count = (nh.node_count as usize).min(out.len());
447            for i in 0..count {
448                out[i] = read_node(payload, i).map_err(Compile10dError::Nodes)?;
449            }
450            return Ok(count);
451        }
452    }
453    Err(Compile10dError::NoNodesSection)
454}
455
456/// A fully compiled geometry asset: the source-imported [`Mesh`], the sealed `.10d`
457/// container, its two content digests, and the q42 semantic manifest that cites the
458/// container by `compiledDigest` (geometry-asset-ontology §1 two-layer model).
459///
460/// This is the whole "GLB → `.10d` + manifest" pipeline output. The `container_10d`
461/// bytes are the on-disk sidecar; `quins`/`lexicon` are the portable q42 facts that
462/// travel in the graph and point at the container by hash.
463#[derive(Debug, Clone)]
464pub struct CompiledAsset {
465    /// The imported triangle mesh (positions + indices + bbox).
466    pub mesh: Mesh,
467    /// The sealed `.10d` container (dense compiled geometry).
468    pub container_10d: Vec<u8>,
469    /// Whole-file CRC-32C of `container_10d` — the manifest's `compiledDigest`.
470    pub compiled_digest: u32,
471    /// CRC-32C of the immutable source bytes — the manifest's `sourceDigest`.
472    pub source_digest: u32,
473    /// The q42 manifest facts, including both digests (via [`mesh_to_nquins_with_digests`]).
474    pub quins: Vec<NQuin>,
475    /// Object-lexicon for the string-valued facts in `quins`.
476    pub lexicon: HashMap<u64, String>,
477}
478
479/// The end-to-end asset-compile step: source asset bytes → [`CompiledAsset`].
480///
481/// Runs the full pipeline — import the source ([`import_asset`]), compile the dense
482/// `.10d` ([`compile_mesh_to_10d`]), hash both layers, then emit the q42 manifest that
483/// binds them ([`mesh_to_nquins_with_digests`]). Deterministic: identical
484/// `(source_bytes, asset_uri, source_format)` → byte-identical container and identical
485/// digests, so the manifest→container citation is attestable.
486///
487/// `hint` is the source-format hint forwarded to `import_asset` (e.g. `Some("glb")`);
488/// `source_format` is the value recorded in the manifest and should agree with the real
489/// format of `source_bytes`.
490pub fn compile_asset(
491    source_bytes: &[u8],
492    hint: Option<&str>,
493    asset_uri: &str,
494    source_format: &str,
495) -> Result<CompiledAsset, Compile10dError> {
496    compile_organ_asset(
497        source_bytes,
498        hint,
499        asset_uri,
500        source_format,
501        None,
502        None,
503        None,
504    )
505}
506
507/// Like [`compile_asset`] but also binds the compiled asset to a 3D-body organ: its `body_system`
508/// (which of the 17 systems colours it by burden) and `anatomy_model` (`"male"` / `"female"`, from the
509/// user's declared XY/XX basis). This is the compile step for an anatomy organ mesh — the manifest it
510/// emits carries `geo:bodySystem` + `geo:anatomyModel` so the renderer can look the organ up in the
511/// per-system percept table (S5.1) and both colour and sonify it.
512#[allow(clippy::too_many_arguments)]
513pub fn compile_organ_asset(
514    source_bytes: &[u8],
515    hint: Option<&str>,
516    asset_uri: &str,
517    source_format: &str,
518    body_system: Option<&str>,
519    anatomy_model: Option<&str>,
520    provenance: Option<&ProvenanceSidecar>,
521) -> Result<CompiledAsset, Compile10dError> {
522    let mesh = import_asset(source_bytes, hint).map_err(Compile10dError::Import)?;
523    // When provenance is supplied it is sealed into the `.10d` as a ProvenanceSidecar section, so the
524    // asset is attested (the renderer's fail-closed governance gate treats an unattested asset with the
525    // default-refuse disposition as REFUSE).
526    let container_10d = compile_mesh_to_10d_with_provenance(&mesh, provenance)?;
527    let compiled = compiled_digest(&container_10d);
528    let source = crc32c(source_bytes);
529    let (quins, lexicon) = mesh_to_nquins_with_meta(
530        &mesh,
531        asset_uri,
532        source_format,
533        source,
534        compiled,
535        body_system,
536        anatomy_model,
537    );
538    Ok(CompiledAsset {
539        mesh,
540        container_10d,
541        compiled_digest: compiled,
542        source_digest: source,
543        quins,
544        lexicon,
545    })
546}
547
548/// Like [`compile_asset`] but binds the compiled asset to a point on the developmental **`t`-axis** — its
549/// `gestational_age_days` (postfertilization) and `carnegie_stage`. This is the compile step for a fetal/
550/// embryonic stage: consecutive stages, ordered by gestational age, form a 4-D developmental body (the
551/// maternal–fetal dyad's fetal side, reproductive-continuum plan §2).
552pub fn compile_developmental_asset(
553    source_bytes: &[u8],
554    hint: Option<&str>,
555    asset_uri: &str,
556    source_format: &str,
557    gestational_age_days: u16,
558    carnegie_stage: u8,
559) -> Result<CompiledAsset, Compile10dError> {
560    let mesh = import_asset(source_bytes, hint).map_err(Compile10dError::Import)?;
561    let container_10d = compile_mesh_to_10d(&mesh)?;
562    let compiled = compiled_digest(&container_10d);
563    let source = crc32c(source_bytes);
564    let (quins, lexicon) = mesh_to_nquins_with_dev(
565        &mesh,
566        asset_uri,
567        source_format,
568        source,
569        compiled,
570        gestational_age_days,
571        carnegie_stage,
572    );
573    Ok(CompiledAsset {
574        mesh,
575        container_10d,
576        compiled_digest: compiled,
577        source_digest: source,
578        quins,
579        lexicon,
580    })
581}
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586    use crate::container_10d::integrity::verify_whole_file_crc32c;
587
588    /// Unit cube, 8 vertices / 12 triangles — a valid `render::Mesh`.
589    fn cube() -> Mesh {
590        Mesh {
591            positions: vec![
592                [0.0, 0.0, 0.0],
593                [1.0, 0.0, 0.0],
594                [1.0, 1.0, 0.0],
595                [0.0, 1.0, 0.0],
596                [0.0, 0.0, 1.0],
597                [1.0, 0.0, 1.0],
598                [1.0, 1.0, 1.0],
599                [0.0, 1.0, 1.0],
600            ],
601            triangles: vec![
602                [0, 3, 2],
603                [0, 2, 1],
604                [4, 5, 6],
605                [4, 6, 7],
606                [0, 1, 5],
607                [0, 5, 4],
608                [3, 7, 6],
609                [3, 6, 2],
610                [0, 4, 7],
611                [0, 7, 3],
612                [1, 2, 6],
613                [1, 6, 5],
614            ],
615            min: [0.0, 0.0, 0.0],
616            max: [1.0, 1.0, 1.0],
617        }
618    }
619
620    #[test]
621    fn container_is_well_formed_and_sealed() {
622        let mut bytes = compile_mesh_to_10d(&cube()).unwrap();
623        assert!(
624            bytes.len() > 64,
625            "container must be larger than the 64-byte header"
626        );
627        // The seal verifies (whole-file CRC matches the header) — proves it's well-formed.
628        verify_whole_file_crc32c(&mut bytes).expect(".10d whole-file CRC must verify");
629    }
630
631    #[test]
632    fn bundles_provenance_physically_and_still_decodes_the_mesh() {
633        use crate::container_10d::provenance_section::{
634            decode_provenance_section, validate_provenance,
635        };
636
637        let source = b"<the original source GLB bytes for this organ>".to_vec();
638        let sidecar = ProvenanceSidecar::new(source.clone(), "model/gltf-binary", "CC-BY-4.0");
639        let mut bytes = compile_mesh_to_10d_with_provenance(&cube(), Some(&sidecar)).unwrap();
640
641        // The whole-file seal still verifies with the extra section.
642        verify_whole_file_crc32c(&mut bytes).expect(".10d whole-file CRC must verify");
643        // The mesh is still decodable (the provenance section does not disturb it).
644        let mesh = decode_10d_mesh(&bytes).unwrap();
645        assert_eq!(mesh.triangle_count(), cube().triangle_count());
646
647        // The provenance sidecar is physically present in the container and passes its gate.
648        let header = Container10dHeader::parse(&bytes).unwrap();
649        let descs = parse_section_table(&bytes, &header).unwrap();
650        let prov = descs
651            .iter()
652            .find(|d| d.typ() == Some(SectionType::ProvenanceSidecar))
653            .expect("provenance section bundled in the .10d");
654        let payload = &bytes[prov.byte_offset as usize..][..prov.byte_length as usize];
655        let view = decode_provenance_section(payload).unwrap();
656        validate_provenance(&view).expect("bundled provenance validates before use");
657        assert_eq!(view.licence(), "CC-BY-4.0");
658        assert_eq!(view.source_bytes(), source.as_slice());
659    }
660
661    #[test]
662    fn round_trips_the_mesh_within_quantization_tolerance() {
663        let m = cube();
664        let bytes = compile_mesh_to_10d(&m).unwrap();
665        let back = decode_10d_mesh(&bytes).unwrap();
666        assert_eq!(back.vertex_count(), m.vertex_count());
667        assert_eq!(back.triangle_count(), m.triangle_count());
668        assert_eq!(back.triangles, m.triangles, "indices are exact");
669        // Positions survive within the u16-in-bbox quantization bound (extent/65535).
670        let tol = 1.0 / 65535.0 * 1.001;
671        for (a, b) in m.positions.iter().zip(back.positions.iter()) {
672            for k in 0..3 {
673                assert!((a[k] - b[k]).abs() <= tol, "vertex {a:?} vs {b:?} axis {k}");
674            }
675        }
676    }
677
678    #[test]
679    fn compilation_is_deterministic() {
680        let m = cube();
681        let a = compile_mesh_to_10d(&m).unwrap();
682        let b = compile_mesh_to_10d(&m).unwrap();
683        assert_eq!(a, b, "identical mesh → byte-identical .10d");
684        assert_eq!(compiled_digest(&a), compiled_digest(&b));
685    }
686
687    #[test]
688    fn digest_changes_when_geometry_changes() {
689        let a = compile_mesh_to_10d(&cube()).unwrap();
690        let mut m2 = cube();
691        m2.positions[6] = [1.0, 1.0, 0.5]; // move one vertex
692        let b = compile_mesh_to_10d(&m2).unwrap();
693        assert_ne!(compiled_digest(&a), compiled_digest(&b));
694    }
695
696    #[test]
697    fn decode_rejects_garbage() {
698        assert_eq!(decode_10d_mesh(&[0u8; 8]), Err(Compile10dError::BadHeader));
699    }
700
701    #[test]
702    fn mesh_with_nodes_round_trips_sigma() {
703        use crate::tensor::Tensor10D;
704        let mesh = cube();
705        let nodes = [
706            Tensor10D::ground_truth(0.0, 0.0, 0.5, 0.5, 0.0, 1.0, 0.9, 0.0, 0.42),
707            Tensor10D::parallel_context(1.0, 0.0, 0.0, 0.1, 0.2, 0.0, 2.0, 0.5, 0.0, 0.7),
708        ];
709        let mut bytes = compile_mesh_to_10d_with_nodes(&mesh, &nodes).unwrap();
710        verify_whole_file_crc32c(&mut bytes).expect("seal");
711        let back = decode_10d_mesh(&bytes).unwrap();
712        assert_eq!(back.triangle_count(), mesh.triangle_count());
713        let mut out = [Tensor10D::default(); 4];
714        let n = decode_10d_nodes(&bytes, &mut out).unwrap();
715        assert_eq!(n, 2);
716        assert!((out[0].sigma - 0.42).abs() < 1e-5);
717        assert!((out[0].x - 0.5).abs() < 1e-5);
718        assert!((out[1].q - 1.0).abs() < 1e-5);
719        assert!((out[1].sigma - 0.7).abs() < 1e-5);
720        // Mesh-only compile must not invent a nodes section.
721        let plain = compile_mesh_to_10d(&mesh).unwrap();
722        assert!(matches!(
723            decode_10d_nodes(&plain, &mut out),
724            Err(Compile10dError::NoNodesSection)
725        ));
726    }
727
728    #[test]
729    fn vision_seal_includes_topology_and_spatial_when_cg() {
730        use crate::tensor::Tensor10D;
731        let mesh = cube();
732        let nodes = [Tensor10D::ground_truth(
733            0.0, 0.0, 0.5, 0.5, 0.0, 0.0, 1.0, 0.0, 0.33,
734        )];
735        let mut bytes = compile_mesh_to_10d_vision(&mesh, &nodes).unwrap();
736        verify_whole_file_crc32c(&mut bytes).expect("seal");
737        let header = Container10dHeader::parse(&bytes).unwrap();
738        let descs = parse_section_table(&bytes, &header).unwrap();
739        let types: Vec<_> = descs.iter().filter_map(|d| d.typ()).collect();
740        assert!(types.contains(&SectionType::QuantizedMesh));
741        assert!(types.contains(&SectionType::Tensor10DNodes));
742        #[cfg(any(not(target_arch = "wasm32"), feature = "wasm-scientific"))]
743        {
744            assert!(
745                types.contains(&SectionType::Topology),
746                "expected Topology section, got {types:?}"
747            );
748            assert!(
749                types.contains(&SectionType::SpatialIndex),
750                "expected SpatialIndex section, got {types:?}"
751            );
752        }
753        let back = decode_10d_mesh(&bytes).unwrap();
754        assert_eq!(back.triangle_count(), mesh.triangle_count());
755    }
756
757    /// A single OBJ triangle — the smallest valid source asset.
758    const TRI_OBJ: &[u8] = b"v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\n";
759
760    #[test]
761    fn compile_asset_binds_manifest_to_its_container() {
762        let a = compile_asset(TRI_OBJ, Some("obj"), "urn:asset:tri", "obj").unwrap();
763
764        // The container round-trips back to the mesh the manifest describes.
765        let back = decode_10d_mesh(&a.container_10d).unwrap();
766        assert_eq!(back.triangle_count(), 1);
767        assert_eq!(back.vertex_count(), 3);
768
769        // compiled_digest field == the container's real whole-file CRC (the citation is honest).
770        assert_eq!(a.compiled_digest, compiled_digest(&a.container_10d));
771        // source_digest == CRC of the source bytes.
772        assert_eq!(a.source_digest, crc32c(TRI_OBJ));
773
774        // Both digests appear as manifest facts (object-side), binding the two layers.
775        let objs: Vec<u64> = a.quins.iter().map(|q| q.object).collect();
776        assert!(
777            objs.contains(&(a.compiled_digest as u64)),
778            "manifest cites compiledDigest"
779        );
780        assert!(
781            objs.contains(&(a.source_digest as u64)),
782            "manifest cites sourceDigest"
783        );
784    }
785
786    #[test]
787    fn compile_asset_is_deterministic() {
788        let a = compile_asset(TRI_OBJ, Some("obj"), "urn:asset:tri", "obj").unwrap();
789        let b = compile_asset(TRI_OBJ, Some("obj"), "urn:asset:tri", "obj").unwrap();
790        assert_eq!(a.container_10d, b.container_10d, "byte-identical container");
791        assert_eq!(a.compiled_digest, b.compiled_digest);
792        assert_eq!(a.source_digest, b.source_digest);
793    }
794
795    #[test]
796    fn compile_asset_surfaces_import_errors() {
797        // Not a recognisable asset in any supported format.
798        let err = compile_asset(b"\x00\x01\x02\x03", None, "urn:asset:junk", "obj");
799        assert!(matches!(err, Err(Compile10dError::Import(_))));
800    }
801
802    #[test]
803    fn compile_organ_asset_binds_body_system_and_model() {
804        let plain = compile_asset(TRI_OBJ, Some("obj"), "urn:asset:organ", "obj").unwrap();
805        let organ = compile_organ_asset(
806            TRI_OBJ,
807            Some("obj"),
808            "urn:asset:organ",
809            "obj",
810            Some("respiratory"),
811            Some("male"),
812            None,
813        )
814        .unwrap();
815        // Same geometry → identical container + digests; only the manifest gains the anatomy facts.
816        assert_eq!(organ.container_10d, plain.container_10d);
817        assert_eq!(organ.compiled_digest, plain.compiled_digest);
818        assert_eq!(
819            organ.quins.len(),
820            plain.quins.len() + 2,
821            "two anatomy facts added"
822        );
823        // bodySystem + anatomyModel strings are carried in the lexicon.
824        let vals: Vec<&str> = organ.lexicon.values().map(String::as_str).collect();
825        assert!(vals.contains(&"respiratory"), "bodySystem fact present");
826        assert!(vals.contains(&"male"), "anatomyModel fact present");
827        // None/None path is exactly compile_asset — no phantom facts.
828        let none = compile_organ_asset(
829            TRI_OBJ,
830            Some("obj"),
831            "urn:asset:organ",
832            "obj",
833            None,
834            None,
835            None,
836        )
837        .unwrap();
838        assert_eq!(none.quins.len(), plain.quins.len());
839    }
840
841    #[test]
842    fn compile_developmental_asset_binds_the_t_axis_coordinate() {
843        let plain = compile_asset(TRI_OBJ, Some("obj"), "urn:asset:fetal", "obj").unwrap();
844        // Carnegie stage 18 ≈ 44 postfertilization days.
845        let dev =
846            compile_developmental_asset(TRI_OBJ, Some("obj"), "urn:asset:fetal", "obj", 44, 18)
847                .unwrap();
848        // Same geometry → identical container; only the manifest gains the two developmental facts.
849        assert_eq!(dev.container_10d, plain.container_10d);
850        assert_eq!(
851            dev.quins.len(),
852            plain.quins.len() + 2,
853            "gestationalAgeDays + carnegieStage"
854        );
855        // The t-axis coordinate (44 days) and the stage (18) are present as u64 fact objects (a 3-vertex,
856        // 1-triangle mesh has no other facts with those values, so this is unambiguous).
857        assert!(
858            dev.quins.iter().any(|q| q.object == 44),
859            "gestationalAgeDays=44"
860        );
861        assert!(dev.quins.iter().any(|q| q.object == 18), "carnegieStage=18");
862    }
863}