hf2q 0.1.7

Pure Rust CLI for converting HuggingFace models to hardware-optimized formats and serving them over an OpenAI-compatible API on Apple Silicon
use std::path::{Component, Path};

use crate::core::provenance::source_shard::{compute_source_bundle_sha256, SourceShard};

use super::{
    ModelRecipe, ModelRecipeError, RecipeArtifact, RecipeArtifactRole, RecipeHardwareProfile,
    RecipeQuantization, RecipeSource, RecipeSourceFile, RecipeStatus, SourceRetentionChoice,
    MODEL_RECIPE_SCHEMA_VERSION, QWEN38_ACCEPTED_REVISION, QWEN38_RECIPE_ID, QWEN38_REPOSITORY_ID,
    RECIPE_KIND,
};

impl ModelRecipe {
    pub(super) fn validate(&self) -> Result<(), ModelRecipeError> {
        require(self.kind == RECIPE_KIND, "wrong kind")?;
        require(
            self.schema_version == MODEL_RECIPE_SCHEMA_VERSION,
            "unsupported schema version",
        )?;
        require(self.recipe_id == QWEN38_RECIPE_ID, "unknown recipe id")?;
        require(
            self.status == RecipeStatus::Accepted,
            "recipe is not accepted",
        )?;
        require(
            self.conversion.producer_version == "hf2q 0.1.6",
            "invalid accepted artifact producer version",
        )?;
        require(
            self.acceptance.decision == "docs/adr/ADR-044-qwen38-native.md"
                && self.acceptance.accepted_at == "2026-08-17",
            "invalid acceptance evidence",
        )?;
        self.source.validate()?;
        validate_artifacts(&self.artifacts)?;
        validate_hardware_profiles(&self.hardware_profiles)?;
        self.validate_disk()?;
        require(
            self.source_retention.interactive_default == SourceRetentionChoice::Keep
                && self.source_retention.non_interactive_requires_explicit
                && self.source_retention.deletion_scope == "recipe_owned_source_only",
            "invalid source-retention policy",
        )
    }

    fn validate_disk(&self) -> Result<(), ModelRecipeError> {
        let source_bytes = checked_sum(self.source.files.iter().map(|file| file.size))?;
        let artifact_bytes = checked_sum(self.artifacts.iter().map(|artifact| artifact.size))?;
        let minimum = source_bytes
            .checked_add(artifact_bytes)
            .and_then(|value| value.checked_add(self.disk.safety_reserve_bytes))
            .ok_or_else(|| invalid("disk byte total overflow"))?;
        require(
            self.disk.source_bytes == source_bytes,
            "source byte total mismatch",
        )?;
        require(
            self.disk.artifact_bytes == artifact_bytes,
            "artifact byte total mismatch",
        )?;
        require(
            self.disk.safety_reserve_bytes == 8 * 1024 * 1024 * 1024,
            "unexpected safety reserve",
        )?;
        require(
            self.disk.minimum_free_bytes == minimum,
            "minimum free byte total mismatch",
        )
    }
}

impl RecipeSource {
    fn validate(&self) -> Result<(), ModelRecipeError> {
        require(
            self.repository_id == QWEN38_REPOSITORY_ID,
            "wrong repository id",
        )?;
        require(self.repository_type == "model", "wrong repository type")?;
        require(
            self.canonical_url == "https://huggingface.co/Qwen/Qwen3.8-27B",
            "wrong canonical URL",
        )?;
        require(
            self.revision == QWEN38_ACCEPTED_REVISION,
            "wrong source revision",
        )?;
        validate_sha256(&self.bundle_sha256, "source bundle sha256")?;
        require(
            self.files.len() == 29,
            "Qwen3.8 recipe must contain 29 files",
        )?;

        let mut prior = None;
        for file in &self.files {
            file.validate()?;
            if let Some(previous) = prior {
                require(
                    previous < file.path.as_str(),
                    "source files are not unique/sorted",
                )?;
            }
            prior = Some(file.path.as_str());
        }

        let source_shards = self
            .files
            .iter()
            .map(|file| SourceShard {
                filename: file.path.clone(),
                bytes: file.size,
                sha256: file.hf_lfs_sha256.clone(),
                hf_etag: file.hub_etag.clone(),
                is_lfs: file.hf_lfs_sha256.is_some(),
                verified_at_secs: 0,
            })
            .collect::<Vec<_>>();
        require(
            compute_source_bundle_sha256(&source_shards).as_deref()
                == Some(self.bundle_sha256.as_str()),
            "source bundle sha256 does not match LFS entries",
        )
    }
}

impl RecipeSourceFile {
    fn validate(&self) -> Result<(), ModelRecipeError> {
        let path = Path::new(&self.path);
        require(
            !self.path.is_empty()
                && self.path.len() <= crate::input::hf_reference::MAX_HF_FILENAME_BYTES
                && self.path.is_ascii()
                && !self.path.contains('\\')
                && path.components().count() == 1
                && path
                    .components()
                    .all(|component| matches!(component, Component::Normal(_))),
            "unsafe recipe source path",
        )?;
        require(self.size > 0, "recipe source file is empty")?;
        validate_sha256(&self.sha256, "recipe source sha256")?;
        if let Some(lfs) = &self.hf_lfs_sha256 {
            validate_sha256(lfs, "recipe LFS sha256")?;
            require(
                lfs == &self.sha256 && lfs == &self.hub_etag,
                "LFS identities differ",
            )?;
        } else {
            validate_lower_hex(&self.hub_etag, 40, "recipe Git blob sha1")?;
        }
        require(
            !self.path.ends_with(".safetensors") || self.hf_lfs_sha256.is_some(),
            "safetensors source lacks LFS sha256",
        )
    }
}

fn validate_artifacts(artifacts: &[RecipeArtifact]) -> Result<(), ModelRecipeError> {
    require(
        artifacts.len() == 2,
        "recipe must contain exactly two artifacts",
    )?;
    let expected = [
        (
            RecipeArtifactRole::Text,
            RecipeQuantization::Q4KM,
            "Qwen3.8-27B-Q4_K_M.gguf",
        ),
        (
            RecipeArtifactRole::VisionProjector,
            RecipeQuantization::F16Mmproj,
            "Qwen3.8-27B-mmproj-F16.gguf",
        ),
    ];
    for (artifact, (role, quantization, filename)) in artifacts.iter().zip(expected) {
        require(
            artifact.role == role
                && artifact.quantization == quantization
                && artifact.filename == filename,
            "invalid artifact role/quantization/name",
        )?;
        require(artifact.size > 0, "empty accepted artifact")?;
        validate_sha256(&artifact.sha256, "artifact sha256")?;
    }
    Ok(())
}

fn validate_hardware_profiles(profiles: &[RecipeHardwareProfile]) -> Result<(), ModelRecipeError> {
    require(
        profiles.len() == 1,
        "recipe must contain one proven hardware profile",
    )?;
    let profile = &profiles[0];
    require(
        profile.profile_id == "qwen38-m5-max-128g-q4-k-m-v1"
            && profile.target == "aarch64-apple-darwin"
            && profile.chip_model == "Apple M5 Max"
            && profile.minimum_unified_memory_bytes == 128 * 1024 * 1024 * 1024
            && profile.text_quantization == RecipeQuantization::Q4KM
            && profile.runtime_calibration_required,
        "invalid Qwen3.8 hardware profile",
    )
}

fn validate_sha256(value: &str, field: &str) -> Result<(), ModelRecipeError> {
    validate_lower_hex(value, 64, field)
}

fn validate_lower_hex(value: &str, length: usize, field: &str) -> Result<(), ModelRecipeError> {
    require(
        value.len() == length
            && value
                .bytes()
                .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()),
        field,
    )
}

fn checked_sum(values: impl IntoIterator<Item = u64>) -> Result<u64, ModelRecipeError> {
    values.into_iter().try_fold(0_u64, |total, value| {
        total
            .checked_add(value)
            .ok_or_else(|| invalid("byte total overflow"))
    })
}

fn require(condition: bool, reason: &str) -> Result<(), ModelRecipeError> {
    if condition {
        Ok(())
    } else {
        Err(invalid(reason))
    }
}

fn invalid(reason: &str) -> ModelRecipeError {
    ModelRecipeError::Invalid {
        reason: reason.to_owned(),
    }
}