cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
//! Warn when a terminal-status parent still has an open child — likely
//! a forgotten close. Severity is warning, not error.

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
}