1use crate::{
4 artifact::ArtifactError, ArtifactFormat, ArtifactInspection, GgufCompanionRole,
5 InputModalities, ModelResourceProfile, Observed, PreparationAdmission,
6 PreparationAdmissionError, SessionCapabilities,
7};
8use serde::{Deserialize, Serialize};
9use std::{
10 collections::{BTreeMap, BTreeSet},
11 path::{Path, PathBuf},
12};
13
14#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum InspectionReadiness {
18 Ready,
20 Missing,
22 Unsupported,
24 Invalid,
26 RequestDependent,
28 Unverified,
30 NotApplicable,
32}
33
34#[derive(Debug, Clone, Copy, Eq, PartialEq)]
36pub struct RealizedInspectionOutcomes {
37 pub artifact_authority: bool,
39 pub binding: bool,
41 pub construction: bool,
43 pub communication: bool,
45 pub execution: bool,
47 pub session: SessionCapabilities,
49}
50
51pub fn finalize_realized_model_inspection(
57 admitted: &ModelInspectionReport,
58 outcomes: RealizedInspectionOutcomes,
59) -> ModelInspectionReport {
60 let mut report = admitted.clone();
61 let admission = admitted.preparation_admission;
62 let session_matches =
63 admission.is_some_and(|value| value.session_capabilities() == outcomes.session);
64 report.container = if outcomes.artifact_authority {
65 InspectionReadiness::Ready
66 } else {
67 InspectionReadiness::Invalid
68 };
69 report.architecture_support = if outcomes.construction {
70 InspectionReadiness::Ready
71 } else {
72 InspectionReadiness::Invalid
73 };
74 report.structural_binding = if outcomes.binding {
75 InspectionReadiness::Ready
76 } else {
77 InspectionReadiness::Invalid
78 };
79 report.model_loadability = if outcomes.artifact_authority
80 && outcomes.binding
81 && outcomes.construction
82 && outcomes.communication
83 && outcomes.execution
84 {
85 InspectionReadiness::Ready
86 } else {
87 InspectionReadiness::Invalid
88 };
89 report.requested_load =
90 if report.model_loadability == InspectionReadiness::Ready && session_matches {
91 InspectionReadiness::Ready
92 } else {
93 InspectionReadiness::Unsupported
94 };
95 if !outcomes.session.activation_inspection() {
96 if let Some(support) = &mut report.observation_support {
97 for point in &mut support.points {
98 point.prefill = crate::ObservationSupportStatus::Unsupported(
99 "Realized session does not support activation inspection".into(),
100 );
101 point.decode = point.prefill.clone();
102 }
103 }
104 }
105 report
106}
107
108pub fn assemble_portable_model_inspection<P>(
116 inspection: &ArtifactInspection<P>,
117 admission: PreparationAdmission,
118 modalities: InputModalities,
119 embedded_draft_layers: Option<usize>,
120 processor: Option<(bool, MediaFeatureAvailability)>,
121) -> ModelInspectionReport {
122 let mut report = ModelInspectionReport::unverified(inspection.path(), inspection.format());
123 report.record_artifact_inspection(inspection);
124 report.record_architecture_capabilities(modalities, embedded_draft_layers);
125 if let Some((has_processor, availability)) = processor {
126 record_processor_inspection(&mut report, has_processor, availability);
127 }
128 report.record_preparation_admission(admission);
129 report
130}
131
132pub fn reject_portable_artifact_inspection(
134 report: &mut ModelInspectionReport,
135 path: &Path,
136 error: &ArtifactError,
137) {
138 let detail = error.to_string();
139 let missing_media_projector = matches!(
140 error,
141 ArtifactError::MissingRequiredGgufCompanion {
142 role: GgufCompanionRole::MediaProjector,
143 ..
144 }
145 );
146 let (code, container, architecture, structural, type_code) = match error {
147 ArtifactError::UnsupportedGgufArchitecture(name) => {
148 report.architecture = Some(name.clone());
149 (
150 InspectionIssueCode::UnsupportedArchitecture,
151 InspectionReadiness::Ready,
152 InspectionReadiness::Unsupported,
153 InspectionReadiness::Unsupported,
154 None,
155 )
156 }
157 ArtifactError::MissingGgufArchitecture => (
158 InspectionIssueCode::InvalidConfiguration,
159 InspectionReadiness::Ready,
160 InspectionReadiness::Invalid,
161 InspectionReadiness::Invalid,
162 None,
163 ),
164 ArtifactError::MissingRequiredGgufCompanion {
165 role: GgufCompanionRole::MediaProjector,
166 ..
167 } => (
168 InspectionIssueCode::MissingMediaProjector,
169 InspectionReadiness::Ready,
170 InspectionReadiness::Ready,
171 InspectionReadiness::Unverified,
172 None,
173 ),
174 ArtifactError::InvalidArtifact(_)
175 | ArtifactError::InvalidArchitecturePlan(_)
176 | ArtifactError::DuplicateTensor(_)
177 | ArtifactError::Catalog(_) => (
178 InspectionIssueCode::InvalidConfiguration,
179 InspectionReadiness::Ready,
180 InspectionReadiness::Unverified,
181 InspectionReadiness::Invalid,
182 None,
183 ),
184 ArtifactError::Gguf(error) => {
185 let type_code = error.unsupported_tensor_type_code();
186 (
187 if type_code.is_some() {
188 InspectionIssueCode::UnsupportedTensorEncoding
189 } else {
190 InspectionIssueCode::InvalidContainer
191 },
192 InspectionReadiness::Invalid,
193 InspectionReadiness::Unverified,
194 InspectionReadiness::Unverified,
195 type_code,
196 )
197 }
198 _ => (
199 InspectionIssueCode::InvalidContainer,
200 InspectionReadiness::Invalid,
201 InspectionReadiness::Unverified,
202 InspectionReadiness::Unverified,
203 None,
204 ),
205 };
206 report.container = container;
207 report.architecture_support = architecture;
208 report.structural_binding = structural;
209 report.model_loadability = if missing_media_projector {
210 InspectionReadiness::Missing
211 } else if architecture == InspectionReadiness::Unsupported {
212 InspectionReadiness::Unsupported
213 } else {
214 InspectionReadiness::Invalid
215 };
216 report.requested_load = report.model_loadability;
217 if missing_media_projector {
218 report.multimodal = InspectionReadiness::Missing;
219 report.requirements.push(InspectionRequirement {
220 code: InspectionIssueCode::MissingMediaProjector,
221 readiness: InspectionReadiness::Missing,
222 detail: detail.clone(),
223 path: None,
224 });
225 }
226 report.issues.push(InspectionIssue {
227 code,
228 severity: InspectionSeverity::Error,
229 detail,
230 path: Some(path.to_path_buf()),
231 metadata_key: None,
232 tensor_name: None,
233 tensor_type_code: type_code,
234 });
235}
236
237#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
239#[serde(rename_all = "snake_case")]
240pub enum InspectionSeverity {
241 Error,
243 Warning,
245 Info,
247}
248
249#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
251#[serde(rename_all = "snake_case")]
252#[non_exhaustive]
253pub enum InspectionIssueCode {
254 InvalidContainer,
256 InvalidConfiguration,
258 UnsupportedArchitecture,
260 MissingCheckpointShard,
262 UnsupportedTensorEncoding,
264 MissingRequiredTensor,
266 ConflictingTensorLayout,
268 TensorShapeMismatch,
270 QuantizationCompanionMismatch,
272 InvalidLayerOrExpertCount,
274 MissingTokenizer,
276 MissingChatTemplate,
278 MissingMediaProjector,
280 MissingProcessor,
282 UnsupportedQuantizationRequest,
284 UnsupportedResidencyPolicy,
286 UnsupportedParallelTopology,
288 UnsupportedSemanticProtocol,
290 UnsupportedToolProtocol,
292 MissingEosMetadata,
294 ValidationUnavailableUntilLoad,
296 RequestSpecificValidation,
298 Io,
300}
301
302#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
304pub struct InspectionIssue {
305 pub code: InspectionIssueCode,
307 pub severity: InspectionSeverity,
309 pub detail: String,
311 #[serde(default, skip_serializing_if = "Option::is_none")]
313 pub path: Option<PathBuf>,
314 #[serde(skip_serializing_if = "Option::is_none")]
316 pub metadata_key: Option<String>,
317 #[serde(skip_serializing_if = "Option::is_none")]
319 pub tensor_name: Option<String>,
320 #[serde(skip_serializing_if = "Option::is_none")]
322 pub tensor_type_code: Option<u32>,
323}
324
325#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
327pub struct ArtifactTensorEncoding {
328 pub name: String,
330 #[serde(skip_serializing_if = "Option::is_none")]
332 pub ggml_type_code: Option<u32>,
333}
334
335#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
337#[serde(rename_all = "snake_case")]
338pub enum ArtifactModality {
339 Text,
341 Image,
343 Video,
345 Audio,
347}
348
349#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
351pub struct InspectionRequirement {
352 pub code: InspectionIssueCode,
354 pub readiness: InspectionReadiness,
356 pub detail: String,
358 #[serde(skip_serializing_if = "Option::is_none")]
360 pub path: Option<PathBuf>,
361}
362
363#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
365pub struct ModelInspectionReport {
366 #[serde(default, skip_serializing_if = "Option::is_none")]
368 pub architecture_descriptor: Option<crate::ArchitectureDescriptor>,
369 #[serde(default, skip_serializing_if = "Option::is_none")]
371 pub observation_support: Option<crate::ObservationSupportReport>,
372 pub path: PathBuf,
374 pub artifact_format: ArtifactFormat,
376 #[serde(skip_serializing_if = "Option::is_none")]
378 pub model_family: Option<String>,
379 #[serde(skip_serializing_if = "Option::is_none")]
381 pub architecture: Option<String>,
382 #[serde(skip_serializing_if = "Option::is_none")]
384 pub gguf_versions: Option<Vec<u32>>,
385 #[serde(skip_serializing_if = "Option::is_none")]
387 pub checkpoint_shards: Option<usize>,
388 #[serde(skip_serializing_if = "Option::is_none")]
390 pub tensor_count: Option<usize>,
391 pub resources: ModelResourceProfile,
393 pub tensor_encodings: Vec<ArtifactTensorEncoding>,
395 pub expected_modalities: Vec<ArtifactModality>,
397 pub container: InspectionReadiness,
399 pub architecture_support: InspectionReadiness,
401 pub structural_binding: InspectionReadiness,
403 pub model_loadability: InspectionReadiness,
405 pub requested_load: InspectionReadiness,
407 #[serde(skip_serializing_if = "Option::is_none")]
409 pub preparation_admission: Option<PreparationAdmission>,
410 pub text_generation: InspectionReadiness,
412 pub tokenizer: InspectionReadiness,
414 pub chat_template: InspectionReadiness,
416 pub semantic_streaming: InspectionReadiness,
418 pub native_tools: InspectionReadiness,
420 pub multimodal: InspectionReadiness,
422 pub requirements: Vec<InspectionRequirement>,
424 pub issues: Vec<InspectionIssue>,
426}
427
428impl ModelInspectionReport {
429 pub fn unverified(path: &Path, artifact_format: ArtifactFormat) -> Self {
431 Self {
432 architecture_descriptor: None,
433 observation_support: None,
434 path: path.to_path_buf(),
435 artifact_format,
436 model_family: None,
437 architecture: None,
438 gguf_versions: None,
439 checkpoint_shards: None,
440 tensor_count: None,
441 resources: ModelResourceProfile::unmeasured(path.to_path_buf(), artifact_format),
442 tensor_encodings: Vec::new(),
443 expected_modalities: Vec::new(),
444 container: InspectionReadiness::Unverified,
445 architecture_support: InspectionReadiness::Unverified,
446 structural_binding: InspectionReadiness::Unverified,
447 model_loadability: InspectionReadiness::Unverified,
448 requested_load: InspectionReadiness::Unverified,
449 preparation_admission: None,
450 text_generation: InspectionReadiness::Unverified,
451 tokenizer: InspectionReadiness::Unverified,
452 chat_template: InspectionReadiness::Unverified,
453 semantic_streaming: InspectionReadiness::Unverified,
454 native_tools: InspectionReadiness::Unverified,
455 multimodal: InspectionReadiness::Unverified,
456 requirements: Vec::new(),
457 issues: Vec::new(),
458 }
459 }
460
461 pub fn is_loadable(&self) -> bool {
463 self.container == InspectionReadiness::Ready
464 && self.architecture_support == InspectionReadiness::Ready
465 && self.structural_binding == InspectionReadiness::Ready
466 && self.model_loadability == InspectionReadiness::Ready
467 && self.requested_load == InspectionReadiness::Ready
468 && !self
469 .issues
470 .iter()
471 .any(|issue| issue.code == InspectionIssueCode::ValidationUnavailableUntilLoad)
472 }
473
474 pub fn record_artifact_inspection<P>(&mut self, inspection: &ArtifactInspection<P>) {
476 self.path = inspection.path().to_owned();
477 self.artifact_format = inspection.format();
478 self.model_family = Some(inspection.configuration().family().to_owned());
479 self.architecture = Some(inspection.configuration().effective_model_type().to_owned());
480 self.container = InspectionReadiness::Ready;
481 self.architecture_support = InspectionReadiness::Ready;
482 self.structural_binding = InspectionReadiness::Ready;
483 self.model_loadability = InspectionReadiness::Ready;
484 self.tensor_count = Some(inspection.tensors().len());
485
486 let (shards, encodings, stored_bytes, largest_bytes, gguf_versions) =
487 if let Some(validated) = inspection.validated_gguf() {
488 let checkpoint = validated.checkpoint();
489 let encodings = checkpoint
490 .tensors()
491 .map(|tensor| tensor.descriptor().ggml_type)
492 .map(|encoding| (encoding.code(), format!("{encoding:?}")))
493 .collect::<BTreeMap<_, _>>()
494 .into_iter()
495 .map(|(code, name)| ArtifactTensorEncoding {
496 name,
497 ggml_type_code: Some(code),
498 })
499 .collect();
500 let mut total = Some(0_u64);
501 let mut largest = 0_u64;
502 for tensor in checkpoint.tensors() {
503 let bytes = tensor.descriptor().byte_len;
504 total = total.and_then(|value| value.checked_add(bytes));
505 largest = largest.max(bytes);
506 }
507 let versions = checkpoint
508 .shards()
509 .iter()
510 .map(|shard| shard.version())
511 .collect::<BTreeSet<_>>()
512 .into_iter()
513 .collect();
514 (
515 checkpoint.shards().len(),
516 encodings,
517 total,
518 largest,
519 Some(versions),
520 )
521 } else {
522 let mut shards = BTreeSet::new();
523 let mut encodings = BTreeSet::new();
524 let mut total = Some(0_u64);
525 let mut largest = 0_u64;
526 for tensor in inspection.tensors().descriptors() {
527 encodings.insert(safetensors_encoding_name(&tensor.dtype));
528 if let Some(storage) = &tensor.storage {
529 shards.insert(storage.member.clone());
530 total = total.and_then(|value| value.checked_add(storage.length));
531 largest = largest.max(storage.length);
532 } else {
533 total = None;
534 }
535 }
536 (
537 shards.len(),
538 encodings
539 .into_iter()
540 .map(|name| ArtifactTensorEncoding {
541 name,
542 ggml_type_code: None,
543 })
544 .collect(),
545 total,
546 largest,
547 None,
548 )
549 };
550 self.checkpoint_shards = Some(shards);
551 self.tensor_encodings = encodings;
552 self.gguf_versions = gguf_versions;
553 self.resources.model_family = self.model_family.clone();
554 self.resources.architecture = self.architecture.clone();
555 self.resources.tensor_count = self.tensor_count;
556 self.resources.checkpoint_shards = self.checkpoint_shards;
557 match stored_bytes {
558 Some(total) => {
559 self.resources.stored_tensor_bytes =
560 Observed::exact(total, "authoritative portable tensor catalog");
561 self.resources.largest_stored_tensor_bytes =
562 Observed::exact(largest_bytes, "authoritative portable tensor catalog");
563 }
564 None => {
565 self.resources.stored_tensor_bytes =
566 Observed::unavailable("portable payload-byte catalog was incomplete");
567 self.resources.largest_stored_tensor_bytes =
568 Observed::unavailable("portable payload-byte catalog was incomplete");
569 }
570 }
571 }
572
573 pub fn record_preparation_admission(&mut self, admission: PreparationAdmission) {
575 self.preparation_admission = Some(admission);
576 self.requested_load = InspectionReadiness::Ready;
577 }
578
579 pub fn record_architecture_capabilities(
581 &mut self,
582 modalities: InputModalities,
583 embedded_draft_layers: Option<usize>,
584 ) {
585 self.expected_modalities = artifact_modalities(modalities);
586 self.resources.embedded_draft_layers = embedded_draft_layers.map_or_else(
587 || Observed::unsupported("artifact convention does not expose embedded drafting"),
588 |layers| Observed::exact(layers, "normalized architecture configuration"),
589 );
590 }
591
592 pub fn reject_preparation_admission(&mut self, error: PreparationAdmissionError) {
594 use PreparationAdmissionError as Admission;
595 self.preparation_admission = None;
596 self.requested_load = InspectionReadiness::Unsupported;
597 let code = match error {
598 Admission::UnsupportedQuantization(_) => {
599 InspectionIssueCode::UnsupportedQuantizationRequest
600 }
601 Admission::UnsupportedResidency(_) | Admission::ArchitectureParameterBanks => {
602 InspectionIssueCode::UnsupportedResidencyPolicy
603 }
604 Admission::ArchitectureParallelAxis(_) | Admission::BackendParallelAxis(_) => {
605 InspectionIssueCode::UnsupportedParallelTopology
606 }
607 Admission::ArchitectureInputModality(_) | Admission::BackendInputModality(_) => {
608 InspectionIssueCode::MissingProcessor
609 }
610 _ => InspectionIssueCode::UnsupportedArchitecture,
611 };
612 self.issue(
613 code,
614 InspectionSeverity::Error,
615 error.to_string(),
616 Some(self.path.clone()),
617 );
618 }
619
620 pub fn issue(
622 &mut self,
623 code: InspectionIssueCode,
624 severity: InspectionSeverity,
625 detail: impl Into<String>,
626 path: Option<PathBuf>,
627 ) {
628 self.issues.push(InspectionIssue {
629 code,
630 severity,
631 detail: detail.into(),
632 path,
633 metadata_key: None,
634 tensor_name: None,
635 tensor_type_code: None,
636 });
637 }
638}
639
640fn safetensors_encoding_name(dtype: &crate::checkpoint::TensorDtype) -> String {
641 use crate::checkpoint::TensorDtype;
642 match dtype {
643 TensorDtype::Bf16 => "BF16".into(),
644 TensorDtype::Complex64 => "C64".into(),
645 TensorDtype::Encoded(name) if name == "F8_E4M3" => "F8E4M3".into(),
646 TensorDtype::Encoded(name) if name == "F4" => "F4".into(),
647 TensorDtype::Encoded(name) if name == "F8_E8M0" => "F8E8M0".into(),
648 TensorDtype::Encoded(name) if name == "F8_E5M2" => "F8E5M2".into(),
649 TensorDtype::Encoded(name) => format!("Other({name:?})"),
650 dtype => format!("{dtype:?}"),
651 }
652}
653
654pub fn artifact_modalities(modalities: InputModalities) -> Vec<ArtifactModality> {
656 [
657 (modalities.text, ArtifactModality::Text),
658 (modalities.image, ArtifactModality::Image),
659 (modalities.video, ArtifactModality::Video),
660 (modalities.audio, ArtifactModality::Audio),
661 ]
662 .into_iter()
663 .filter_map(|(enabled, modality)| enabled.then_some(modality))
664 .collect()
665}
666
667#[derive(Debug, Clone, Copy, Eq, PartialEq)]
669pub struct MediaFeatureAvailability {
670 pub image: bool,
672 pub audio: bool,
674}
675
676#[derive(Debug, Clone, Copy, Eq, PartialEq)]
678pub enum MediaProjectorRequirement {
679 NotApplicable,
681 Optional,
683 Required,
685}
686
687pub fn media_feature_readiness(
689 expected: &[ArtifactModality],
690 availability: MediaFeatureAvailability,
691) -> InspectionReadiness {
692 if expected == [ArtifactModality::Text] {
693 return InspectionReadiness::NotApplicable;
694 }
695 if expected.iter().all(|modality| match modality {
696 ArtifactModality::Text => true,
697 ArtifactModality::Image | ArtifactModality::Video => availability.image,
698 ArtifactModality::Audio => availability.audio,
699 }) {
700 InspectionReadiness::Ready
701 } else {
702 InspectionReadiness::Unsupported
703 }
704}
705
706pub fn record_processor_inspection(
708 report: &mut ModelInspectionReport,
709 has_processor: bool,
710 availability: MediaFeatureAvailability,
711) {
712 let feature_readiness = media_feature_readiness(&report.expected_modalities, availability);
713 if feature_readiness == InspectionReadiness::NotApplicable {
714 report.multimodal = feature_readiness;
715 return;
716 }
717 if has_processor {
718 report.multimodal = feature_readiness;
719 report.requirements.push(InspectionRequirement {
720 code: InspectionIssueCode::MissingProcessor,
721 readiness: feature_readiness,
722 detail: if feature_readiness == InspectionReadiness::Ready {
723 "authoritative processor plan and required host-media features are available".into()
724 } else {
725 "authoritative processor plan is available, but required host-media features are not enabled".into()
726 },
727 path: None,
728 });
729 } else {
730 report.multimodal = InspectionReadiness::Missing;
731 report.issue(
732 InspectionIssueCode::MissingProcessor,
733 InspectionSeverity::Warning,
734 "authoritative architecture inspection admitted no media processor",
735 Some(report.path.clone()),
736 );
737 }
738}
739
740pub fn record_media_projector_inspection(
742 report: &mut ModelInspectionReport,
743 artifact_path: &Path,
744 requirement: MediaProjectorRequirement,
745 projector: Option<PathBuf>,
746) -> bool {
747 match (requirement, projector) {
748 (MediaProjectorRequirement::NotApplicable, _) => {
749 report.multimodal = InspectionReadiness::NotApplicable;
750 false
751 }
752 (_, Some(path)) => {
753 report.requirements.push(InspectionRequirement {
754 code: InspectionIssueCode::MissingMediaProjector,
755 readiness: InspectionReadiness::Ready,
756 detail: "portable admission validated the architecture-declared media projector"
757 .into(),
758 path: Some(path),
759 });
760 true
761 }
762 (MediaProjectorRequirement::Optional, None) => {
763 report.multimodal = InspectionReadiness::Missing;
764 report.requirements.push(InspectionRequirement {
765 code: InspectionIssueCode::MissingMediaProjector,
766 readiness: InspectionReadiness::Missing,
767 detail: "text loading is available, but media input requires an architecture-declared sibling projector GGUF".into(),
768 path: None,
769 });
770 report.issue(
771 InspectionIssueCode::MissingMediaProjector,
772 InspectionSeverity::Warning,
773 "no sibling media projector was admitted; text loading remains available",
774 Some(artifact_path.to_path_buf()),
775 );
776 false
777 }
778 (MediaProjectorRequirement::Required, None) => {
779 report.multimodal = InspectionReadiness::Missing;
780 report.model_loadability = InspectionReadiness::Invalid;
781 report.requested_load = InspectionReadiness::Invalid;
782 report.requirements.push(InspectionRequirement {
783 code: InspectionIssueCode::MissingMediaProjector,
784 readiness: InspectionReadiness::Missing,
785 detail:
786 "architecture preparation requires a validated sibling media projector GGUF"
787 .into(),
788 path: None,
789 });
790 report.issue(
791 InspectionIssueCode::MissingMediaProjector,
792 InspectionSeverity::Error,
793 "portable admission omitted an architecture-required media projector",
794 Some(artifact_path.to_path_buf()),
795 );
796 false
797 }
798 }
799}
800
801#[cfg(test)]
802mod tests {
803 use super::*;
804
805 #[test]
806 fn report_schema_round_trips_without_a_backend() {
807 let report =
808 ModelInspectionReport::unverified(Path::new("model.gguf"), ArtifactFormat::Gguf);
809 let json = serde_json::to_string(&report).unwrap();
810 let decoded: ModelInspectionReport = serde_json::from_str(&json).unwrap();
811 assert_eq!(decoded, report);
812 }
813
814 #[test]
815 fn neutral_admission_rejection_has_a_stable_report_code() {
816 let mut report =
817 ModelInspectionReport::unverified(Path::new("model"), ArtifactFormat::SafeTensors);
818 report.reject_preparation_admission(PreparationAdmissionError::BackendParallelAxis(
819 crate::ParallelAxis::Pipeline,
820 ));
821 assert_eq!(report.requested_load, InspectionReadiness::Unsupported);
822 assert!(report.preparation_admission.is_none());
823 assert_eq!(
824 report.issues[0].code,
825 InspectionIssueCode::UnsupportedParallelTopology
826 );
827 }
828}