use crate::storage::cas::blob::{self, BLOB_MAGIC, BLOB_VERSION, BadBlob};
use crate::storage::generation::manifest::{
MANIFEST_MAGIC, MANIFEST_VERSION, Manifest, ManifestError, ModelIdentity,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ArtifactOutcome<T> {
Ok(T),
Rebuild(RebuildReason),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RebuildReason {
ModelNameMismatch {
expected: String,
actual: String,
},
ModelDigestMismatch {
expected: String,
actual: String,
},
ModelDimensionMismatch {
expected: u32,
actual: u32,
},
}
impl RebuildReason {
pub fn actionable_message(&self) -> String {
match self {
Self::ModelNameMismatch { expected, actual } => {
format!(
"Model name mismatch: manifest says '{actual}' but current profile is \
'{expected}'. A rebuild is required to re-embed with the new model. \
Run: leindex index --force"
)
}
Self::ModelDigestMismatch { expected, actual } => {
format!(
"Model digest mismatch: manifest says '{actual}' but current model digest is \
'{expected}'. The model weights/tokenizer have changed. \
Run: leindex index --force to rebuild neural vectors."
)
}
Self::ModelDimensionMismatch { expected, actual } => {
format!(
"Embedding dimension mismatch: manifest says {actual} but current model \
produces {expected} dimensions. Run: leindex index --force to rebuild."
)
}
}
}
}
impl std::fmt::Display for RebuildReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.actionable_message())
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ArtifactError {
#[error("CAS blob validation failed: {0}")]
BadBlob(#[from] BadBlob),
#[error("Manifest validation failed: {0}")]
BadManifest(#[from] ManifestError),
#[error("Artifact not found: {0}")]
NotFound(String),
}
pub fn validate_blob(bytes: &[u8]) -> Result<[u8; 32], ArtifactError> {
blob::validate_blob(bytes).map_err(ArtifactError::BadBlob)
}
pub fn extract_blob_payload(bytes: &[u8]) -> Result<(&[u8], [u8; 32]), ArtifactError> {
blob::extract_payload(bytes).map_err(ArtifactError::BadBlob)
}
pub fn blob_format_identity() -> (&'static [u8], u8) {
(BLOB_MAGIC, BLOB_VERSION)
}
pub fn validate_manifest(bytes: &[u8]) -> Result<Manifest, ArtifactError> {
Manifest::from_bytes(bytes).map_err(ArtifactError::BadManifest)
}
pub fn validate_manifest_fingerprints(
manifest: &Manifest,
graph_recomputed: &[u8; 32],
search_recomputed: &[u8; 32],
) -> Result<(), ArtifactError> {
manifest
.validate_graph_fingerprint(graph_recomputed)
.map_err(ArtifactError::BadManifest)?;
manifest
.validate_search_fingerprint(search_recomputed)
.map_err(ArtifactError::BadManifest)?;
Ok(())
}
pub fn manifest_format_identity() -> (&'static [u8], u16) {
(MANIFEST_MAGIC, MANIFEST_VERSION)
}
pub fn check_model_identity(manifest: &Manifest, expected: &ModelIdentity) -> ArtifactOutcome<()> {
use ArtifactOutcome::*;
if manifest.model_identity.name != expected.name {
return Rebuild(RebuildReason::ModelNameMismatch {
expected: expected.name.clone(),
actual: manifest.model_identity.name.clone(),
});
}
if manifest.model_identity.digest != expected.digest {
return Rebuild(RebuildReason::ModelDigestMismatch {
expected: expected.digest.clone(),
actual: manifest.model_identity.digest.clone(),
});
}
if manifest.model_identity.dimensions != expected.dimensions {
return Rebuild(RebuildReason::ModelDimensionMismatch {
expected: expected.dimensions,
actual: manifest.model_identity.dimensions,
});
}
Ok(())
}
pub fn validate_generation_manifest(
bytes: &[u8],
expected_model: Option<&ModelIdentity>,
) -> Result<ArtifactOutcome<Manifest>, ArtifactError> {
let manifest = validate_manifest(bytes)?;
if let Some(expected) = expected_model {
match check_model_identity(&manifest, expected) {
ArtifactOutcome::Ok(()) => Ok(ArtifactOutcome::Ok(manifest)),
ArtifactOutcome::Rebuild(reason) => Ok(ArtifactOutcome::Rebuild(reason)),
}
} else {
Ok(ArtifactOutcome::Ok(manifest))
}
}
#[cfg(test)]
#[path = "artifact_test.rs"]
mod tests;