latent-inspector 0.1.0

Fast CLI for inspecting and comparing learned representations across self-supervised vision models
Documentation
use crate::errors::ValidationError;
use crate::models::loader::ModelOutput;
use crate::models::preprocess::{preprocess, PreprocessConfig};
use crate::models::registry::RegistryEntry;
use crate::validation::fixtures::{ContractArtifact, LoadedFixtureSet};
use crate::validation::freshness::{preprocess_evidence_freshness, tensor_evidence_freshness};
use crate::validation::report::{CheckSummary, TensorValidationSummary, ValidationStatus};

pub fn evaluate_preprocess_contract(
    entry: &RegistryEntry,
    contract: &ContractArtifact,
    fixture_set: &LoadedFixtureSet,
) -> Result<CheckSummary, ValidationError> {
    let expected = &entry.validation.preprocess;
    let freshness = preprocess_evidence_freshness(entry, contract, fixture_set);
    let mut mismatches = Vec::new();

    let fixture = fixture_set
        .materialize_fixtures()?
        .into_iter()
        .next()
        .ok_or_else(|| {
            ValidationError::MissingFixtures("No validation fixtures available".to_string())
        })?;
    let cfg = PreprocessConfig::new(entry.info.input_size, entry.norm_mean, entry.norm_std);
    let tensor =
        preprocess(&fixture.image, &cfg).map_err(|err| ValidationError::ContractMismatch {
            model: entry.info.name.clone(),
            reason: err.to_string(),
        })?;
    let expected_shape = [
        1,
        3,
        expected.input_size as usize,
        expected.input_size as usize,
    ];
    if tensor.shape() != expected_shape.as_slice() {
        mismatches.push(format!(
            "preprocessed tensor shape mismatch: observed={:?} expected={:?}",
            tensor.shape(),
            expected_shape
        ));
    }

    let pixel = fixture.image.to_rgb8().get_pixel(0, 0).0;
    for (channel, (mean, std)) in expected.mean.iter().zip(expected.std.iter()).enumerate() {
        let raw = pixel[channel] as f32 / 255.0;
        let expected_value = (raw - mean) / std;
        let observed = tensor[[0, channel, 0, 0]];
        if (observed - expected_value).abs() > 1e-5 {
            mismatches.push(format!(
                "channel {} normalization drift: observed={observed:.6} expected={expected_value:.6}",
                channel
            ));
        }
    }

    if mismatches.is_empty() {
        if freshness.is_stale() {
            Ok(CheckSummary::stale(format!(
                "Live preprocessing still matches the current registry contract, but the approved preprocessing evidence is stale: {}.",
                freshness.reasons().join("; ")
            )))
        } else {
            Ok(CheckSummary::validated(
                "Input size, normalization, resize filter, and channel layout match the approved source-model contract.",
            ))
        }
    } else {
        let mut summary = format!(
            "Live preprocessing no longer matches the current registry contract: {}.",
            mismatches.join("; ")
        );
        if freshness.is_stale() {
            summary.push_str(" Approved preprocessing evidence is also stale: ");
            summary.push_str(&freshness.reasons().join("; "));
            summary.push('.');
        }
        Ok(CheckSummary::failed(summary))
    }
}

pub fn evaluate_tensor_semantics(
    entry: &RegistryEntry,
    contract: &ContractArtifact,
    fixture_set: &LoadedFixtureSet,
    output: &ModelOutput,
) -> Vec<TensorValidationSummary> {
    let expected = &entry.validation.tensor;
    let freshness = tensor_evidence_freshness(entry, contract, fixture_set);
    let observed_shape = output.tensor_metadata.output_shape.clone();
    let expected_shape = expected.expected_shape();
    let mut mismatches = Vec::new();

    if output.tensor_metadata.output_name != expected.name {
        mismatches.push(format!(
            "output name mismatch: observed={} expected={}",
            output.tensor_metadata.output_name, expected.name
        ));
    }

    if observed_shape != expected_shape {
        mismatches.push(format!(
            "output shape mismatch: observed={observed_shape:?} expected={expected_shape:?}"
        ));
    }

    if output.tensor_metadata.sequence_has_cls != expected.cls_expected {
        mismatches.push(format!(
            "CLS semantics mismatch: observed={} expected={}",
            output.tensor_metadata.sequence_has_cls, expected.cls_expected
        ));
    }

    if output.tensor_metadata.observed_patch_count != expected.patch_count {
        mismatches.push(format!(
            "patch count mismatch: observed={} expected={}",
            output.tensor_metadata.observed_patch_count, expected.patch_count
        ));
    }

    if output.tensor_metadata.embedding_dim != expected.embedding_dim {
        mismatches.push(format!(
            "embedding width mismatch: observed={} expected={}",
            output.tensor_metadata.embedding_dim, expected.embedding_dim
        ));
    }

    if output.cls_token.is_some() != expected.cls_expected {
        mismatches.push(format!(
            "CLS token presence mismatch: observed={} expected={}",
            output.cls_token.is_some(),
            expected.cls_expected
        ));
    }

    let status = if !mismatches.is_empty() {
        ValidationStatus::Failed
    } else if freshness.is_stale() {
        ValidationStatus::Stale
    } else {
        ValidationStatus::Validated
    };

    let summary = if !mismatches.is_empty() {
        let mut summary = format!(
            "Tensor semantic mismatches detected for {}: {}.",
            entry.info.name,
            mismatches.join("; ")
        );
        if freshness.is_stale() {
            summary.push_str(" Approved tensor evidence is also stale: ");
            summary.push_str(&freshness.reasons().join("; "));
            summary.push('.');
        }
        summary
    } else if freshness.is_stale() {
        format!(
            "Live tensor semantics still match the current registry contract, but the approved tensor evidence is stale: {}.",
            freshness.reasons().join("; ")
        )
    } else {
        format!(
            "{} matches the expected {} contract for {} patch tokens.",
            expected.name, expected.role, expected.patch_count
        )
    };

    vec![TensorValidationSummary {
        name: expected.name.clone(),
        role: expected.role.to_string(),
        status,
        summary,
    }]
}