use crate::check::{Check, Finding, FindingKind, Severity};
use koala_core::invariant::Context;
use koala_wiki::{verify_tier1_checksums, Tier1Status};
pub struct Tier1Integrity;
impl Check for Tier1Integrity {
fn id(&self) -> &'static str {
"tier1.no-hand-edit"
}
fn intent(&self) -> &'static str {
"Auto-generated Tier 1 files must match the body checksum \
embedded in their header — hand edits are forbidden, run \
`koala-core wiki gen` to regenerate."
}
fn run(&self, ctx: &Context) -> Vec<Finding> {
let mut out = Vec::new();
for r in verify_tier1_checksums(ctx.root()) {
if let Tier1Status::Tampered { expected, actual } = r.status {
out.push(Finding {
check_id: self.id(),
file: r.path.clone(),
line: 0,
claim: format!(
"body checksum mismatch (expected sha256:{}, got sha256:{})",
short(&expected),
short(&actual)
),
kind: FindingKind::Tier1Tampered { expected, actual },
severity: Severity::Hard,
fix_hint: Some(format!(
"this file is auto-generated; revert hand edits and run \
`koala-core wiki gen` to refresh `{}`",
r.path.display()
)),
});
}
}
out
}
}
fn short(hex: &str) -> &str {
hex.get(..12).unwrap_or(hex)
}
#[cfg(test)]
mod tests {
use super::*;
use koala_wiki::gen;
use std::fs;
use tempfile::TempDir;
fn write_feature(dir: &std::path::Path, name: &str, fm: &str) {
let p = dir.join("wiki/features").join(name);
fs::create_dir_all(p.parent().unwrap()).unwrap();
fs::write(p, format!("---\n{fm}\n---\n\n# {name}\n")).unwrap();
}
#[test]
fn clean_repo_no_findings() {
let tmp = TempDir::new().unwrap();
write_feature(tmp.path(), "x.md", "id: x\nstatus: done\ntags: [core]\n");
gen(tmp.path()).unwrap();
let ctx = Context::new(tmp.path().to_path_buf());
let findings = Tier1Integrity.run(&ctx);
assert!(findings.is_empty(), "{findings:?}");
}
#[test]
fn hand_edit_blocks() {
let tmp = TempDir::new().unwrap();
write_feature(tmp.path(), "x.md", "id: x\nstatus: done\ntags: [core]\n");
gen(tmp.path()).unwrap();
let path = tmp.path().join("wiki/features/_index.md");
let body = fs::read_to_string(&path).unwrap();
fs::write(&path, format!("{body}\nUNAUTHORIZED\n")).unwrap();
let ctx = Context::new(tmp.path().to_path_buf());
let findings = Tier1Integrity.run(&ctx);
assert!(
findings
.iter()
.any(|f| matches!(f.kind, FindingKind::Tier1Tampered { .. })),
"expected Tier1Tampered finding, got {findings:?}"
);
}
}