Skip to main content

eredu_core/
inspection.rs

1//! Portable model-artifact inspection results.
2
3use 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/// A readiness result that preserves distinct failure modes.
15#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum InspectionReadiness {
18    /// The inspected artifacts establish this capability.
19    Ready,
20    /// The artifact omits a required component.
21    Missing,
22    /// The selected backend does not implement the combination.
23    Unsupported,
24    /// Relevant artifact data is malformed.
25    Invalid,
26    /// A concrete request is needed before deciding.
27    RequestDependent,
28    /// The check necessarily occurs during preparation or execution.
29    Unverified,
30    /// The capability does not apply.
31    NotApplicable,
32}
33
34/// Mechanism observations required to finalize an admitted inspection report.
35#[derive(Debug, Clone, Copy, Eq, PartialEq)]
36pub struct RealizedInspectionOutcomes {
37    /// The admitted artifact remained authoritative through construction.
38    pub artifact_authority: bool,
39    /// Canonical bindings were published into the constructed module.
40    pub binding: bool,
41    /// Native-independent model construction completed.
42    pub construction: bool,
43    /// The selected communication realization was exercised.
44    pub communication: bool,
45    /// At least one session operation completed and was observed.
46    pub execution: bool,
47    /// Facilities actually observed on the constructed session.
48    pub session: SessionCapabilities,
49}
50
51/// Finalizes readiness from actual construction and execution observations.
52///
53/// Artifact identity and descriptive fields remain those admitted by the
54/// portable report. Readiness is recomputed from realized mechanisms, so a
55/// backend adapter cannot prove realization by replaying artifact inspection.
56pub 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
108/// Assembles the portable portion of a model inspection report from one
109/// authoritative artifact admission and its exact preparation admission.
110///
111/// This is the canonical adapter surface for backend-independent callers: all
112/// readiness derived from artifact headers, architecture capabilities, load
113/// policy, and portable media facilities is recorded here rather than rebuilt
114/// by individual backends or integration adapters.
115pub 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
132/// Records one failed portable artifact inspection without backend policy.
133pub 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/// Severity attached to an inspection issue.
238#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
239#[serde(rename_all = "snake_case")]
240pub enum InspectionSeverity {
241    /// Prevents the stated readiness or requested preparation route.
242    Error,
243    /// Limits a capability without preventing selected preparation.
244    Warning,
245    /// Actionable context that is neither rejection nor warning.
246    Info,
247}
248
249/// Stable machine-readable inspection issue category.
250#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
251#[serde(rename_all = "snake_case")]
252#[non_exhaustive]
253pub enum InspectionIssueCode {
254    /// Artifact path or container structure is invalid.
255    InvalidContainer,
256    /// Configuration or architecture metadata is invalid.
257    InvalidConfiguration,
258    /// Architecture dispatch has no backend implementation.
259    UnsupportedArchitecture,
260    /// A referenced checkpoint shard is absent or contradictory.
261    MissingCheckpointShard,
262    /// A tensor storage encoding cannot be consumed.
263    UnsupportedTensorEncoding,
264    /// An architecture-required tensor is absent.
265    MissingRequiredTensor,
266    /// Tensor aliases or layouts conflict after translation.
267    ConflictingTensorLayout,
268    /// A catalog tensor has the wrong rank or dimensions.
269    TensorShapeMismatch,
270    /// Packed quantization metadata and companion tensors disagree.
271    QuantizationCompanionMismatch,
272    /// Configured layer, attention, or expert geometry is invalid.
273    InvalidLayerOrExpertCount,
274    /// No usable tokenizer is available.
275    MissingTokenizer,
276    /// No checkpoint or sidecar chat template is available.
277    MissingChatTemplate,
278    /// A required multimodal projector is absent or ambiguous.
279    MissingMediaProjector,
280    /// A media processor or its build feature is unavailable.
281    MissingProcessor,
282    /// Requested on-load quantization is incompatible.
283    UnsupportedQuantizationRequest,
284    /// Requested weight-residency route is incompatible.
285    UnsupportedResidencyPolicy,
286    /// Requested parallel topology cannot use this loader.
287    UnsupportedParallelTopology,
288    /// No fail-closed semantic streaming protocol was recognized.
289    UnsupportedSemanticProtocol,
290    /// No fail-closed native-tool protocol was recognized.
291    UnsupportedToolProtocol,
292    /// EOS metadata is absent.
293    MissingEosMetadata,
294    /// Exact binding requires preparation-time module validation.
295    ValidationUnavailableUntilLoad,
296    /// Request data or runtime state still needs validation.
297    RequestSpecificValidation,
298    /// An ordinary local I/O operation failed.
299    Io,
300}
301
302/// One structured diagnostic produced by inspection.
303#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
304pub struct InspectionIssue {
305    /// Stable category for routing and UI behavior.
306    pub code: InspectionIssueCode,
307    /// Diagnostic severity.
308    pub severity: InspectionSeverity,
309    /// Human-readable actionable detail.
310    pub detail: String,
311    /// Relevant artifact or sidecar path.
312    #[serde(default, skip_serializing_if = "Option::is_none")]
313    pub path: Option<PathBuf>,
314    /// Relevant metadata key.
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub metadata_key: Option<String>,
317    /// Relevant logical or physical tensor name.
318    #[serde(skip_serializing_if = "Option::is_none")]
319    pub tensor_name: Option<String>,
320    /// Relevant numeric tensor type code.
321    #[serde(skip_serializing_if = "Option::is_none")]
322    pub tensor_type_code: Option<u32>,
323}
324
325/// One tensor storage encoding observed in artifact headers.
326#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
327pub struct ArtifactTensorEncoding {
328    /// Stable textual representation.
329    pub name: String,
330    /// GGML type code for GGUF encodings.
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub ggml_type_code: Option<u32>,
333}
334
335/// Input modality advertised by the resolved architecture.
336#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
337#[serde(rename_all = "snake_case")]
338pub enum ArtifactModality {
339    /// Text tokens.
340    Text,
341    /// Still images.
342    Image,
343    /// Video frame sequences.
344    Video,
345    /// Audio waveforms or features.
346    Audio,
347}
348
349/// A sidecar or companion requirement discovered during inspection.
350#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
351pub struct InspectionRequirement {
352    /// Machine-readable issue category.
353    pub code: InspectionIssueCode,
354    /// Current readiness of the requirement.
355    pub readiness: InspectionReadiness,
356    /// Human-readable explanation.
357    pub detail: String,
358    /// Expected or selected path.
359    #[serde(skip_serializing_if = "Option::is_none")]
360    pub path: Option<PathBuf>,
361}
362
363/// Structured pre-preparation compatibility report for a local artifact.
364#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
365pub struct ModelInspectionReport {
366    /// Logical architecture and declared captures, generated without backend resources.
367    #[serde(default, skip_serializing_if = "Option::is_none")]
368    pub architecture_descriptor: Option<crate::ArchitectureDescriptor>,
369    /// Capture support under the selected backend execution configuration.
370    #[serde(default, skip_serializing_if = "Option::is_none")]
371    pub observation_support: Option<crate::ObservationSupportReport>,
372    /// Submitted local artifact path.
373    pub path: PathBuf,
374    /// Detected artifact container.
375    pub artifact_format: ArtifactFormat,
376    /// Resolved high-level model family.
377    #[serde(skip_serializing_if = "Option::is_none")]
378    pub model_family: Option<String>,
379    /// Submitted model type or architecture value.
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub architecture: Option<String>,
382    /// GGUF versions observed across validated shards.
383    #[serde(skip_serializing_if = "Option::is_none")]
384    pub gguf_versions: Option<Vec<u32>>,
385    /// Number of checkpoint payload shards.
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub checkpoint_shards: Option<usize>,
388    /// Number of cataloged logical tensors.
389    #[serde(skip_serializing_if = "Option::is_none")]
390    pub tensor_count: Option<usize>,
391    /// Header-only resource accounting.
392    pub resources: ModelResourceProfile,
393    /// Distinct storage encodings observed in headers.
394    pub tensor_encodings: Vec<ArtifactTensorEncoding>,
395    /// Expected input modalities.
396    pub expected_modalities: Vec<ArtifactModality>,
397    /// Container/config/header validity.
398    pub container: InspectionReadiness,
399    /// Whether architecture dispatch selects a backend implementation.
400    pub architecture_support: InspectionReadiness,
401    /// Exact header/catalog binding to that implementation.
402    pub structural_binding: InspectionReadiness,
403    /// Model preparation readiness independent of text sidecars.
404    pub model_loadability: InspectionReadiness,
405    /// Compatibility with backend preparation options.
406    pub requested_load: InspectionReadiness,
407    /// Exact portable preparation admission selected from immutable cold facts.
408    #[serde(skip_serializing_if = "Option::is_none")]
409    pub preparation_admission: Option<PreparationAdmission>,
410    /// Combined model and tokenizer readiness for raw text generation.
411    pub text_generation: InspectionReadiness,
412    /// Tokenizer reconstruction/readiness.
413    pub tokenizer: InspectionReadiness,
414    /// Chat-template availability and parseability.
415    pub chat_template: InspectionReadiness,
416    /// Behavioral structured semantic-streaming readiness.
417    pub semantic_streaming: InspectionReadiness,
418    /// Behavioral native-tool readiness.
419    pub native_tools: InspectionReadiness,
420    /// Processor/projector readiness for non-text modalities.
421    pub multimodal: InspectionReadiness,
422    /// Discovered sidecar and request requirements.
423    pub requirements: Vec<InspectionRequirement>,
424    /// Structured rejection reasons, warnings, and limitations.
425    pub issues: Vec<InspectionIssue>,
426}
427
428impl ModelInspectionReport {
429    /// Creates an initially unverified report for backend-specific enrichment.
430    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    /// Returns whether artifact and requested backend policy passed preflight.
462    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    /// Applies reusable header/catalog facts from one admitted portable inspection.
475    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    /// Records the exact successful cold admission used by later realization.
574    pub fn record_preparation_admission(&mut self, admission: PreparationAdmission) {
575        self.preparation_admission = Some(admission);
576        self.requested_load = InspectionReadiness::Ready;
577    }
578
579    /// Records architecture-owned modality and embedded-drafting facts.
580    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    /// Records one stable portable admission rejection.
593    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    /// Adds a structured issue with an optional artifact path.
621    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
654/// Converts portable modality flags into stable inspection report values.
655pub 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/// Portable availability of optional host-media processors.
668#[derive(Debug, Clone, Copy, Eq, PartialEq)]
669pub struct MediaFeatureAvailability {
670    /// Image and video host processing is linked.
671    pub image: bool,
672    /// Audio host processing is linked.
673    pub audio: bool,
674}
675
676/// Architecture-owned requirement for a GGUF media projector.
677#[derive(Debug, Clone, Copy, Eq, PartialEq)]
678pub enum MediaProjectorRequirement {
679    /// No projector applies.
680    NotApplicable,
681    /// Text remains available when the projector is absent.
682    Optional,
683    /// Model loading requires a projector.
684    Required,
685}
686
687/// Computes portable host-feature readiness for declared modalities.
688pub 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
706/// Records portable processor and host-feature readiness in one report.
707pub 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
740/// Records a validated or absent GGUF projector and returns whether it was selected.
741pub 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}