nodex_core/query/
detect.rs1use chrono::{Local, NaiveDate};
2
3use crate::config::Config;
4use crate::model::Graph;
5
6#[derive(Debug, serde::Serialize)]
7pub struct OrphanEntry {
8 pub id: String,
9 pub title: String,
10 pub kind: String,
11 pub path: String,
12 pub created: Option<NaiveDate>,
13}
14
15pub fn find_orphans(graph: &Graph, config: &Config) -> Vec<OrphanEntry> {
17 let today = Local::now().date_naive();
18 let Some(grace_cutoff) = today.checked_sub_days(chrono::Days::new(u64::from(
23 config.detection.orphan_grace_days,
24 ))) else {
25 return Vec::new();
26 };
27
28 let mut orphans: Vec<OrphanEntry> = graph
29 .nodes()
30 .values()
31 .filter(|node| {
32 if config.is_orphan_ok_kind(node.kind.as_str()) {
34 return false;
35 }
36
37 if node.orphan_ok {
39 return false;
40 }
41
42 if !graph.incoming_indices(&node.id).is_empty() {
44 return false;
45 }
46
47 if let Some(created) = node.created
49 && created > grace_cutoff
50 {
51 return false;
52 }
53
54 true
55 })
56 .map(|node| OrphanEntry {
57 id: node.id.clone(),
58 title: node.title.clone(),
59 kind: node.kind.to_string(),
60 path: node.path.to_string_lossy().to_string(),
61 created: node.created,
62 })
63 .collect();
64
65 orphans.sort_by(|a, b| a.id.cmp(&b.id));
66 orphans
67}
68
69#[derive(Debug, serde::Serialize)]
70pub struct StaleEntry {
71 pub id: String,
72 pub title: String,
73 pub path: String,
74 pub reviewed: NaiveDate,
75 pub days_since: i64,
76}
77
78pub fn find_stale(graph: &Graph, config: &Config) -> Vec<StaleEntry> {
80 let today = Local::now().date_naive();
81 let Some(cutoff) =
83 today.checked_sub_days(chrono::Days::new(u64::from(config.detection.stale_days)))
84 else {
85 return Vec::new();
86 };
87
88 let mut stale: Vec<StaleEntry> = graph
89 .nodes()
90 .values()
91 .filter(|node| {
92 if config.is_terminal(node.status.as_str()) {
94 return false;
95 }
96
97 match node.reviewed {
99 Some(reviewed) => reviewed < cutoff,
100 None => false, }
102 })
103 .filter_map(|node| {
104 let reviewed = node.reviewed?; Some(StaleEntry {
106 id: node.id.clone(),
107 title: node.title.clone(),
108 path: node.path.to_string_lossy().to_string(),
109 reviewed,
110 days_since: (today - reviewed).num_days(),
111 })
112 })
113 .collect();
114
115 stale.sort_by(|a, b| a.reviewed.cmp(&b.reviewed).then_with(|| a.id.cmp(&b.id)));
116 stale
117}