koala_drift/checks/
tier1_integrity.rs1use crate::check::{Check, Finding, FindingKind, Severity};
8use koala_core::invariant::Context;
9use koala_wiki::{verify_tier1_checksums, Tier1Status};
10
11pub struct Tier1Integrity;
12
13impl Check for Tier1Integrity {
14 fn id(&self) -> &'static str {
15 "tier1.no-hand-edit"
16 }
17
18 fn intent(&self) -> &'static str {
19 "Auto-generated Tier 1 files must match the body checksum \
20 embedded in their header — hand edits are forbidden, run \
21 `koala-core wiki gen` to regenerate."
22 }
23
24 fn run(&self, ctx: &Context) -> Vec<Finding> {
25 let mut out = Vec::new();
26 for r in verify_tier1_checksums(ctx.root()) {
27 if let Tier1Status::Tampered { expected, actual } = r.status {
28 out.push(Finding {
29 check_id: self.id(),
30 file: r.path.clone(),
31 line: 0,
32 claim: format!(
33 "body checksum mismatch (expected sha256:{}, got sha256:{})",
34 short(&expected),
35 short(&actual)
36 ),
37 kind: FindingKind::Tier1Tampered { expected, actual },
38 severity: Severity::Hard,
39 fix_hint: Some(format!(
40 "this file is auto-generated; revert hand edits and run \
41 `koala-core wiki gen` to refresh `{}`",
42 r.path.display()
43 )),
44 });
45 }
46 }
47 out
48 }
49}
50
51fn short(hex: &str) -> &str {
52 hex.get(..12).unwrap_or(hex)
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58 use koala_wiki::gen;
59 use std::fs;
60 use tempfile::TempDir;
61
62 fn write_feature(dir: &std::path::Path, name: &str, fm: &str) {
63 let p = dir.join("wiki/features").join(name);
64 fs::create_dir_all(p.parent().unwrap()).unwrap();
65 fs::write(p, format!("---\n{fm}\n---\n\n# {name}\n")).unwrap();
66 }
67
68 #[test]
69 fn clean_repo_no_findings() {
70 let tmp = TempDir::new().unwrap();
71 write_feature(tmp.path(), "x.md", "id: x\nstatus: done\ntags: [core]\n");
72 gen(tmp.path()).unwrap();
73 let ctx = Context::new(tmp.path().to_path_buf());
74 let findings = Tier1Integrity.run(&ctx);
75 assert!(findings.is_empty(), "{findings:?}");
76 }
77
78 #[test]
79 fn hand_edit_blocks() {
80 let tmp = TempDir::new().unwrap();
81 write_feature(tmp.path(), "x.md", "id: x\nstatus: done\ntags: [core]\n");
82 gen(tmp.path()).unwrap();
83 let path = tmp.path().join("wiki/features/_index.md");
84 let body = fs::read_to_string(&path).unwrap();
85 fs::write(&path, format!("{body}\nUNAUTHORIZED\n")).unwrap();
86 let ctx = Context::new(tmp.path().to_path_buf());
87 let findings = Tier1Integrity.run(&ctx);
88 assert!(
89 findings
90 .iter()
91 .any(|f| matches!(f.kind, FindingKind::Tier1Tampered { .. })),
92 "expected Tier1Tampered finding, got {findings:?}"
93 );
94 }
95}