use crate::check::{Check, Finding, FindingKind, Severity};
use crate::scan::{list_feature_files, rel};
use koala_core::invariant::Context;
use std::fs;
const DONE_STATUSES: &[&str] = &["done", "implemented", "shipped"];
pub struct FeatureStatusHasImpl;
impl Check for FeatureStatusHasImpl {
fn id(&self) -> &'static str {
"feature.status-done-has-impl"
}
fn intent(&self) -> &'static str {
"A feature whose frontmatter claims status=done (or implemented) \
must point at an `owner` directory containing real source code."
}
fn run(&self, ctx: &Context) -> Vec<Finding> {
let mut out = Vec::new();
for feature in list_feature_files(ctx.root()) {
let Ok(content) = fs::read_to_string(&feature) else {
continue;
};
let Some((status, owner, status_line)) = parse_status_owner(&content) else {
continue;
};
if !DONE_STATUSES
.iter()
.any(|s| s.eq_ignore_ascii_case(&status))
{
continue;
}
let display = rel(&feature, ctx.root());
let owner_dir = ctx.root().join(owner.trim_end_matches('/'));
if !owner_dir.is_dir() {
out.push(Finding {
check_id: self.id(),
file: display.clone(),
line: status_line,
claim: format!("status: {status}; owner: {owner}"),
kind: FindingKind::AcceptanceTestRefMissing,
severity: Severity::Hard,
fix_hint: Some(format!(
"owner directory `{owner}` doesn't exist; either implement it or \
set status to `in-progress` / `planned`",
)),
});
continue;
}
if !contains_rust_source(&owner_dir) {
out.push(Finding {
check_id: self.id(),
file: display.clone(),
line: status_line,
claim: format!("status: {status}; owner: {owner}"),
kind: FindingKind::AcceptanceTestRefMissing,
severity: Severity::Hard,
fix_hint: Some(format!(
"owner directory `{owner}` has no .rs files yet; either start \
the implementation or set status back to `in-progress`",
)),
});
}
}
out
}
}
fn parse_status_owner(content: &str) -> Option<(String, String, usize)> {
let rest = content.strip_prefix("---\n")?;
let end = rest.find("\n---\n")?;
let front = &rest[..end];
let mut status: Option<String> = None;
let mut owner: Option<String> = None;
let mut status_line: usize = 1;
for (i, line) in front.lines().enumerate() {
let Some((k, v)) = line.split_once(':') else {
continue;
};
match k.trim() {
"status" => {
status = Some(v.trim().to_string());
status_line = i + 2; }
"owner" => owner = Some(v.trim().to_string()),
_ => {}
}
}
Some((status?, owner?, status_line))
}
fn contains_rust_source(dir: &std::path::Path) -> bool {
use walkdir::WalkDir;
WalkDir::new(dir)
.into_iter()
.filter_map(Result::ok)
.any(|e| {
e.file_type().is_file() && e.path().extension().and_then(|s| s.to_str()) == Some("rs")
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn setup(tmp: &TempDir, feature_body: &str, source_present: bool) {
fs::create_dir_all(tmp.path().join("wiki/features")).unwrap();
fs::write(tmp.path().join("wiki/features/x.md"), feature_body).unwrap();
fs::create_dir_all(tmp.path().join("crates/x/src")).unwrap();
if source_present {
fs::write(tmp.path().join("crates/x/src/lib.rs"), "fn x() {}\n").unwrap();
}
}
#[test]
fn done_without_source_blocks() {
let tmp = TempDir::new().unwrap();
setup(
&tmp,
"---\nid: x\nstatus: done\nowner: crates/x/\n---\n# x\n",
false,
);
let ctx = Context::new(tmp.path().to_path_buf());
let f = FeatureStatusHasImpl.run(&ctx);
assert_eq!(f.len(), 1);
assert_eq!(f[0].severity, Severity::Hard);
}
#[test]
fn implemented_with_source_passes() {
let tmp = TempDir::new().unwrap();
setup(
&tmp,
"---\nid: x\nstatus: implemented\nowner: crates/x/\n---\n# x\n",
true,
);
let ctx = Context::new(tmp.path().to_path_buf());
assert!(FeatureStatusHasImpl.run(&ctx).is_empty());
}
#[test]
fn in_progress_status_skipped() {
let tmp = TempDir::new().unwrap();
setup(
&tmp,
"---\nid: x\nstatus: in-progress\nowner: crates/x/\n---\n# x\n",
false,
);
let ctx = Context::new(tmp.path().to_path_buf());
assert!(FeatureStatusHasImpl.run(&ctx).is_empty());
}
}