koala-core 1.0.4

Shared types, invariant evaluator, and primitives for the koala framework.
Documentation
use crate::invariant::{Category, Context, Invariant, Outcome};
use std::fs;

pub struct LicenseDeclared;

impl Invariant for LicenseDeclared {
    fn id(&self) -> &'static str {
        "docs.license-declared"
    }
    fn category(&self) -> Category {
        Category::Docs
    }
    fn intent(&self) -> &'static str {
        "Workspace declares a license AND a LICENSE file exists at the repo root."
    }
    fn adr(&self) -> Option<&'static str> {
        Some("ADR-0013")
    }

    fn evaluate(&self, ctx: &Context) -> Outcome {
        let cargo = ctx.root().join("Cargo.toml");
        let Ok(content) = fs::read_to_string(&cargo) else {
            return Outcome::skip("root Cargo.toml not found");
        };
        let parsed: toml::Value = match toml::from_str(&content) {
            Ok(v) => v,
            Err(e) => return Outcome::fail(format!("failed to parse Cargo.toml: {e}")),
        };
        let declared = parsed
            .get("workspace")
            .and_then(|w| w.get("package"))
            .and_then(|p| p.get("license"))
            .and_then(|e| e.as_str());

        let Some(license) = declared else {
            return Outcome::fail_repro(
                "Cargo.toml [workspace.package] missing `license`",
                "grep -A2 '\\[workspace.package\\]' Cargo.toml",
            );
        };

        let candidates = ["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING"];
        let has_file = candidates.iter().any(|c| ctx.root().join(c).exists());
        if !has_file {
            return Outcome::fail_repro(
                format!("license declared as `{license}` but no LICENSE file at repo root"),
                "ls LICENSE LICENSE.md LICENSE.txt COPYING 2>/dev/null",
            );
        }
        Outcome::pass_with(format!("license = \"{license}\""))
    }
}