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