pleme-doc-gen 0.1.53

Rust replacement for the M0 Python _gen-patterns.py + _gen-docs.py scripts in pleme-io/actions. Walks every action.yml + emits substrate's patterns-full.nix + per-action README.md + root catalog. Per the NO-SHELL prime directive.
//! render_health — typed measurement of rendered scaffold completeness.
//!
//! Per the operator's directive "everything we consume has running
//! tests, flows, and makes it all the way out to OSS with green CI":
//! this module measures the LOCAL prerequisites — manifest present,
//! auto-release workflow present, test stub present — for each
//! rendered scaffold.
//!
//! Composes with fidelity: fidelity scores how well consumption
//! PRESERVES the original; render-health scores how well the
//! rendered output is WIRED for the auto-release pipeline. Both
//! contribute to substrate-quality at scale.

use std::path::{Path, PathBuf};

/// Typed health snapshot of one rendered scaffold.
#[derive(Debug, Clone, Default)]
pub struct RenderHealth {
    pub ecosystem: String,
    /// Manifest file (Cargo.toml, package.json, etc.) is present.
    pub has_manifest: bool,
    /// Path to the located manifest, if any.
    pub manifest_path: Option<PathBuf>,
    /// Count of workflow files under .github/workflows/.
    pub workflow_count: usize,
    /// The substrate's typed auto-release shim is present.
    pub has_auto_release: bool,
    /// Count of test-surface files (per-ecosystem location).
    pub test_count: usize,
    /// Paths to detected test files (useful for operator drill-down).
    pub test_paths: Vec<PathBuf>,
    /// All workflow .yml files parse cleanly via serde_yaml. False if
    /// ANY workflow file fails to parse — surfaces typed-emission
    /// regressions in workflow renderers mechanically.
    pub workflows_parse: bool,
    /// Per-workflow parse outcomes — paths that failed to parse, with
    /// the serde_yaml error message. Empty when workflows_parse is true.
    pub workflow_parse_errors: Vec<(PathBuf, String)>,
    /// The ecosystem's manifest file parses cleanly (where parseable):
    ///   - helm Chart.yaml + github-action action.yml: via serde_yaml
    ///   - others: trivially true (parser not yet wired)
    /// False = substrate emitted a structurally-broken manifest.
    pub manifest_parses: bool,
    /// Parser error when manifest_parses is false.
    pub manifest_parse_error: Option<String>,
}

impl RenderHealth {
    /// True iff all five pipeline prereqs are present + valid: manifest
    /// file present, manifest parses, auto-release workflow present,
    /// at least one test file, AND every workflow .yml parses cleanly.
    /// The minimum state required for the GitHub-Actions auto-release
    /// loop to run + succeed.
    pub fn is_pipeline_ready(&self) -> bool {
        self.has_manifest && self.manifest_parses && self.has_auto_release
            && self.test_count > 0 && self.workflows_parse
    }

    /// Score in permille — 1000 = fully wired + valid for the auto-
    /// release pipeline; 0 = nothing rendered. Five equal-weight gates.
    pub fn score_permille(&self) -> i64 {
        let mut score = 0_i64;
        if self.has_manifest     { score += 200; }
        if self.manifest_parses  { score += 200; }
        if self.has_auto_release { score += 200; }
        if self.test_count > 0   { score += 200; }
        if self.workflows_parse  { score += 200; }
        score
    }
}

/// Check a rendered-scaffold directory + return typed health.
pub fn check(rendered: &Path, ecosystem: &str) -> RenderHealth {
    let mut h = RenderHealth {
        ecosystem: ecosystem.to_string(),
        workflows_parse: true, // assume true; flipped false on any failure
        manifest_parses: true, // assume true; flipped false on parse fail
        ..Default::default()
    };

    // Manifest detection + parse validation per ecosystem.
    let manifest_name = manifest_for(ecosystem);
    if let Some(name) = manifest_name {
        let path = rendered.join(name);
        if path.is_file() {
            h.has_manifest = true;
            h.manifest_path = Some(path.clone());
            // Per-ecosystem manifest parse — only YAML-shape manifests
            // for now (Chart.yaml, action.yml). TOML/JSON parse gates
            // arrive when those parser crates land as deps.
            if let Ok(text) = std::fs::read_to_string(&path) {
                let parse_result: Result<(), String> = match ecosystem {
                    "helm" | "github-action" =>
                        serde_yaml::from_str::<serde_yaml::Value>(&text)
                            .map(|_| ()).map_err(|e| e.to_string()),
                    "rust-single-crate" | "rust-workspace" |
                    "python" | "python-pdm" | "python-pipenv" =>
                        toml::from_str::<toml::Value>(&text)
                            .map(|_| ()).map_err(|e| e.to_string()),
                    "npm" | "js-pnpm" =>
                        serde_json::from_str::<serde_json::Value>(&text)
                            .map(|_| ()).map_err(|e| e.to_string()),
                    _ => Ok(()),
                };
                if let Err(e) = parse_result {
                    h.manifest_parses = false;
                    h.manifest_parse_error = Some(e);
                }
            }
        }
    }

    // Workflow + auto-release detection + parse validation.
    let wf_dir = rendered.join(".github").join("workflows");
    if wf_dir.is_dir() {
        if let Ok(entries) = std::fs::read_dir(&wf_dir) {
            for entry in entries.flatten() {
                let name = entry.file_name();
                let name_str = name.to_string_lossy();
                if name_str.ends_with(".yml") || name_str.ends_with(".yaml") {
                    h.workflow_count += 1;
                    if name_str == "auto-release.yml" || name_str == "auto-release.yaml" {
                        h.has_auto_release = true;
                    }
                    // Typed parse validation via serde_yaml — catches
                    // any structural YAML emission regression mechanically.
                    let path = entry.path();
                    if let Ok(text) = std::fs::read_to_string(&path) {
                        if let Err(e) = serde_yaml::from_str::<serde_yaml::Value>(&text) {
                            h.workflows_parse = false;
                            h.workflow_parse_errors.push((path, e.to_string()));
                        }
                    }
                }
            }
        }
    }

    // Test-surface detection per ecosystem.
    for test_glob in tests_for(ecosystem) {
        let p = rendered.join(test_glob);
        if p.is_file() {
            h.test_count += 1;
            h.test_paths.push(p);
        }
    }

    h
}

/// Per-ecosystem manifest filename (the file whose presence ANCHORS
/// the rendered scaffold). Returns None for ecosystems we haven't
/// surveyed yet.
fn manifest_for(ecosystem: &str) -> Option<&'static str> {
    Some(match ecosystem {
        "rust-single-crate" | "rust-workspace" => "Cargo.toml",
        "npm" | "js-pnpm" => "package.json",
        "python" | "python-pdm" | "python-pipenv" => "pyproject.toml",
        "helm" => "Chart.yaml",
        "github-action" => "action.yml",
        "nix-flake" => "flake.nix",
        "go" => "go.mod",
        "zig" => "build.zig",
        "ruby-gem" => "Gemfile",
        "elixir-mix" => "mix.exs",
        "java-gradle-kts" => "build.gradle.kts",
        "java-maven" => "pom.xml",
        _ => return None,
    })
}

/// Per-ecosystem test-surface filenames the substrate's renderer + its
/// GreenCiStarter actually emit. Multiple entries are OR'd — any one
/// present scores. Paths must match what `green_ci::starter_for(eco)`
/// writes; mismatch = false negative in pipeline-readiness reports.
fn tests_for(ecosystem: &str) -> &'static [&'static str] {
    match ecosystem {
        "rust-single-crate" | "rust-workspace" => &["src/lib.rs", "tests/smoke.rs"],
        "npm" | "js-pnpm"   => &["test/test.js", "test/index.test.js", "tests/smoke.js"],
        "python" | "python-pdm" | "python-pipenv" => &["tests/test_smoke.py"],
        "helm"              => &["tests/example.yaml"],
        "github-action"     => &["run.tlisp"],
        "nix-flake"         => &["flake.nix"],
        "go"                => &["main_test.go", "smoke_test.go"],
        "zig"               => &["src/main.zig"],
        "java-gradle-kts"   => &["src/test/java/io/pleme/wrapper/SmokeTest.java"],
        "java-maven"        => &["src/test/java/io/pleme/wrapper/SmokeTest.java"],
        _ => &[],
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;

    fn mk(files: &[(&str, &str)]) -> tempdir::TempDir {
        let tmp = tempdir::TempDir::new("rh").unwrap();
        for (rel, body) in files {
            let p = tmp.path().join(rel);
            if let Some(parent) = p.parent() { fs::create_dir_all(parent).unwrap(); }
            fs::write(&p, body).unwrap();
        }
        tmp
    }

    #[test]
    fn fully_wired_rust_scaffold_scores_1000() {
        let dir = mk(&[
            ("Cargo.toml", "[package]\nname=\"x\"\n"),
            (".github/workflows/auto-release.yml", "name: x\n"),
            ("src/lib.rs", "#[cfg(test)] mod t { #[test] fn s(){} }"),
        ]);
        let h = check(dir.path(), "rust-single-crate");
        assert!(h.has_manifest);
        assert!(h.manifest_parses); // rust: not gated yet (TOML parser pending)
        assert!(h.has_auto_release);
        assert_eq!(h.test_count, 1);
        assert!(h.is_pipeline_ready());
        assert_eq!(h.score_permille(), 1000);
    }

    #[test]
    fn malformed_action_yml_flips_manifest_parses_false() {
        let dir = mk(&[
            ("action.yml", "name: x\nbroken: [unclosed\n"),
            (".github/workflows/auto-release.yml", "name: x\n"),
            ("run.tlisp", "(noop)"),
        ]);
        let h = check(dir.path(), "github-action");
        assert!(h.has_manifest);
        assert!(!h.manifest_parses,
            "expected manifest_parses=false on malformed action.yml");
        assert!(h.manifest_parse_error.is_some());
        assert!(!h.is_pipeline_ready());
    }

    #[test]
    fn missing_auto_release_breaks_pipeline_ready() {
        let dir = mk(&[
            ("Cargo.toml", "[package]\nname=\"x\"\n"),
            (".github/workflows/other.yml", "name: x\n"),
            ("src/lib.rs", "// stub"),
        ]);
        let h = check(dir.path(), "rust-single-crate");
        assert!(!h.has_auto_release);
        assert_eq!(h.workflow_count, 1);
        assert!(h.workflows_parse, "valid YAML should parse");
        assert!(!h.is_pipeline_ready());
        assert!(h.score_permille() < 1000);
    }

    #[test]
    fn empty_dir_scores_only_parse_gates() {
        // Empty dir has no workflows to fail + no manifest to fail →
        // workflows_parse + manifest_parses both default true → two of
        // five gates met → 400/1000.
        let dir = mk(&[]);
        let h = check(dir.path(), "rust-single-crate");
        assert_eq!(h.score_permille(), 400);
        assert!(!h.is_pipeline_ready());
    }

    #[test]
    fn malformed_workflow_yaml_flips_parse_gate_false() {
        let dir = mk(&[
            ("Cargo.toml", "[package]\nname=\"x\"\n"),
            (".github/workflows/auto-release.yml", "name: x\nbroken: [unclosed\n"),
            ("src/lib.rs", "// stub"),
        ]);
        let h = check(dir.path(), "rust-single-crate");
        assert!(!h.workflows_parse,
            "expected workflows_parse=false on malformed YAML");
        assert!(!h.workflow_parse_errors.is_empty());
        assert!(!h.is_pipeline_ready(),
            "pipeline_ready should require workflows_parse");
    }

    #[test]
    fn nix_flake_uses_flake_nix_as_test_surface() {
        // Per the substrate's nix-flake model: the flake itself IS
        // the surface; `nix flake check` is the runtime test.
        let dir = mk(&[
            ("flake.nix", "{ description = \"x\"; outputs = ...; }"),
            (".github/workflows/auto-release.yml", "name: x\n"),
        ]);
        let h = check(dir.path(), "nix-flake");
        assert!(h.has_manifest);
        assert_eq!(h.test_count, 1); // flake.nix counts as its own test surface
        assert!(h.is_pipeline_ready());
    }

    #[test]
    fn unknown_ecosystem_returns_partial_zero_health() {
        let dir = mk(&[
            ("Cargo.toml", "[package]\nname=\"x\"\n"),
        ]);
        let h = check(dir.path(), "totally-unknown-eco");
        assert!(!h.has_manifest); // no manifest_for entry → not checked
        assert_eq!(h.test_count, 0);
    }
}