koala-drift 1.0.4

Wiki ↔ code drift detector.
Documentation
use koala_core::invariant::Context;
use std::path::PathBuf;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
    /// Blocks the PR. Either fix the claim or fix the code.
    Hard,
    /// Reported but never blocks. Reserved for sleep/dormancy signals
    /// (see ADR-0010).
    Advisory,
}

impl Severity {
    pub fn label(&self) -> &'static str {
        match self {
            Self::Hard => "",
            Self::Advisory => "",
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FindingKind {
    /// Acceptance criterion points to a path that does not exist.
    AcceptanceTestRefMissing,
    /// `[ADR-NNNN]` reference cannot be resolved to a decision file.
    AdrRefDangling,
    /// Reference targets an ADR with `status: superseded`.
    AdrRefSuperseded { superseder: Option<String> },
    /// Accepted ADR has zero inbound references in the wiki tree.
    AdrDormant,
    /// Gap in ADR id sequence — implies an ADR file was deleted (ADR-0008).
    AdrIdGap { missing: u32 },
    /// Auto-generated Tier 1 file's body checksum disagrees with the
    /// embedded `<!-- Checksum-of-body-below -->` header.
    Tier1Tampered { expected: String, actual: String },
    /// A Tier 2/3 file scaffolded by `koala-core init` still contains
    /// an unsubstituted `<...>` placeholder — the file was never filled
    /// in.
    TemplatePlaceholderUnfilled,
    /// A feature whose frontmatter claims `status: done` still has an
    /// unchecked `- [ ]` item in its `## Acceptance criteria` section.
    AcceptanceItemUnchecked,
    /// The ADR supersede graph is not clean — a cycle, dangling target,
    /// status/pointer mismatch, or asymmetric link reported by
    /// `koala_adr::validate` (see ADR-0018).
    AdrGraphUnclean,
}

impl FindingKind {
    pub fn short(&self) -> &'static str {
        match self {
            Self::AcceptanceTestRefMissing => "acceptance test ref missing",
            Self::AdrRefDangling => "ADR reference does not resolve",
            Self::AdrRefSuperseded { .. } => "ADR reference is superseded",
            Self::AdrDormant => "ADR has no inbound references",
            Self::AdrIdGap { .. } => "ADR id is missing — file was deleted",
            Self::Tier1Tampered { .. } => "Tier 1 file body edited by hand",
            Self::TemplatePlaceholderUnfilled => "scaffolded placeholder never filled in",
            Self::AcceptanceItemUnchecked => "done feature has an unchecked acceptance item",
            Self::AdrGraphUnclean => "ADR supersede graph is not clean",
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Finding {
    pub check_id: &'static str,
    /// Wiki file the claim lives in, relative to the repo root for display.
    pub file: PathBuf,
    /// 1-indexed line number of the claim.
    pub line: usize,
    /// Verbatim claim text (trimmed).
    pub claim: String,
    pub kind: FindingKind,
    pub severity: Severity,
    pub fix_hint: Option<String>,
}

pub trait Check: Send + Sync {
    fn id(&self) -> &'static str;
    fn intent(&self) -> &'static str;
    fn run(&self, ctx: &Context) -> Vec<Finding>;
}