use crate::domain::model::check::{CheckViolationKind, Severity};
use crate::domain::model::issue::Issue;
use crate::domain::model::record_ref::IssueRef;
use crate::domain::usecases::check::{CheckViolation, IssueCheckCtx, IssueFinding, IssueRule};
pub struct TerminalParentOpenChildRule;
pub const RULE_ID: &str = "issue/terminal-parent-open-child";
impl IssueRule for TerminalParentOpenChildRule {
fn id(&self) -> &'static str {
RULE_ID
}
fn find(&self, ctx: &IssueCheckCtx<'_>) -> anyhow::Result<Vec<IssueFinding>> {
let issues: Vec<Issue> = ctx.issues.iter().map(|(_, i)| i.clone()).collect();
let mut out = Vec::new();
for (parent_id, kind) in find_violations(&issues) {
let Some(path) = ctx.path_of(&parent_id) else {
continue;
};
out.push(IssueFinding::report(CheckViolation {
rule_id: RULE_ID,
path: path.to_path_buf(),
severity: Severity::Warning,
kind,
}));
}
Ok(out)
}
}
fn find_violations(issues: &[Issue]) -> Vec<(IssueRef, CheckViolationKind)> {
use std::collections::HashMap;
let by_id: HashMap<&IssueRef, &Issue> = issues.iter().map(|i| (&i.id, i)).collect();
let mut out: Vec<(IssueRef, CheckViolationKind)> = Vec::new();
for parent in issues {
if !parent.status.terminal {
continue;
}
let mut open_children: Vec<&Issue> = Vec::new();
for child_ref in parent.children() {
if let Some(child) = by_id.get(child_ref) {
if !child.status.terminal {
open_children.push(*child);
}
}
}
if open_children.is_empty() {
continue;
}
open_children.sort_by(|a, b| a.id.cmp(&b.id));
let kind = CheckViolationKind::TerminalParentWithOpenChildren {
parent: parent.id.as_entity_ref().clone(),
parent_status: parent.status.as_str().to_string(),
open_children: open_children
.iter()
.map(|c| c.id.as_entity_ref().clone())
.collect(),
};
out.push((parent.id.clone(), kind));
}
out.sort_by(|a, b| a.0.cmp(&b.0));
out
}