shepherd-compiler 6.6.1

Pure, deterministic Shepherd content compiler and prompt-budget engine.
//! Content addressing for emitted trees.
//!
//! Whole-tree and per-bundle digests. Deterministic by construction: the
//! emitted order is the hashed order.

// Tightly coupled to its siblings by construction: this is one compiler
// split by concern, not four independent modules.
use super::*;

pub(super) fn bind_startup_skill_digests(
    roles: &mut [EmittedRole],
    files: &[EmittedFile],
    target: TargetHarness,
) -> Result<(), CompileError> {
    let target_prefix = if target == TargetHarness::Codex {
        ".agents/skills"
    } else {
        "skills"
    };
    for role in roles {
        let skill = required_startup_skill(role)?;
        let prefix = format!("{target_prefix}/{skill}/");
        let bundle = files
            .iter()
            .filter(|file| file.path.starts_with(&prefix))
            .collect::<Vec<_>>();
        if bundle.is_empty() {
            return Err(CompileError::Invalid(format!(
                "startup skill `{skill}` emitted no target files"
            )));
        }
        role.startup_skill_sha256 = Some(startup_bundle_digest(&bundle, target));
    }
    Ok(())
}

pub(super) fn update_tree_digest(digest: &mut Sha256, file: &EmittedFile) {
    digest.update(file.path.as_bytes());
    digest.update([0]);
    digest.update(file.mode.to_be_bytes());
    digest.update([0]);
    digest.update(file.content.as_bytes());
    digest.update([0, 0]);
}

pub(super) fn tree_digest(files: &[EmittedFile]) -> String {
    let mut digest = Sha256::new();
    for file in files {
        update_tree_digest(&mut digest, file);
    }
    hex(&digest.finalize())
}

pub(super) fn startup_bundle_digest(files: &[&EmittedFile], target: TargetHarness) -> String {
    let mut digest = Sha256::new();
    for file in files {
        let path = if target == TargetHarness::Codex {
            file.path
                .strip_prefix(".agents/")
                .expect("Codex startup bundle lives below .agents")
        } else {
            &file.path
        };
        digest.update(path.as_bytes());
        digest.update([0]);
        digest.update(file.mode.to_be_bytes());
        digest.update([0]);
        digest.update(file.content.as_bytes());
        digest.update([0, 0]);
    }
    hex(&digest.finalize())
}

pub(super) fn sha256(bytes: &[u8]) -> String {
    hex(&Sha256::digest(bytes))
}

pub(super) fn hex(bytes: &[u8]) -> String {
    let mut output = String::with_capacity(bytes.len() * 2);
    for byte in bytes {
        write!(output, "{byte:02x}").expect("writing to String cannot fail");
    }
    output
}