1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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>;
}