shepherd-compiler 6.6.0

Pure, deterministic Shepherd content compiler and prompt-budget engine.
use std::{
    env,
    fmt::Write as _,
    fs,
    path::{Path, PathBuf},
};

fn main() {
    println!("cargo:rerun-if-changed=package-content");
    let manifest = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("manifest directory"));
    let content = manifest.join("package-content/content");
    let roles = collect_roles(&content.join("roles"));
    let (skills, skill_resources) = collect_skills(&content.join("skills"));
    let predicates = collect_predicates(&content.join("predicates"));
    let templates = collect_templates(&content.join("templates"));
    assert!(!roles.is_empty(), "canonical content has zero roles");
    assert!(!skills.is_empty(), "canonical content has zero skills");
    assert!(
        !predicates.is_empty(),
        "canonical content has zero predicates"
    );

    let authored = manifest.join("../../content");
    if authored.is_dir() {
        println!("cargo:rerun-if-changed=../../content");
        let authored_roles = collect_roles(&authored.join("roles"));
        let (authored_skills, authored_skill_resources) = collect_skills(&authored.join("skills"));
        let authored_predicates = collect_predicates(&authored.join("predicates"));
        let authored_templates = collect_templates(&authored.join("templates"));
        assert_eq!(
            snapshot(
                &content,
                [&roles, &skills, &skill_resources, &predicates, &templates]
                    .into_iter()
                    .flatten(),
            ),
            snapshot(
                &authored,
                [
                    &authored_roles,
                    &authored_skills,
                    &authored_skill_resources,
                    &authored_predicates,
                    &authored_templates,
                ]
                .into_iter()
                .flatten(),
            ),
            "generated compiler package content differs from authored root content"
        );
    }

    let mut generated = String::from(
        "// Generated by crates/compiler/build.rs from the canonical top-level content tree.\n\
         // This file lives in OUT_DIR and must never be hand-edited.\n",
    );
    emit_array(&mut generated, "EMBEDDED_ROLES", &content, &roles);
    emit_array(&mut generated, "EMBEDDED_SKILLS", &content, &skills);
    emit_resource_array(
        &mut generated,
        "EMBEDDED_SKILL_RESOURCES",
        &content,
        &skill_resources,
    );
    emit_array(&mut generated, "EMBEDDED_PREDICATES", &content, &predicates);
    emit_array(&mut generated, "EMBEDDED_TEMPLATES", &content, &templates);

    let output = PathBuf::from(env::var_os("OUT_DIR").expect("output directory"))
        .join("embedded_content.rs");
    fs::write(output, generated).expect("write embedded canonical content");
}

fn collect_templates(directory: &Path) -> Vec<PathBuf> {
    let handoff = directory.join("handoff.md");
    let metadata = fs::symlink_metadata(&handoff).expect("templates must contain handoff.md");
    assert!(
        metadata.file_type().is_file() && !metadata.file_type().is_symlink(),
        "handoff template is not a regular file: {}",
        handoff.display()
    );
    vec![handoff]
}

fn snapshot<'a>(
    content: &Path,
    files: impl Iterator<Item = &'a PathBuf>,
) -> Vec<(String, Vec<u8>)> {
    let mut snapshot = files
        .map(|path| {
            let logical = path
                .strip_prefix(content)
                .expect("content source below root")
                .to_string_lossy()
                .replace('\\', "/");
            let raw = fs::read(path)
                .unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display()));
            (logical, raw)
        })
        .collect::<Vec<_>>();
    snapshot.sort();
    snapshot
}

fn collect_roles(directory: &Path) -> Vec<PathBuf> {
    let mut files = Vec::new();
    for entry in read_directory(directory) {
        let path = entry.path();
        let kind = entry.file_type().expect("inspect role entry");
        assert!(
            kind.is_file() && !kind.is_symlink(),
            "unexpected role entry: {}",
            path.display()
        );
        assert_eq!(
            path.extension().and_then(|value| value.to_str()),
            Some("md"),
            "role is not Markdown: {}",
            path.display()
        );
        files.push(path);
    }
    files.sort();
    files
}

fn collect_skills(directory: &Path) -> (Vec<PathBuf>, Vec<PathBuf>) {
    let mut skills = Vec::new();
    let mut resources = Vec::new();
    for entry in read_directory(directory) {
        let path = entry.path();
        let kind = entry.file_type().expect("inspect skill entry");
        assert!(
            kind.is_dir() && !kind.is_symlink(),
            "unexpected skill entry: {}",
            path.display()
        );
        let skill = path.join("SKILL.md");
        let metadata = fs::symlink_metadata(&skill).expect("skill directory must contain SKILL.md");
        assert!(
            metadata.file_type().is_file() && !metadata.file_type().is_symlink(),
            "skill source is not a regular file: {}",
            skill.display()
        );
        skills.push(skill);
        for resource_directory in ["assets", "references", "scripts"] {
            let resource_root = path.join(resource_directory);
            let metadata = match fs::symlink_metadata(&resource_root) {
                Ok(metadata) => metadata,
                Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
                Err(error) => panic!(
                    "cannot inspect skill resource category {}: {error}",
                    resource_root.display()
                ),
            };
            assert!(
                metadata.file_type().is_dir() && !metadata.file_type().is_symlink(),
                "skill resource category is not a regular directory: {}",
                resource_root.display()
            );
            for resource in read_directory(&resource_root) {
                let resource_path = resource.path();
                let resource_type = resource.file_type().expect("inspect skill resource");
                assert!(
                    resource_type.is_file() && !resource_type.is_symlink(),
                    "skill resource must be one regular file below its category: {}",
                    resource_path.display()
                );
                resources.push(resource_path);
            }
        }
        for child in read_directory(&path) {
            let child_path = child.path();
            let child_name = child.file_name();
            let child_name = child_name.to_str().expect("UTF-8 skill entry");
            assert!(
                child_name == "SKILL.md"
                    || ["assets", "references", "scripts"].contains(&child_name),
                "unexpected skill entry: {}",
                child_path.display()
            );
        }
    }
    skills.sort();
    resources.sort();
    (skills, resources)
}

fn collect_predicates(directory: &Path) -> Vec<PathBuf> {
    let mut files = Vec::new();
    for entry in read_directory(directory) {
        let path = entry.path();
        let kind = entry.file_type().expect("inspect predicate entry");
        assert!(
            kind.is_file() && !kind.is_symlink(),
            "unexpected predicate entry: {}",
            path.display()
        );
        assert_eq!(
            path.extension().and_then(|value| value.to_str()),
            Some("toml"),
            "predicate is not TOML: {}",
            path.display()
        );
        files.push(path);
    }
    files.sort();
    files
}

fn read_directory(directory: &Path) -> Vec<fs::DirEntry> {
    fs::read_dir(directory)
        .unwrap_or_else(|error| panic!("cannot read {}: {error}", directory.display()))
        .map(|entry| entry.expect("read canonical content entry"))
        .collect()
}

fn emit_array(output: &mut String, name: &str, content: &Path, files: &[PathBuf]) {
    writeln!(output, "pub(crate) const {name}: &[(&str, &str)] = &[").expect("write array");
    let source_root = content.parent().expect("content parent");
    for path in files {
        let logical = path
            .strip_prefix(source_root)
            .expect("canonical source below root")
            .to_string_lossy()
            .replace('\\', "/");
        let raw = fs::read_to_string(path)
            .unwrap_or_else(|error| panic!("{} is not UTF-8: {error}", path.display()));
        writeln!(output, "    ({logical:?}, {raw:?}),").expect("write embedded entry");
    }
    writeln!(output, "];\n").expect("finish array");
}

fn emit_resource_array(output: &mut String, name: &str, content: &Path, files: &[PathBuf]) {
    writeln!(
        output,
        "pub(crate) const {name}: &[(&str, &str, bool)] = &["
    )
    .expect("write resource array");
    let source_root = content.parent().expect("content parent");
    for path in files {
        let logical = path
            .strip_prefix(source_root)
            .expect("canonical resource below root")
            .to_string_lossy()
            .replace('\\', "/");
        let raw = fs::read_to_string(path)
            .unwrap_or_else(|error| panic!("{} is not UTF-8: {error}", path.display()));
        let executable = path
            .parent()
            .and_then(Path::file_name)
            .and_then(|value| value.to_str())
            == Some("scripts");
        writeln!(output, "    ({logical:?}, {raw:?}, {executable}),")
            .expect("write embedded resource");
    }
    writeln!(output, "];\n").expect("finish resource array");
}