use anyhow::Result;
use clap::Args;
use std::path::PathBuf;
use dkp_core::{
Pack,
validate::{gate4, gate7, gate8, gates::GateStatus},
};
use crate::cli::CmdCtx;
#[derive(Args, Debug)]
pub struct ReleaseCheckArgs {
pub pack: PathBuf,
#[arg(long)]
pub strict: bool,
}
struct Item {
label: &'static str,
pass: bool,
detail: String,
is_warning: bool,
}
impl Item {
fn pass(label: &'static str) -> Self {
Self {
label,
pass: true,
detail: String::new(),
is_warning: false,
}
}
fn fail(label: &'static str, detail: impl Into<String>) -> Self {
Self {
label,
pass: false,
detail: detail.into(),
is_warning: false,
}
}
fn warn(label: &'static str, detail: impl Into<String>) -> Self {
Self {
label,
pass: false,
detail: detail.into(),
is_warning: true,
}
}
}
fn stale_handbook_formats(pack_root: &std::path::Path) -> Option<String> {
let human_dir = pack_root.join("human");
let md_mtime = std::fs::metadata(human_dir.join("handbook.md"))
.and_then(|m| m.modified())
.ok()?;
let mut stale = Vec::new();
for name in ["handbook.pdf", "handbook.epub"] {
match std::fs::metadata(human_dir.join(name)).and_then(|m| m.modified()) {
Ok(mtime) if mtime >= md_mtime => {}
_ => stale.push(name),
}
}
if stale.is_empty() {
None
} else {
Some(stale.join(", "))
}
}
pub async fn run(args: ReleaseCheckArgs, _cli: &CmdCtx) -> Result<()> {
let pack = Pack::open(&args.pack)?;
let mut items: Vec<Item> = Vec::new();
let g4 = gate4::run(&pack);
if g4.status == GateStatus::Pass {
items.push(Item::pass("Gate 4 (machine usability)"));
} else {
let failures: Vec<_> = g4
.checks
.iter()
.filter(|c| c.status == GateStatus::Fail)
.map(|c| c.description.as_str())
.collect();
items.push(Item::fail(
"Gate 4 (machine usability)",
failures.join("; "),
));
}
let g8 = gate8::run(&pack);
if g8.status == GateStatus::Pass || g8.status == GateStatus::Skipped {
items.push(Item::pass("Gate 8 (OKF conformance)"));
} else {
let failures: Vec<_> = g8
.checks
.iter()
.filter(|c| c.status == GateStatus::Fail)
.map(|c| c.description.as_str())
.collect();
items.push(Item::fail("Gate 8 (OKF conformance)", failures.join("; ")));
}
let g7 = gate7::run(&pack);
match g7.status {
GateStatus::Pass => items.push(Item::pass("Gate 7 (evaluation)")),
GateStatus::Skipped => {
items.push(Item::warn(
"Gate 7 (evaluation)",
"eval_set.jsonl absent — DKP-Evaluated tier not achievable",
));
}
_ => {
let failures: Vec<_> = g7
.checks
.iter()
.filter(|c| c.status == GateStatus::Fail)
.map(|c| c.description.as_str())
.collect();
items.push(Item::fail("Gate 7 (evaluation)", failures.join("; ")));
}
}
let review_notes = pack.evidence_file("review_notes.md");
if review_notes.exists() {
let has_signoff = std::fs::read_to_string(&review_notes)
.map(|s| s.lines().any(|l| l.trim() == "- **Status:** DKP-Reviewed"))
.unwrap_or(false);
if has_signoff {
items.push(Item::pass("review_notes.md sign-off"));
} else {
items.push(Item::warn(
"review_notes.md sign-off",
"file present but missing '- **Status:** DKP-Reviewed' attestation",
));
}
} else {
items.push(Item::warn(
"review_notes.md sign-off",
"evidence/review_notes.md not present",
));
}
if pack.has_bundle_sig() {
items.push(Item::pass("bundle.sig present"));
} else {
items.push(Item::warn(
"bundle.sig present",
"run 'dkp sign' before publishing",
));
}
if pack.manifest.publisher.is_some() {
items.push(Item::pass("manifest.publisher set"));
} else {
items.push(Item::warn(
"manifest.publisher set",
"recommended before registry publish",
));
}
if let Some(stale) = stale_handbook_formats(&pack.root) {
items.push(Item::warn(
"handbook.pdf/.epub up to date",
format!("{stale} older than handbook.md — run `dkp render-handbook`"),
));
} else if pack.root.join("human/handbook.md").exists() {
items.push(Item::pass("handbook.pdf/.epub up to date"));
}
println!(
"Release check — {} v{}\n",
pack.manifest.name, pack.manifest.version
);
let mut any_fail = false;
let mut any_warn = false;
for item in &items {
if item.pass {
println!(" [OK] {}", item.label);
} else if item.is_warning {
println!(" [WARN] {} — {}", item.label, item.detail);
any_warn = true;
} else {
println!(" [FAIL] {} — {}", item.label, item.detail);
any_fail = true;
}
}
println!();
if any_fail || (args.strict && any_warn) {
anyhow::bail!("release check failed");
}
if any_warn {
println!("Release check passed with warnings.");
} else {
println!("Release check passed — pack is ready to publish.");
}
Ok(())
}