use crate::check::{Check, Finding, FindingKind, Severity};
use crate::scan::{list_adr_files, list_all_wiki_md, rel};
use koala_core::invariant::Context;
use koala_core::wiki::{extract_frontmatter, parse_yaml_frontmatter};
use regex::Regex;
use std::fs;
use std::path::Path;
use std::sync::OnceLock;
pub struct AdrDormancy;
fn adr_pattern() -> &'static Regex {
static R: OnceLock<Regex> = OnceLock::new();
R.get_or_init(|| Regex::new(r"\bADR-(\d{4})\b").expect("static regex compiles"))
}
impl Check for AdrDormancy {
fn id(&self) -> &'static str {
"adr.dormancy-advisory"
}
fn intent(&self) -> &'static str {
"Accepted ADRs that nothing in the wiki references any more get an \
advisory ping for the next pruning sprint."
}
fn run(&self, ctx: &Context) -> Vec<Finding> {
let adrs = list_adr_files(ctx.root());
if adrs.is_empty() {
return Vec::new();
}
let mut inbound: std::collections::HashMap<String, usize> =
std::collections::HashMap::new();
let all = list_all_wiki_md(ctx.root());
let adr_paths: std::collections::HashSet<&Path> =
adrs.iter().map(|p| p.as_path()).collect();
for path in &all {
if adr_paths.contains(path.as_path()) {
continue;
}
let Ok(content) = fs::read_to_string(path) else {
continue;
};
let mut seen_in_file: std::collections::HashSet<String> =
std::collections::HashSet::new();
for caps in adr_pattern().captures_iter(&content) {
let id = caps.get(1).expect("group 1").as_str().to_string();
seen_in_file.insert(id);
}
for id in seen_in_file {
*inbound.entry(id).or_default() += 1;
}
}
let mut out = Vec::new();
for path in adrs {
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
let id = &name[..4];
let Ok(content) = fs::read_to_string(&path) else {
continue;
};
let fm = extract_frontmatter(&content).unwrap_or("");
let fields = parse_yaml_frontmatter(fm);
let status = fields
.get("status")
.map(String::as_str)
.unwrap_or("unknown");
if status != "accepted" {
continue;
}
if inbound.get(id).copied().unwrap_or(0) > 0 {
continue;
}
out.push(Finding {
check_id: self.id(),
file: rel(&path, ctx.root()),
line: 1,
claim: format!("ADR-{id} accepted with no inbound wiki references"),
kind: FindingKind::AdrDormant,
severity: Severity::Advisory,
fix_hint: Some(
"next pruning sprint: confirm still load-bearing or supersede".to_string(),
),
});
}
out
}
}