ggen 5.1.3

Ontology-driven code generation: Transform RDF ontologies into typed code through SPARQL queries and Tera templates
Documentation
//! Build script for compile-time validation
//!
//! Part of the Andon Signal Validation Framework - Layer 1: Compile-Time Validation
//!
//! This build script validates CLI commands and test configurations at compile time.
//!
//! ## Poka-Yoke: Verb Module Registration Validation
//!
//! Prevents the silent linkme registration failure by verifying at compile time
//! that all verb modules in `crates/ggen-cli/src/cmds/` are properly imported
//! in `mod.rs` with the force-link pattern (`use module as _;`).
//!
//! This catches the bug where a new command module is added but forgotten to be
//! force-linked, causing linkme's distributed slice to silently exclude it.

fn main() {
    // Poka-Yoke: Validate verb module registration
    validate_verb_module_registration();

    // Validate clnrm test configurations
    validate_clnrm_configs();

    // Rerun if cmds directory changes
    println!("cargo:rerun-if-changed=crates/ggen-cli/src/cmds/");
}

/// Poka-Yoke: Validate all verb modules are properly registered in mod.rs
///
/// This prevents the silent linkme failure where #[verb] macros are defined
/// but the module is optimized away by the linker because it's not referenced.
///
/// ## What This Checks
///
/// 1. Scans `crates/ggen-cli/src/cmds/*.rs` for files containing `#[verb(`
/// 2. Verifies each such file is imported in `mod.rs` with `use MODULE as _;`
/// 3. Emits cargo:warning if any verb module is missing the force-link pattern
///
/// ## Why This Matters
///
/// Without `use module as _;`, Rust's dead code elimination removes the linkme
/// static items generated by #[verb], causing commands to silently disappear.
fn validate_verb_module_registration() {
    use std::fs;
    use std::path::Path;

    let cmds_dir = Path::new("crates/ggen-cli/src/cmds");
    if !cmds_dir.exists() {
        // Not in workspace root or cmds dir doesn't exist
        return;
    }

    let mod_rs_path = cmds_dir.join("mod.rs");
    let mod_rs_content = match fs::read_to_string(&mod_rs_path) {
        Ok(content) => content,
        Err(_) => return, // Can't read mod.rs, skip validation
    };

    // Find all .rs files in cmds/ that contain #[verb(
    let mut verb_modules: Vec<String> = Vec::new();
    let mut missing_registrations: Vec<String> = Vec::new();

    if let Ok(entries) = fs::read_dir(cmds_dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.extension().is_some_and(|e| e == "rs") {
                let file_name = path.file_stem().unwrap().to_string_lossy().to_string();

                // Skip mod.rs and helpers.rs (not verb modules)
                if file_name == "mod" || file_name == "helpers" {
                    continue;
                }

                // Check if this file contains a #[verb( attribute
                if let Ok(content) = fs::read_to_string(&path) {
                    if content.contains("#[verb(") {
                        verb_modules.push(file_name.clone());

                        // Check if mod.rs has the force-link pattern for this module
                        // Pattern: `use MODULE as _;` or `use MODULE as _`
                        let force_link_pattern = format!("use {} as _", file_name);
                        if !mod_rs_content.contains(&force_link_pattern) {
                            missing_registrations.push(file_name);
                        }
                    }
                }
            }
        }
    }

    // Emit warnings for missing registrations (Andon YELLOW signal)
    for module in &missing_registrations {
        println!(
            "cargo:warning=POKA-YOKE: Verb module '{}' contains #[verb] but is NOT force-linked in cmds/mod.rs!",
            module
        );
        println!(
            "cargo:warning=POKA-YOKE: Add `#[allow(unused_imports)] use {} as _;` to cmds/mod.rs",
            module
        );
        println!(
            "cargo:warning=POKA-YOKE: Without this, the command will be silently removed by the linker!"
        );
    }

    // If any missing registrations, emit a summary warning
    if !missing_registrations.is_empty() {
        println!(
            "cargo:warning=POKA-YOKE SUMMARY: {} verb module(s) missing force-link pattern: {:?}",
            missing_registrations.len(),
            missing_registrations
        );
        println!("cargo:warning=POKA-YOKE: This will cause commands to silently fail at runtime!");
    }

    // Emit positive confirmation if all modules are properly registered
    if !verb_modules.is_empty() && missing_registrations.is_empty() {
        // Use note for success (less visible than warning)
        eprintln!(
            "note: POKA-YOKE: All {} verb module(s) properly force-linked: {:?}",
            verb_modules.len(),
            verb_modules
        );
    }
}

/// Validate all clnrm test configuration files
fn validate_clnrm_configs() {
    use std::fs;
    use std::path::Path;

    let test_dir = Path::new("tests/clnrm");
    if !test_dir.exists() {
        // Test directory doesn't exist - this is OK for some build contexts
        return;
    }

    // Check for main CLI commands test file
    let cli_commands = test_dir.join("cli_commands.clnrm.toml");
    if cli_commands.exists() {
        // Basic validation - file exists and is readable
        if let Err(e) = fs::metadata(&cli_commands) {
            println!("cargo:warning=clnrm test file validation failed: {}", e);
        }
    }
}