Skip to main content

nodex_core/rules/
freshness.rs

1use chrono::Local;
2
3use crate::config::Config;
4use crate::model::Graph;
5
6use super::{Rule, Severity, Violation};
7
8/// Warn about active documents not reviewed within the threshold.
9pub struct StaleReviewRule;
10
11impl Rule for StaleReviewRule {
12    fn id(&self) -> &str {
13        "stale_review"
14    }
15
16    fn severity(&self) -> Severity {
17        Severity::Warning
18    }
19
20    fn check(&self, graph: &Graph, config: &Config) -> Vec<Violation> {
21        let today = Local::now().date_naive();
22        // `stale_days` is a user-supplied u32; subtract via the checked
23        // API so a pathological `u32::MAX` doesn't panic the whole CLI.
24        // If the cutoff underflows chrono's representable range, treat
25        // every doc as within threshold (nothing is stale).
26        let Some(cutoff) =
27            today.checked_sub_days(chrono::Days::new(u64::from(config.detection.stale_days)))
28        else {
29            return Vec::new();
30        };
31
32        graph
33            .nodes()
34            .values()
35            .filter_map(|node| {
36                if config.is_terminal(node.status.as_str()) {
37                    return None;
38                }
39                let reviewed = node.reviewed?;
40                if reviewed >= cutoff {
41                    return None;
42                }
43                let days = (today - reviewed).num_days();
44                Some(Violation {
45                    rule_id: self.id().to_string(),
46                    severity: self.severity(),
47                    node_id: Some(node.id.clone()),
48                    path: Some(node.path.to_string_lossy().to_string()),
49                    message: format!(
50                        "not reviewed for {days} days (threshold: {} days)",
51                        config.detection.stale_days
52                    ),
53                })
54            })
55            .collect()
56    }
57}