Skip to main content

koala_drift/checks/
tier1_integrity.rs

1//! `tier1.no-hand-edit` — every auto-generated Tier 1 file
2//! (`wiki/features/_index.md`, `wiki/decisions/_index.md`,
3//! `wiki/_tags/*.md`, `wiki/health.md`) must round-trip through its
4//! embedded `<!-- Checksum-of-body-below -->` header. Hand edits are
5//! rejected so the wiki stays the source of truth.
6
7use 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}