shepherd-compiler 6.4.5

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 = 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 = 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, &predicates, &templates]
                    .into_iter()
                    .flatten(),
            ),
            snapshot(
                &authored,
                [
                    &authored_roles,
                    &authored_skills,
                    &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_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> {
    let mut files = 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(),
            "skill source is not a regular file: {}",
            skill.display()
        );
        files.push(skill);
    }
    files.sort();
    files
}

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");
}