latent-inspector 0.1.0

Fast CLI for inspecting and comparing learned representations across self-supervised vision models
Documentation
pub(crate) mod evidence;
pub mod fixtures;
pub mod freshness;
pub mod parity;
pub mod report;
pub mod semantics;

use crate::errors::{ModelError, ValidationError};
use crate::models::ModelSession;
use evidence::summarize_registered_evidence;
use fixtures::{build_reference_artifact_id, load_fixture_set, ContractArtifact, LoadedFixtureSet};
use parity::{build_reference_artifact, evaluate_reference_parity, summarize_outputs};
use report::ModelValidationSummary;
use semantics::{evaluate_preprocess_contract, evaluate_tensor_semantics};

pub use fixtures::{
    ContractFixture, FixturePattern, FixtureSignalSummary, MaterializedFixture, ReferenceArtifact,
    ReferenceSignals, ValidationFixtureManifest, ValidationFixtureSpec,
};
pub use report::{
    CheckSummary, ModelValidationSummary as ValidationSummary, ParitySignalDelta,
    ParityValidationSummary, TensorValidationSummary, ValidationStatus,
};

fn map_model_error(model: &str, err: ModelError) -> ValidationError {
    match err {
        ModelError::NotFound(_) => {
            ValidationError::Usage(format!("Unknown model identifier: {model}"))
        }
        other => ValidationError::FailedValidation {
            model: model.to_string(),
            reason: other.to_string(),
        },
    }
}

fn load_contract(
    fixture_set: &LoadedFixtureSet,
    model: &str,
) -> Result<ContractArtifact, ValidationError> {
    fixture_set.load_contract(model)
}

pub fn validate_session(
    session: &mut ModelSession,
    fixture_selection: Option<&str>,
    refresh_goldens: bool,
) -> Result<ModelValidationSummary, ValidationError> {
    let fixture_set = load_fixture_set(fixture_selection)?;
    validate_session_with_fixture_set(session, &fixture_set, refresh_goldens)
}

pub fn validate_model(
    model: &str,
    fixture_selection: Option<&str>,
    refresh_goldens: bool,
) -> Result<ModelValidationSummary, ValidationError> {
    let mut session = ModelSession::load(model).map_err(|err| map_model_error(model, err))?;
    validate_session(&mut session, fixture_selection, refresh_goldens)
}

pub fn summarize_session_or_unverified(
    session: &mut ModelSession,
    fixture_selection: Option<&str>,
) -> ModelValidationSummary {
    if !session.entry().is_ready() {
        return ModelValidationSummary::unverified(
            &session.info().name,
            &session.entry().validation.evidence_timestamp,
            format!(
                "{} is still a planned integration. Development reports may use stubbed outputs, but source-alignment validation remains withheld until the real ONNX path is wired.",
                session.info().name
            ),
        )
        .with_backend(session.backend());
    }

    match summarize_registered_evidence(session.entry(), fixture_selection) {
        Ok(summary) => summary.with_backend(session.backend()),
        Err(err) => ModelValidationSummary::unverified(
            &session.info().name,
            &session.entry().validation.evidence_timestamp,
            format!("Approved validation evidence could not be loaded: {err}"),
        )
        .with_backend(session.backend()),
    }
}

pub fn validate_session_with_fixture_set(
    session: &mut ModelSession,
    fixture_set: &LoadedFixtureSet,
    refresh_goldens: bool,
) -> Result<ModelValidationSummary, ValidationError> {
    let model = session.info().name.clone();
    let contract = load_contract(fixture_set, &model)?;
    let preprocess = evaluate_preprocess_contract(session.entry(), &contract, fixture_set)?;
    let fixtures = fixture_set.materialize_fixtures()?;

    if fixtures.is_empty() {
        return Err(ValidationError::MissingFixtures(format!(
            "Fixture set '{}' does not contain any fixture definitions",
            fixture_set.manifest.fixture_set
        )));
    }

    let mut outputs = Vec::with_capacity(fixtures.len());
    for fixture in &fixtures {
        let output =
            session
                .infer(&fixture.image)
                .map_err(|err| ValidationError::FailedValidation {
                    model: model.clone(),
                    reason: format!("Inference failed on fixture '{}': {err}", fixture.spec.id),
                })?;
        outputs.push(output);
    }

    let tensors = evaluate_tensor_semantics(session.entry(), &contract, fixture_set, &outputs[0]);
    let observed = summarize_outputs(fixtures.as_slice(), outputs.as_slice());

    let reference = if refresh_goldens {
        let artifact = build_reference_artifact(
            &model,
            &session.entry().validation,
            session.backend(),
            observed.clone(),
        );
        fixture_set.write_reference(&model, &artifact)?;
        artifact
    } else {
        fixture_set.load_reference(&model)?
    };

    let parity = evaluate_reference_parity(
        session.entry(),
        fixture_set,
        session.backend(),
        &observed,
        &reference,
    );
    let evidence_timestamp = reference.evidence_timestamp.clone();
    let artifact_id = reference.artifact_id.clone();
    let fixture_set_id = reference.fixture_set.clone();
    let summary = ModelValidationSummary::from_checks(
        model,
        evidence_timestamp,
        preprocess,
        tensors,
        parity.with_artifact(Some(artifact_id), Some(fixture_set_id)),
    )
    .with_backend(session.backend());

    Ok(summary)
}

pub fn default_artifact_id(model: &str, fixture_set: &str, evidence_timestamp: &str) -> String {
    build_reference_artifact_id(model, fixture_set, evidence_timestamp)
}