Skip to main content

animsmith_core/
raw_scene_inventory.rs

1//! Bounded format-neutral raw scene, attachment, and primitive evidence.
2//!
3//! Normalized mesh assets are not source-presence authority: a loader can
4//! discard source primitive modes it does not normalize. This contract keeps
5//! definition-side primitive rows separate from node attachments, so a later
6//! consumer must make any attachment-by-primitive join under its own budget.
7
8use crate::InputIdentity;
9use crate::bounded_deserialize::{
10    BudgetedCappedSequenceSeed, CappedSequence, RowBudget, consume_ignored_tail,
11};
12use serde::de::{DeserializeSeed, Error as _, IgnoredAny, MapAccess, SeqAccess, Visitor};
13use serde::{Deserialize, Deserializer, Serialize};
14use std::fmt;
15use std::marker::PhantomData;
16
17/// Semantic identity of the raw scene/attachment inventory V1 contract.
18pub const RAW_SCENE_ATTACHMENT_INVENTORY_V1_ID: &str =
19    "urn:animsmith:raw-scene-attachment-inventory:1";
20/// Maximum aggregate rows retained by one V1 inventory.
21pub const RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS: usize = 4_096;
22/// Maximum aggregate UTF-8 bytes retained by one V1 inventory.
23///
24/// V1 is index-only and therefore retains no text. The explicit zero bound
25/// prevents a later producer from slipping unbounded names into this contract.
26pub const RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_TEXT_BYTES: usize = 0;
27
28fn deserialize_inventory_vec<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
29where
30    D: Deserializer<'de>,
31    T: Deserialize<'de>,
32{
33    struct BoundedVecVisitor<T>(PhantomData<T>);
34
35    impl<'de, T> Visitor<'de> for BoundedVecVisitor<T>
36    where
37        T: Deserialize<'de>,
38    {
39        type Value = Vec<T>;
40
41        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
42            write!(
43                formatter,
44                "at most {RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS} raw inventory rows"
45            )
46        }
47
48        fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
49        where
50            A: SeqAccess<'de>,
51        {
52            let mut values = Vec::with_capacity(
53                sequence
54                    .size_hint()
55                    .unwrap_or(0)
56                    .min(RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS),
57            );
58            while values.len() < RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS {
59                let Some(value) = sequence.next_element()? else {
60                    return Ok(values);
61                };
62                values.push(value);
63            }
64            if sequence.next_element::<serde::de::IgnoredAny>()?.is_some() {
65                return Err(A::Error::custom(
66                    "raw scene/attachment inventory exceeded its row bound",
67                ));
68            }
69            Ok(values)
70        }
71    }
72
73    deserializer.deserialize_seq(BoundedVecVisitor(PhantomData))
74}
75
76/// Whether a source-order row set is exhaustive.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(rename_all = "snake_case")]
79pub enum RawSceneAttachmentCoverageV1 {
80    /// Every source row is retained; empty proves absence.
81    Complete,
82    /// Rows are the canonical source-order prefix and later rows overflowed the V1 budget.
83    PrefixOverflow,
84    /// The loader could not make this source domain available.
85    Unavailable,
86}
87
88impl RawSceneAttachmentCoverageV1 {
89    /// Whether an empty row list proves the source domain absent.
90    pub const fn proves_absence(self) -> bool {
91        matches!(self, Self::Complete)
92    }
93}
94
95/// Same-load source-skeleton evidence carried beside raw rows.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
97#[serde(deny_unknown_fields)]
98pub struct RawSourceSkeletonEvidenceV1 {
99    coverage: RawSceneAttachmentCoverageV1,
100    source_node_count: u64,
101    source_skin_count: u64,
102}
103
104impl RawSourceSkeletonEvidenceV1 {
105    /// Construct coverage-qualified source-skeleton cardinality evidence.
106    pub const fn new(
107        coverage: RawSceneAttachmentCoverageV1,
108        source_node_count: u64,
109        source_skin_count: u64,
110    ) -> Self {
111        Self {
112            coverage,
113            source_node_count,
114            source_skin_count,
115        }
116    }
117    /// Source-skeleton evidence coverage.
118    pub const fn coverage(&self) -> RawSceneAttachmentCoverageV1 {
119        self.coverage
120    }
121    /// Source-node rows observed by this load.
122    pub const fn source_node_count(&self) -> u64 {
123        self.source_node_count
124    }
125    /// Source-skin rows observed by this load.
126    pub const fn source_skin_count(&self) -> u64 {
127        self.source_skin_count
128    }
129}
130
131/// One source scene and its declared roots in authored order.
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133#[serde(deny_unknown_fields)]
134pub struct RawSceneRootRowV1 {
135    source_scene_index: u64,
136    #[serde(deserialize_with = "deserialize_inventory_vec")]
137    root_node_indices: Vec<u64>,
138}
139impl RawSceneRootRowV1 {
140    /// Construct one source-scene row.
141    pub fn new(source_scene_index: u64, root_node_indices: Vec<u64>) -> Self {
142        Self {
143            source_scene_index,
144            root_node_indices,
145        }
146    }
147    /// Source scene-array index.
148    pub const fn source_scene_index(&self) -> u64 {
149        self.source_scene_index
150    }
151    /// Root-node identities in authored order.
152    pub fn root_node_indices(&self) -> &[u64] {
153        &self.root_node_indices
154    }
155}
156
157/// One raw node-to-mesh declaration.
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
159#[serde(deny_unknown_fields)]
160pub struct RawNodeMeshAttachmentRowV1 {
161    source_node_index: u64,
162    source_mesh_index: u64,
163}
164impl RawNodeMeshAttachmentRowV1 {
165    /// Construct one raw attachment row.
166    pub const fn new(source_node_index: u64, source_mesh_index: u64) -> Self {
167        Self {
168            source_node_index,
169            source_mesh_index,
170        }
171    }
172    /// Source node-array index.
173    pub const fn source_node_index(&self) -> u64 {
174        self.source_node_index
175    }
176    /// Source mesh-array index.
177    pub const fn source_mesh_index(&self) -> u64 {
178        self.source_mesh_index
179    }
180}
181
182/// Raw primitive topology, including modes normalized mesh assets can omit.
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
184#[serde(rename_all = "snake_case")]
185pub enum RawPrimitiveTopologyV1 {
186    /// glTF mode 0.
187    Points,
188    /// glTF mode 1.
189    Lines,
190    /// glTF mode 2.
191    LineLoop,
192    /// glTF mode 3.
193    LineStrip,
194    /// glTF mode 4.
195    Triangles,
196    /// glTF mode 5.
197    TriangleStrip,
198    /// glTF mode 6.
199    TriangleFan,
200    /// A raw mode outside glTF's enumerated domain.
201    Other {
202        /// Raw `mode` value.
203        mode: u64,
204    },
205}
206impl RawPrimitiveTopologyV1 {
207    /// Classify a raw glTF primitive mode.
208    pub const fn from_gltf_mode(mode: u64) -> Self {
209        match mode {
210            0 => Self::Points,
211            1 => Self::Lines,
212            2 => Self::LineLoop,
213            3 => Self::LineStrip,
214            4 => Self::Triangles,
215            5 => Self::TriangleStrip,
216            6 => Self::TriangleFan,
217            _ => Self::Other { mode },
218        }
219    }
220    /// Raw glTF mode number.
221    pub const fn gltf_mode(self) -> u64 {
222        match self {
223            Self::Points => 0,
224            Self::Lines => 1,
225            Self::LineLoop => 2,
226            Self::LineStrip => 3,
227            Self::Triangles => 4,
228            Self::TriangleStrip => 5,
229            Self::TriangleFan => 6,
230            Self::Other { mode } => mode,
231        }
232    }
233}
234
235/// One raw mesh primitive, independent of node attachment rows.
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
237#[serde(deny_unknown_fields)]
238pub struct RawMeshPrimitiveRowV1 {
239    source_mesh_index: u64,
240    source_primitive_index: u64,
241    topology: RawPrimitiveTopologyV1,
242    indices_accessor_index: Option<u64>,
243}
244impl RawMeshPrimitiveRowV1 {
245    /// Construct one raw primitive row.
246    pub const fn new(
247        source_mesh_index: u64,
248        source_primitive_index: u64,
249        topology: RawPrimitiveTopologyV1,
250        indices_accessor_index: Option<u64>,
251    ) -> Self {
252        Self {
253            source_mesh_index,
254            source_primitive_index,
255            topology,
256            indices_accessor_index,
257        }
258    }
259    /// Source mesh-array index.
260    pub const fn source_mesh_index(&self) -> u64 {
261        self.source_mesh_index
262    }
263    /// Primitive-array index inside the source mesh.
264    pub const fn source_primitive_index(&self) -> u64 {
265        self.source_primitive_index
266    }
267    /// Raw source topology.
268    pub const fn topology(&self) -> RawPrimitiveTopologyV1 {
269        self.topology
270    }
271    /// Declared index accessor, absent for unindexed primitives.
272    pub const fn indices_accessor_index(&self) -> Option<u64> {
273        self.indices_accessor_index
274    }
275}
276
277/// Coverage-qualified source scenes.
278#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
279#[serde(deny_unknown_fields)]
280pub struct RawSceneRootRowsV1 {
281    coverage: RawSceneAttachmentCoverageV1,
282    #[serde(deserialize_with = "deserialize_inventory_vec")]
283    rows: Vec<RawSceneRootRowV1>,
284}
285impl RawSceneRootRowsV1 {
286    /// Construct a source-scene row set.
287    pub fn new(coverage: RawSceneAttachmentCoverageV1, rows: Vec<RawSceneRootRowV1>) -> Self {
288        Self { coverage, rows }
289    }
290    /// Exhaustiveness state.
291    pub const fn coverage(&self) -> RawSceneAttachmentCoverageV1 {
292        self.coverage
293    }
294    /// Canonical source-order rows.
295    pub fn rows(&self) -> &[RawSceneRootRowV1] {
296        &self.rows
297    }
298}
299
300/// Coverage-qualified raw node-to-mesh declarations.
301#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
302#[serde(deny_unknown_fields)]
303pub struct RawNodeMeshAttachmentRowsV1 {
304    coverage: RawSceneAttachmentCoverageV1,
305    #[serde(deserialize_with = "deserialize_inventory_vec")]
306    rows: Vec<RawNodeMeshAttachmentRowV1>,
307}
308impl RawNodeMeshAttachmentRowsV1 {
309    /// Construct an attachment row set.
310    pub fn new(
311        coverage: RawSceneAttachmentCoverageV1,
312        rows: Vec<RawNodeMeshAttachmentRowV1>,
313    ) -> Self {
314        Self { coverage, rows }
315    }
316    /// Exhaustiveness state.
317    pub const fn coverage(&self) -> RawSceneAttachmentCoverageV1 {
318        self.coverage
319    }
320    /// Canonical source-node-order rows.
321    pub fn rows(&self) -> &[RawNodeMeshAttachmentRowV1] {
322        &self.rows
323    }
324}
325
326/// Coverage-qualified raw mesh primitive declarations.
327#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
328#[serde(deny_unknown_fields)]
329pub struct RawMeshPrimitiveRowsV1 {
330    coverage: RawSceneAttachmentCoverageV1,
331    #[serde(deserialize_with = "deserialize_inventory_vec")]
332    rows: Vec<RawMeshPrimitiveRowV1>,
333}
334impl RawMeshPrimitiveRowsV1 {
335    /// Construct a primitive row set.
336    pub fn new(coverage: RawSceneAttachmentCoverageV1, rows: Vec<RawMeshPrimitiveRowV1>) -> Self {
337        Self { coverage, rows }
338    }
339    /// Exhaustiveness state.
340    pub const fn coverage(&self) -> RawSceneAttachmentCoverageV1 {
341        self.coverage
342    }
343    /// Canonical mesh/primitive-order rows.
344    pub fn rows(&self) -> &[RawMeshPrimitiveRowV1] {
345        &self.rows
346    }
347}
348
349/// Bounded same-load raw scene/node-mesh/primitive inventory.
350#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
351#[serde(deny_unknown_fields)]
352pub struct RawSceneAttachmentInventoryV1 {
353    schema: &'static str,
354    identity: InputIdentity,
355    primary_input: InputIdentity,
356    source_skeleton: RawSourceSkeletonEvidenceV1,
357    scenes: RawSceneRootRowsV1,
358    node_mesh_attachments: RawNodeMeshAttachmentRowsV1,
359    mesh_primitives: RawMeshPrimitiveRowsV1,
360}
361
362fn set_once<T, E>(slot: &mut Option<T>, value: T, field: &'static str) -> Result<(), E>
363where
364    E: serde::de::Error,
365{
366    if slot.replace(value).is_some() {
367        return Err(E::duplicate_field(field));
368    }
369    Ok(())
370}
371
372fn required<T, E>(value: Option<T>, field: &'static str) -> Result<T, E>
373where
374    E: serde::de::Error,
375{
376    value.ok_or_else(|| E::missing_field(field))
377}
378
379enum BudgetedSceneRow {
380    Value(RawSceneRootRowV1),
381    Skipped,
382}
383
384struct BudgetedSceneRowSeed<'a> {
385    budget: &'a mut RowBudget,
386}
387
388impl<'de> DeserializeSeed<'de> for BudgetedSceneRowSeed<'_> {
389    type Value = BudgetedSceneRow;
390
391    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
392    where
393        D: Deserializer<'de>,
394    {
395        if !self.budget.admit() {
396            return IgnoredAny::deserialize(deserializer).map(|_| BudgetedSceneRow::Skipped);
397        }
398
399        #[derive(Deserialize)]
400        #[serde(field_identifier, rename_all = "snake_case")]
401        enum Field {
402            SourceSceneIndex,
403            RootNodeIndices,
404        }
405
406        struct SceneRowVisitor<'a> {
407            budget: &'a mut RowBudget,
408        }
409
410        impl<'de> Visitor<'de> for SceneRowVisitor<'_> {
411            type Value = RawSceneRootRowV1;
412
413            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
414                formatter.write_str("one raw scene-root row")
415            }
416
417            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
418            where
419                A: MapAccess<'de>,
420            {
421                let mut source_scene_index = None;
422                let mut root_node_indices = None;
423                while let Some(field) = map.next_key()? {
424                    match field {
425                        Field::SourceSceneIndex => set_once(
426                            &mut source_scene_index,
427                            map.next_value()?,
428                            "source_scene_index",
429                        )?,
430                        Field::RootNodeIndices => {
431                            if root_node_indices.is_some() {
432                                return Err(A::Error::duplicate_field("root_node_indices"));
433                            }
434                            root_node_indices =
435                                Some(map.next_value_seed(BudgetedCappedSequenceSeed {
436                                    budget: self.budget,
437                                    local_limit: RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS,
438                                    element: PhantomData,
439                                })?);
440                        }
441                    }
442                }
443                let roots: CappedSequence<u64> = required(root_node_indices, "root_node_indices")?;
444                if roots.overflowed {
445                    return Err(A::Error::custom(
446                        "raw scene/attachment inventory exceeded its row bound",
447                    ));
448                }
449                Ok(RawSceneRootRowV1 {
450                    source_scene_index: required(source_scene_index, "source_scene_index")?,
451                    root_node_indices: roots.values,
452                })
453            }
454        }
455
456        deserializer
457            .deserialize_struct(
458                "RawSceneRootRowV1",
459                &["source_scene_index", "root_node_indices"],
460                SceneRowVisitor {
461                    budget: self.budget,
462                },
463            )
464            .map(BudgetedSceneRow::Value)
465    }
466}
467
468struct BudgetedSceneRowsSeed<'a> {
469    budget: &'a mut RowBudget,
470}
471
472impl<'de> DeserializeSeed<'de> for BudgetedSceneRowsSeed<'_> {
473    type Value = CappedSequence<RawSceneRootRowV1>;
474
475    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
476    where
477        D: Deserializer<'de>,
478    {
479        struct RowsVisitor<'a> {
480            budget: &'a mut RowBudget,
481        }
482
483        impl<'de> Visitor<'de> for RowsVisitor<'_> {
484            type Value = CappedSequence<RawSceneRootRowV1>;
485
486            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
487                formatter.write_str("a bounded sequence of raw scene-root rows")
488            }
489
490            fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
491            where
492                A: SeqAccess<'de>,
493            {
494                let mut values = Vec::with_capacity(
495                    sequence
496                        .size_hint()
497                        .unwrap_or(0)
498                        .min(RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS),
499                );
500                let mut seen = 0usize;
501                while seen < RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS {
502                    let Some(row) = sequence.next_element_seed(BudgetedSceneRowSeed {
503                        budget: self.budget,
504                    })?
505                    else {
506                        return Ok(CappedSequence {
507                            values,
508                            overflowed: false,
509                        });
510                    };
511                    seen += 1;
512                    match row {
513                        BudgetedSceneRow::Value(row) => values.push(row),
514                        BudgetedSceneRow::Skipped => {
515                            let overflowed = consume_ignored_tail(
516                                &mut sequence,
517                                seen,
518                                RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS,
519                            )?;
520                            return Ok(CappedSequence { values, overflowed });
521                        }
522                    }
523                }
524                let overflowed = consume_ignored_tail(
525                    &mut sequence,
526                    seen,
527                    RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS,
528                )?;
529                Ok(CappedSequence { values, overflowed })
530            }
531        }
532
533        deserializer.deserialize_seq(RowsVisitor {
534            budget: self.budget,
535        })
536    }
537}
538
539struct RawSceneRowsSetSeed<'a> {
540    budget: &'a mut RowBudget,
541}
542
543impl<'de> DeserializeSeed<'de> for RawSceneRowsSetSeed<'_> {
544    type Value = RawSceneRootRowsV1;
545
546    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
547    where
548        D: Deserializer<'de>,
549    {
550        #[derive(Deserialize)]
551        #[serde(field_identifier, rename_all = "snake_case")]
552        enum Field {
553            Coverage,
554            Rows,
555        }
556        struct SetVisitor<'a> {
557            budget: &'a mut RowBudget,
558        }
559        impl<'de> Visitor<'de> for SetVisitor<'_> {
560            type Value = RawSceneRootRowsV1;
561            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
562                formatter.write_str("coverage-qualified raw scene-root rows")
563            }
564            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
565            where
566                A: MapAccess<'de>,
567            {
568                let mut coverage = None;
569                let mut rows = None;
570                while let Some(field) = map.next_key()? {
571                    match field {
572                        Field::Coverage => set_once(&mut coverage, map.next_value()?, "coverage")?,
573                        Field::Rows => {
574                            if rows.is_some() {
575                                return Err(A::Error::duplicate_field("rows"));
576                            }
577                            rows = Some(map.next_value_seed(BudgetedSceneRowsSeed {
578                                budget: self.budget,
579                            })?);
580                        }
581                    }
582                }
583                let rows: CappedSequence<_> = required(rows, "rows")?;
584                if rows.overflowed {
585                    return Err(A::Error::custom(
586                        "raw scene/attachment inventory exceeded its row bound",
587                    ));
588                }
589                Ok(RawSceneRootRowsV1 {
590                    coverage: required(coverage, "coverage")?,
591                    rows: rows.values,
592                })
593            }
594        }
595        deserializer.deserialize_struct(
596            "RawSceneRootRowsV1",
597            &["coverage", "rows"],
598            SetVisitor {
599                budget: self.budget,
600            },
601        )
602    }
603}
604
605trait RawRowsSet: Sized {
606    type Row;
607    const NAME: &'static str;
608    fn from_parts(coverage: RawSceneAttachmentCoverageV1, rows: Vec<Self::Row>) -> Self;
609}
610
611impl RawRowsSet for RawNodeMeshAttachmentRowsV1 {
612    type Row = RawNodeMeshAttachmentRowV1;
613    const NAME: &'static str = "RawNodeMeshAttachmentRowsV1";
614    fn from_parts(coverage: RawSceneAttachmentCoverageV1, rows: Vec<Self::Row>) -> Self {
615        Self { coverage, rows }
616    }
617}
618
619impl RawRowsSet for RawMeshPrimitiveRowsV1 {
620    type Row = RawMeshPrimitiveRowV1;
621    const NAME: &'static str = "RawMeshPrimitiveRowsV1";
622    fn from_parts(coverage: RawSceneAttachmentCoverageV1, rows: Vec<Self::Row>) -> Self {
623        Self { coverage, rows }
624    }
625}
626
627struct RawRowsSetSeed<'a, S> {
628    budget: &'a mut RowBudget,
629    set: PhantomData<fn() -> S>,
630}
631
632impl<'de, S> DeserializeSeed<'de> for RawRowsSetSeed<'_, S>
633where
634    S: RawRowsSet,
635    S::Row: Deserialize<'de>,
636{
637    type Value = S;
638
639    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
640    where
641        D: Deserializer<'de>,
642    {
643        #[derive(Deserialize)]
644        #[serde(field_identifier, rename_all = "snake_case")]
645        enum Field {
646            Coverage,
647            Rows,
648        }
649        struct SetVisitor<'a, S> {
650            budget: &'a mut RowBudget,
651            set: PhantomData<fn() -> S>,
652        }
653        impl<'de, S> Visitor<'de> for SetVisitor<'_, S>
654        where
655            S: RawRowsSet,
656            S::Row: Deserialize<'de>,
657        {
658            type Value = S;
659            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
660                write!(formatter, "coverage-qualified {} rows", S::NAME)
661            }
662            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
663            where
664                A: MapAccess<'de>,
665            {
666                let mut coverage = None;
667                let mut rows = None;
668                while let Some(field) = map.next_key()? {
669                    match field {
670                        Field::Coverage => set_once(&mut coverage, map.next_value()?, "coverage")?,
671                        Field::Rows => {
672                            if rows.is_some() {
673                                return Err(A::Error::duplicate_field("rows"));
674                            }
675                            rows = Some(map.next_value_seed(BudgetedCappedSequenceSeed {
676                                budget: self.budget,
677                                local_limit: RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS,
678                                element: PhantomData,
679                            })?);
680                        }
681                    }
682                }
683                let rows: CappedSequence<S::Row> = required(rows, "rows")?;
684                if rows.overflowed {
685                    return Err(A::Error::custom(
686                        "raw scene/attachment inventory exceeded its row bound",
687                    ));
688                }
689                Ok(S::from_parts(required(coverage, "coverage")?, rows.values))
690            }
691        }
692        deserializer.deserialize_struct(
693            S::NAME,
694            &["coverage", "rows"],
695            SetVisitor {
696                budget: self.budget,
697                set: PhantomData,
698            },
699        )
700    }
701}
702
703impl<'de> Deserialize<'de> for RawSceneAttachmentInventoryV1 {
704    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
705    where
706        D: Deserializer<'de>,
707    {
708        #[derive(Deserialize)]
709        #[serde(field_identifier, rename_all = "snake_case")]
710        enum Field {
711            Schema,
712            Identity,
713            PrimaryInput,
714            SourceSkeleton,
715            Scenes,
716            NodeMeshAttachments,
717            MeshPrimitives,
718        }
719        struct InventoryVisitor;
720        impl<'de> Visitor<'de> for InventoryVisitor {
721            type Value = (
722                String,
723                InputIdentity,
724                InputIdentity,
725                RawSourceSkeletonEvidenceV1,
726                RawSceneRootRowsV1,
727                RawNodeMeshAttachmentRowsV1,
728                RawMeshPrimitiveRowsV1,
729                usize,
730                bool,
731            );
732            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
733                formatter.write_str("a bounded raw scene/attachment inventory")
734            }
735            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
736            where
737                A: MapAccess<'de>,
738            {
739                let mut budget = RowBudget::new(RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS);
740                let mut schema = None;
741                let mut identity = None;
742                let mut primary_input = None;
743                let mut source_skeleton = None;
744                let mut scenes = None;
745                let mut node_mesh_attachments = None;
746                let mut mesh_primitives = None;
747                while let Some(field) = map.next_key()? {
748                    match field {
749                        Field::Schema => set_once(&mut schema, map.next_value()?, "schema")?,
750                        Field::Identity => set_once(&mut identity, map.next_value()?, "identity")?,
751                        Field::PrimaryInput => {
752                            set_once(&mut primary_input, map.next_value()?, "primary_input")?
753                        }
754                        Field::SourceSkeleton => {
755                            set_once(&mut source_skeleton, map.next_value()?, "source_skeleton")?
756                        }
757                        Field::Scenes => {
758                            if scenes.is_some() {
759                                return Err(A::Error::duplicate_field("scenes"));
760                            }
761                            scenes = Some(map.next_value_seed(RawSceneRowsSetSeed {
762                                budget: &mut budget,
763                            })?);
764                        }
765                        Field::NodeMeshAttachments => {
766                            if node_mesh_attachments.is_some() {
767                                return Err(A::Error::duplicate_field("node_mesh_attachments"));
768                            }
769                            node_mesh_attachments = Some(map.next_value_seed(RawRowsSetSeed {
770                                budget: &mut budget,
771                                set: PhantomData,
772                            })?);
773                        }
774                        Field::MeshPrimitives => {
775                            if mesh_primitives.is_some() {
776                                return Err(A::Error::duplicate_field("mesh_primitives"));
777                            }
778                            mesh_primitives = Some(map.next_value_seed(RawRowsSetSeed {
779                                budget: &mut budget,
780                                set: PhantomData,
781                            })?);
782                        }
783                    }
784                }
785                Ok((
786                    required(schema, "schema")?,
787                    required(identity, "identity")?,
788                    required(primary_input, "primary_input")?,
789                    required(source_skeleton, "source_skeleton")?,
790                    required(scenes, "scenes")?,
791                    required(node_mesh_attachments, "node_mesh_attachments")?,
792                    required(mesh_primitives, "mesh_primitives")?,
793                    budget.found(),
794                    budget.overflowed(),
795                ))
796            }
797        }
798        let (
799            schema,
800            identity,
801            primary_input,
802            source_skeleton,
803            scenes,
804            node_mesh_attachments,
805            mesh_primitives,
806            found,
807            overflowed,
808        ) = deserializer.deserialize_struct(
809            "RawSceneAttachmentInventoryV1",
810            &[
811                "schema",
812                "identity",
813                "primary_input",
814                "source_skeleton",
815                "scenes",
816                "node_mesh_attachments",
817                "mesh_primitives",
818            ],
819            InventoryVisitor,
820        )?;
821        if overflowed {
822            return Err(D::Error::custom(
823                RawSceneAttachmentInventoryError::TooManyRows {
824                    found,
825                    limit: RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS,
826                },
827            ));
828        }
829        if schema != RAW_SCENE_ATTACHMENT_INVENTORY_V1_ID {
830            return Err(serde::de::Error::custom(format!(
831                "raw scene/attachment inventory schema must be {RAW_SCENE_ATTACHMENT_INVENTORY_V1_ID:?}"
832            )));
833        }
834        let value = Self {
835            schema: RAW_SCENE_ATTACHMENT_INVENTORY_V1_ID,
836            identity,
837            primary_input,
838            source_skeleton,
839            scenes,
840            node_mesh_attachments,
841            mesh_primitives,
842        };
843        value.validate().map_err(serde::de::Error::custom)?;
844        let expected = inventory_identity(
845            &value.primary_input,
846            value.source_skeleton,
847            &value.scenes,
848            &value.node_mesh_attachments,
849            &value.mesh_primitives,
850        )
851        .map_err(serde::de::Error::custom)?;
852        if value.identity != expected {
853            return Err(serde::de::Error::custom(
854                "raw scene/attachment inventory identity does not match its contents",
855            ));
856        }
857        Ok(value)
858    }
859}
860impl RawSceneAttachmentInventoryV1 {
861    /// Construct and validate a bounded same-load inventory.
862    pub fn new(
863        primary_input: InputIdentity,
864        source_skeleton: RawSourceSkeletonEvidenceV1,
865        scenes: RawSceneRootRowsV1,
866        node_mesh_attachments: RawNodeMeshAttachmentRowsV1,
867        mesh_primitives: RawMeshPrimitiveRowsV1,
868    ) -> Result<Self, RawSceneAttachmentInventoryError> {
869        let identity = inventory_identity(
870            &primary_input,
871            source_skeleton,
872            &scenes,
873            &node_mesh_attachments,
874            &mesh_primitives,
875        )?;
876        let value = Self {
877            schema: RAW_SCENE_ATTACHMENT_INVENTORY_V1_ID,
878            identity,
879            primary_input,
880            source_skeleton,
881            scenes,
882            node_mesh_attachments,
883            mesh_primitives,
884        };
885        value.validate()?;
886        Ok(value)
887    }
888    /// Semantic inventory identifier.
889    pub const fn contract_id(&self) -> &'static str {
890        self.schema
891    }
892    /// Canonical identity over the complete bounded inventory contract.
893    pub const fn identity(&self) -> &InputIdentity {
894        &self.identity
895    }
896    /// Exact primary input identity for this loader pass.
897    pub const fn primary_input(&self) -> &InputIdentity {
898        &self.primary_input
899    }
900    /// Same-load source-skeleton evidence.
901    pub const fn source_skeleton(&self) -> &RawSourceSkeletonEvidenceV1 {
902        &self.source_skeleton
903    }
904    /// Coverage-qualified source-scene rows.
905    pub const fn scenes(&self) -> &RawSceneRootRowsV1 {
906        &self.scenes
907    }
908    /// Coverage-qualified node-to-mesh rows.
909    pub const fn node_mesh_attachments(&self) -> &RawNodeMeshAttachmentRowsV1 {
910        &self.node_mesh_attachments
911    }
912    /// Coverage-qualified primitive definition rows.
913    pub const fn mesh_primitives(&self) -> &RawMeshPrimitiveRowsV1 {
914        &self.mesh_primitives
915    }
916    fn validate(&self) -> Result<(), RawSceneAttachmentInventoryError> {
917        validate_unavailable_rows("scenes", self.scenes.coverage, self.scenes.rows.len())?;
918        validate_unavailable_rows(
919            "node_mesh_attachments",
920            self.node_mesh_attachments.coverage,
921            self.node_mesh_attachments.rows.len(),
922        )?;
923        validate_unavailable_rows(
924            "mesh_primitives",
925            self.mesh_primitives.coverage,
926            self.mesh_primitives.rows.len(),
927        )?;
928        if self.source_skeleton.coverage == RawSceneAttachmentCoverageV1::Unavailable
929            && (self.source_skeleton.source_node_count != 0
930                || self.source_skeleton.source_skin_count != 0)
931        {
932            return Err(RawSceneAttachmentInventoryError::UnavailableSkeletonHasCounts);
933        }
934        let rows = self
935            .scenes
936            .rows
937            .iter()
938            .try_fold(0usize, |total, row| {
939                total
940                    .checked_add(1)?
941                    .checked_add(row.root_node_indices.len())
942            })
943            .and_then(|rows| rows.checked_add(self.node_mesh_attachments.rows.len()))
944            .and_then(|rows| rows.checked_add(self.mesh_primitives.rows.len()))
945            .ok_or(RawSceneAttachmentInventoryError::AggregateRowsOverflow)?;
946        if rows > RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS {
947            return Err(RawSceneAttachmentInventoryError::TooManyRows {
948                found: rows,
949                limit: RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS,
950            });
951        }
952        for (expected, row) in self.scenes.rows.iter().enumerate() {
953            if row.source_scene_index != expected as u64 {
954                return Err(RawSceneAttachmentInventoryError::NonCanonicalSceneOrder {
955                    expected: expected as u64,
956                    found: row.source_scene_index,
957                });
958            }
959        }
960        if self.source_skeleton.coverage == RawSceneAttachmentCoverageV1::Complete {
961            for scene in &self.scenes.rows {
962                for &node in &scene.root_node_indices {
963                    if node >= self.source_skeleton.source_node_count {
964                        return Err(RawSceneAttachmentInventoryError::SceneRootNodeOutOfRange {
965                            node,
966                            node_count: self.source_skeleton.source_node_count,
967                        });
968                    }
969                }
970            }
971            for attachment in &self.node_mesh_attachments.rows {
972                if attachment.source_node_index >= self.source_skeleton.source_node_count {
973                    return Err(RawSceneAttachmentInventoryError::AttachmentNodeOutOfRange {
974                        node: attachment.source_node_index,
975                        node_count: self.source_skeleton.source_node_count,
976                    });
977                }
978            }
979        }
980        if self
981            .node_mesh_attachments
982            .rows
983            .windows(2)
984            .any(|rows| rows[0].source_node_index >= rows[1].source_node_index)
985        {
986            return Err(RawSceneAttachmentInventoryError::NonCanonicalAttachmentOrder);
987        }
988        if self.mesh_primitives.rows.windows(2).any(|rows| {
989            (rows[0].source_mesh_index, rows[0].source_primitive_index)
990                >= (rows[1].source_mesh_index, rows[1].source_primitive_index)
991        }) {
992            return Err(RawSceneAttachmentInventoryError::NonCanonicalPrimitiveOrder);
993        }
994        if self.mesh_primitives.coverage == RawSceneAttachmentCoverageV1::Complete {
995            let mut current_mesh = None;
996            let mut expected_primitive = 0;
997            for primitive in &self.mesh_primitives.rows {
998                if current_mesh != Some(primitive.source_mesh_index) {
999                    current_mesh = Some(primitive.source_mesh_index);
1000                    expected_primitive = 0;
1001                }
1002                if primitive.source_primitive_index != expected_primitive {
1003                    return Err(
1004                        RawSceneAttachmentInventoryError::NonContiguousPrimitiveOrdinal {
1005                            mesh: primitive.source_mesh_index,
1006                            expected: expected_primitive,
1007                            found: primitive.source_primitive_index,
1008                        },
1009                    );
1010                }
1011                expected_primitive = expected_primitive.saturating_add(1);
1012            }
1013        }
1014        Ok(())
1015    }
1016}
1017
1018fn validate_unavailable_rows(
1019    domain: &'static str,
1020    coverage: RawSceneAttachmentCoverageV1,
1021    rows: usize,
1022) -> Result<(), RawSceneAttachmentInventoryError> {
1023    if coverage == RawSceneAttachmentCoverageV1::Unavailable && rows != 0 {
1024        return Err(RawSceneAttachmentInventoryError::UnavailableHasRows { domain, rows });
1025    }
1026    Ok(())
1027}
1028
1029#[derive(Serialize)]
1030struct InventoryIdentityFields<'a> {
1031    schema: &'static str,
1032    primary_input: &'a InputIdentity,
1033    source_skeleton: RawSourceSkeletonEvidenceV1,
1034    scenes: &'a RawSceneRootRowsV1,
1035    node_mesh_attachments: &'a RawNodeMeshAttachmentRowsV1,
1036    mesh_primitives: &'a RawMeshPrimitiveRowsV1,
1037}
1038
1039fn inventory_identity(
1040    primary_input: &InputIdentity,
1041    source_skeleton: RawSourceSkeletonEvidenceV1,
1042    scenes: &RawSceneRootRowsV1,
1043    node_mesh_attachments: &RawNodeMeshAttachmentRowsV1,
1044    mesh_primitives: &RawMeshPrimitiveRowsV1,
1045) -> Result<InputIdentity, RawSceneAttachmentInventoryError> {
1046    let bytes = serde_json::to_vec(&InventoryIdentityFields {
1047        schema: RAW_SCENE_ATTACHMENT_INVENTORY_V1_ID,
1048        primary_input,
1049        source_skeleton,
1050        scenes,
1051        node_mesh_attachments,
1052        mesh_primitives,
1053    })
1054    .map_err(|error| RawSceneAttachmentInventoryError::IdentityEncoding {
1055        message: error.to_string(),
1056    })?;
1057    Ok(InputIdentity::from_bytes(&bytes))
1058}
1059
1060/// Invalid raw scene/attachment inventory.
1061#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1062#[non_exhaustive]
1063pub enum RawSceneAttachmentInventoryError {
1064    /// The canonical identity encoding could not be produced.
1065    #[error("raw scene/attachment inventory identity encoding failed: {message}")]
1066    IdentityEncoding {
1067        /// Serializer diagnostic.
1068        message: String,
1069    },
1070    /// An unavailable row domain retained rows.
1071    #[error("raw scene/attachment unavailable {domain} retained {rows} row(s)")]
1072    UnavailableHasRows {
1073        /// Affected row domain.
1074        domain: &'static str,
1075        /// Retained row count.
1076        rows: usize,
1077    },
1078    /// Unavailable source-skeleton evidence retained cardinality facts.
1079    #[error("raw scene/attachment unavailable source skeleton retained nonzero counts")]
1080    UnavailableSkeletonHasCounts,
1081    /// Aggregate retained-row addition overflowed.
1082    #[error("raw scene/attachment aggregate row count overflowed")]
1083    AggregateRowsOverflow,
1084    /// Aggregate retained rows exceed the V1 limit.
1085    #[error("raw scene/attachment inventory retained {found} rows, exceeding V1 limit {limit}")]
1086    TooManyRows {
1087        /// Retained rows.
1088        found: usize,
1089        /// V1 limit.
1090        limit: usize,
1091    },
1092    /// Scene rows are not a source-order prefix.
1093    #[error("raw scene rows are not in canonical source order: expected {expected}, found {found}")]
1094    NonCanonicalSceneOrder {
1095        /// Required index.
1096        expected: u64,
1097        /// Retained index.
1098        found: u64,
1099    },
1100    /// Attachment rows are not strictly source-node ordered.
1101    #[error("raw node-to-mesh attachment rows are not in canonical source-node order")]
1102    NonCanonicalAttachmentOrder,
1103    /// A scene root references a node outside complete source-skeleton evidence.
1104    #[error("raw scene root node {node} is outside source node count {node_count}")]
1105    SceneRootNodeOutOfRange {
1106        /// Referenced source node.
1107        node: u64,
1108        /// Complete source-node count.
1109        node_count: u64,
1110    },
1111    /// An attachment references a node outside complete source-skeleton evidence.
1112    #[error("raw attachment node {node} is outside source node count {node_count}")]
1113    AttachmentNodeOutOfRange {
1114        /// Referenced source node.
1115        node: u64,
1116        /// Complete source-node count.
1117        node_count: u64,
1118    },
1119    /// Primitive rows are not strictly mesh/primitive ordered.
1120    #[error("raw mesh primitive rows are not in canonical mesh/primitive order")]
1121    NonCanonicalPrimitiveOrder,
1122    /// A complete primitive row set skipped an ordinal inside one mesh.
1123    #[error(
1124        "raw mesh {mesh} primitive order is not contiguous: expected {expected}, found {found}"
1125    )]
1126    NonContiguousPrimitiveOrdinal {
1127        /// Source mesh index.
1128        mesh: u64,
1129        /// Required primitive ordinal.
1130        expected: u64,
1131        /// Retained primitive ordinal.
1132        found: u64,
1133    },
1134}
1135
1136#[cfg(test)]
1137mod tests {
1138    use super::*;
1139    #[test]
1140    fn empty_complete_sets_prove_absence() {
1141        let inventory = RawSceneAttachmentInventoryV1::new(
1142            InputIdentity::from_bytes(b"raw"),
1143            RawSourceSkeletonEvidenceV1::new(RawSceneAttachmentCoverageV1::Complete, 0, 0),
1144            RawSceneRootRowsV1::new(RawSceneAttachmentCoverageV1::Complete, vec![]),
1145            RawNodeMeshAttachmentRowsV1::new(RawSceneAttachmentCoverageV1::Complete, vec![]),
1146            RawMeshPrimitiveRowsV1::new(RawSceneAttachmentCoverageV1::Complete, vec![]),
1147        )
1148        .unwrap();
1149        assert!(inventory.mesh_primitives().coverage().proves_absence());
1150    }
1151    #[test]
1152    fn rejects_noncanonical_primitives() {
1153        let error = RawSceneAttachmentInventoryV1::new(
1154            InputIdentity::from_bytes(b"raw"),
1155            RawSourceSkeletonEvidenceV1::new(RawSceneAttachmentCoverageV1::Complete, 0, 0),
1156            RawSceneRootRowsV1::new(RawSceneAttachmentCoverageV1::Complete, vec![]),
1157            RawNodeMeshAttachmentRowsV1::new(RawSceneAttachmentCoverageV1::Complete, vec![]),
1158            RawMeshPrimitiveRowsV1::new(
1159                RawSceneAttachmentCoverageV1::Complete,
1160                vec![
1161                    RawMeshPrimitiveRowV1::new(1, 0, RawPrimitiveTopologyV1::Triangles, None),
1162                    RawMeshPrimitiveRowV1::new(0, 0, RawPrimitiveTopologyV1::Points, Some(2)),
1163                ],
1164            ),
1165        )
1166        .unwrap_err();
1167        assert_eq!(
1168            error,
1169            RawSceneAttachmentInventoryError::NonCanonicalPrimitiveOrder
1170        );
1171    }
1172
1173    #[test]
1174    fn rejects_unavailable_rows_and_skeleton_counts() {
1175        let identity = InputIdentity::from_bytes(b"raw");
1176        let error = RawSceneAttachmentInventoryV1::new(
1177            identity.clone(),
1178            RawSourceSkeletonEvidenceV1::new(RawSceneAttachmentCoverageV1::Unavailable, 0, 0),
1179            RawSceneRootRowsV1::new(
1180                RawSceneAttachmentCoverageV1::Unavailable,
1181                vec![RawSceneRootRowV1::new(0, vec![])],
1182            ),
1183            RawNodeMeshAttachmentRowsV1::new(RawSceneAttachmentCoverageV1::Unavailable, vec![]),
1184            RawMeshPrimitiveRowsV1::new(RawSceneAttachmentCoverageV1::Unavailable, vec![]),
1185        )
1186        .unwrap_err();
1187        assert!(matches!(
1188            error,
1189            RawSceneAttachmentInventoryError::UnavailableHasRows {
1190                domain: "scenes",
1191                rows: 1
1192            }
1193        ));
1194
1195        let error = RawSceneAttachmentInventoryV1::new(
1196            identity,
1197            RawSourceSkeletonEvidenceV1::new(RawSceneAttachmentCoverageV1::Unavailable, 1, 0),
1198            RawSceneRootRowsV1::new(RawSceneAttachmentCoverageV1::Unavailable, vec![]),
1199            RawNodeMeshAttachmentRowsV1::new(RawSceneAttachmentCoverageV1::Unavailable, vec![]),
1200            RawMeshPrimitiveRowsV1::new(RawSceneAttachmentCoverageV1::Unavailable, vec![]),
1201        )
1202        .unwrap_err();
1203        assert_eq!(
1204            error,
1205            RawSceneAttachmentInventoryError::UnavailableSkeletonHasCounts
1206        );
1207    }
1208
1209    #[test]
1210    fn deserialization_stops_at_each_raw_inventory_n_plus_one_boundary() {
1211        let roots = serde_json::json!({
1212            "source_scene_index": 0,
1213            "root_node_indices": vec![0_u64; RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS + 1]
1214        });
1215        assert!(serde_json::from_value::<RawSceneRootRowV1>(roots).is_err());
1216
1217        let attachments = serde_json::json!({
1218            "coverage": "prefix_overflow",
1219            "rows": (0..=RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS)
1220                .map(|index| serde_json::json!({
1221                    "source_node_index": index,
1222                    "source_mesh_index": 0
1223                }))
1224                .collect::<Vec<_>>()
1225        });
1226        assert!(serde_json::from_value::<RawNodeMeshAttachmentRowsV1>(attachments).is_err());
1227    }
1228
1229    #[test]
1230    fn inventory_deserialization_enforces_nested_scene_roots_against_aggregate_budget() {
1231        let inventory = RawSceneAttachmentInventoryV1::new(
1232            InputIdentity::from_bytes(b"raw"),
1233            RawSourceSkeletonEvidenceV1::new(RawSceneAttachmentCoverageV1::Complete, 1, 0),
1234            RawSceneRootRowsV1::new(
1235                RawSceneAttachmentCoverageV1::Complete,
1236                vec![RawSceneRootRowV1::new(
1237                    0,
1238                    vec![0; RAW_SCENE_ATTACHMENT_INVENTORY_V1_MAX_ROWS - 1],
1239                )],
1240            ),
1241            RawNodeMeshAttachmentRowsV1::new(RawSceneAttachmentCoverageV1::Complete, vec![]),
1242            RawMeshPrimitiveRowsV1::new(RawSceneAttachmentCoverageV1::Complete, vec![]),
1243        )
1244        .expect("one scene row plus N-1 roots consumes the exact aggregate budget");
1245        let exact = serde_json::to_string(&inventory).unwrap();
1246        let decoded: RawSceneAttachmentInventoryV1 = serde_json::from_str(&exact).unwrap();
1247        assert_eq!(decoded, inventory);
1248
1249        let mut overflow: serde_json::Value = serde_json::from_str(&exact).unwrap();
1250        overflow["scenes"]["rows"][0]["root_node_indices"]
1251            .as_array_mut()
1252            .unwrap()
1253            .push(serde_json::Value::Null);
1254        let error = serde_json::from_str::<RawSceneAttachmentInventoryV1>(
1255            &serde_json::to_string(&overflow).unwrap(),
1256        )
1257        .unwrap_err()
1258        .to_string();
1259        assert!(
1260            error.contains("retained 4097 rows"),
1261            "aggregate N+1 must win before the hostile tail value is decoded: {error}"
1262        );
1263    }
1264
1265    #[test]
1266    fn rejects_complete_skeleton_node_references_outside_its_evidence() {
1267        let identity = InputIdentity::from_bytes(b"raw");
1268        let error = RawSceneAttachmentInventoryV1::new(
1269            identity.clone(),
1270            RawSourceSkeletonEvidenceV1::new(RawSceneAttachmentCoverageV1::Complete, 1, 0),
1271            RawSceneRootRowsV1::new(
1272                RawSceneAttachmentCoverageV1::Complete,
1273                vec![RawSceneRootRowV1::new(0, vec![1])],
1274            ),
1275            RawNodeMeshAttachmentRowsV1::new(RawSceneAttachmentCoverageV1::Complete, vec![]),
1276            RawMeshPrimitiveRowsV1::new(RawSceneAttachmentCoverageV1::Complete, vec![]),
1277        )
1278        .unwrap_err();
1279        assert!(matches!(
1280            error,
1281            RawSceneAttachmentInventoryError::SceneRootNodeOutOfRange {
1282                node: 1,
1283                node_count: 1
1284            }
1285        ));
1286
1287        let error = RawSceneAttachmentInventoryV1::new(
1288            identity,
1289            RawSourceSkeletonEvidenceV1::new(RawSceneAttachmentCoverageV1::Complete, 1, 0),
1290            RawSceneRootRowsV1::new(RawSceneAttachmentCoverageV1::Complete, vec![]),
1291            RawNodeMeshAttachmentRowsV1::new(
1292                RawSceneAttachmentCoverageV1::Complete,
1293                vec![RawNodeMeshAttachmentRowV1::new(1, 0)],
1294            ),
1295            RawMeshPrimitiveRowsV1::new(RawSceneAttachmentCoverageV1::Complete, vec![]),
1296        )
1297        .unwrap_err();
1298        assert!(matches!(
1299            error,
1300            RawSceneAttachmentInventoryError::AttachmentNodeOutOfRange {
1301                node: 1,
1302                node_count: 1
1303            }
1304        ));
1305    }
1306
1307    #[test]
1308    fn complete_primitive_rows_must_be_contiguous_per_mesh() {
1309        let error = RawSceneAttachmentInventoryV1::new(
1310            InputIdentity::from_bytes(b"raw"),
1311            RawSourceSkeletonEvidenceV1::new(RawSceneAttachmentCoverageV1::Complete, 0, 0),
1312            RawSceneRootRowsV1::new(RawSceneAttachmentCoverageV1::Complete, vec![]),
1313            RawNodeMeshAttachmentRowsV1::new(RawSceneAttachmentCoverageV1::Complete, vec![]),
1314            RawMeshPrimitiveRowsV1::new(
1315                RawSceneAttachmentCoverageV1::Complete,
1316                vec![RawMeshPrimitiveRowV1::new(
1317                    0,
1318                    1,
1319                    RawPrimitiveTopologyV1::Triangles,
1320                    None,
1321                )],
1322            ),
1323        )
1324        .unwrap_err();
1325        assert!(matches!(
1326            error,
1327            RawSceneAttachmentInventoryError::NonContiguousPrimitiveOrdinal {
1328                mesh: 0,
1329                expected: 0,
1330                found: 1,
1331            }
1332        ));
1333    }
1334}