spec_driven_docs/gates/
ki_checked_date.rs1use jiff::civil::Date;
17
18use crate::domain::finding::Finding;
19use crate::domain::rule_id::RuleId;
20use crate::gates::ki_record::{STATES, axis};
21use crate::gates::paths::ki_records_judged;
22use crate::gates::{GateCtx, GateResult, Violation, front_matter_values, read_text};
23use crate::services::tracking::today_utc;
24
25pub const CITES: &[RuleId] = &[RuleId::RecordRecordsItsLastCheck];
27
28const RULE: RuleId = RuleId::RecordRecordsItsLastCheck;
29
30const DATED_STATES: [&str; 2] = ["masked", "monitoring"];
32
33fn iso_date(value: &str) -> Option<Date> {
38 let bytes = value.as_bytes();
39 if bytes.len() != 10 || bytes[4] != b'-' || bytes[7] != b'-' {
40 return None;
41 }
42 if !bytes
43 .iter()
44 .enumerate()
45 .all(|(index, byte)| index == 4 || index == 7 || byte.is_ascii_digit())
46 {
47 return None;
48 }
49 value.parse::<Date>().ok()
50}
51
52fn detail(state: &str, values: &[String], as_of: Date) -> Option<String> {
53 let dated = DATED_STATES.contains(&state);
54 match (dated, values) {
55 (false, []) => None,
56 (false, _) => Some(format!("a {state} record states a checked:")),
57 (true, []) => Some(format!("a {state} record states no checked:")),
58 (true, [value]) => match iso_date(value) {
59 None if value.is_empty() => Some(format!("a {state} record states an empty checked:")),
60 None => Some(format!("checked: {value} is not an ISO YYYY-MM-DD date")),
61 Some(date) if date > as_of => Some(format!("checked: {value} is in the future")),
62 Some(_) => None,
63 },
64 (true, many) => Some(format!("{} checked: lines, expected one", many.len())),
65 }
66}
67
68fn judge(ctx: &GateCtx, args: &[String], as_of: Date) -> GateResult {
69 let mut violations = Vec::new();
70 for record in ki_records_judged(ctx, args)? {
71 let text = read_text(ctx, &record)?;
72 let Some(state) = axis(&text, "state", &STATES) else {
76 continue;
77 };
78 let values = front_matter_values(&text, "checked");
79 if let Some(detail) = detail(&state, &values, as_of) {
80 violations.push(Violation::Finding(Finding::on_file(RULE, &record, detail)));
81 }
82 }
83 Ok(violations)
84}
85
86pub fn run(ctx: &GateCtx, args: &[String]) -> GateResult {
92 judge(ctx, args, today_utc())
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::gates::tests_support::ki_fixture_checked;
99
100 const AS_OF: &str = "2026-06-30";
101
102 fn run_on(state: &str, checked_line: &str) -> Vec<String> {
103 let dir = ki_fixture_checked(state, checked_line);
104 let ctx = GateCtx::new(dir.path().to_str().unwrap());
105 judge(&ctx, &[], AS_OF.parse().unwrap())
106 .unwrap()
107 .iter()
108 .map(ToString::to_string)
109 .collect()
110 }
111
112 #[test]
113 fn accepts_a_masked_record_with_a_past_date() {
114 assert!(run_on("masked", "checked: 2026-06-18\n").is_empty());
115 }
116
117 #[test]
118 fn accepts_a_monitoring_record_dated_today() {
119 assert!(run_on("monitoring", "checked: 2026-06-30\n").is_empty());
120 }
121
122 #[test]
123 fn accepts_an_undated_state_carrying_none() {
124 for state in ["investigating", "mitigated"] {
125 assert!(run_on(state, "").is_empty(), "state {state}");
126 }
127 }
128
129 #[test]
130 fn rejects_a_dated_state_carrying_none() {
131 for state in DATED_STATES {
132 let out = run_on(state, "");
133 assert_eq!(out.len(), 1, "state {state}");
134 assert!(out[0].starts_with("FAIL known-issues:a-record-records-its-last-check "));
135 assert!(out[0].ends_with(&format!(": a {state} record states no checked:")));
136 }
137 }
138
139 #[test]
140 fn rejects_an_empty_date() {
141 let out = run_on("masked", "checked:\n");
142 assert_eq!(out.len(), 1);
143 assert!(out[0].ends_with(": a masked record states an empty checked:"));
144 }
145
146 #[test]
147 fn rejects_a_date_that_is_not_iso() {
148 for value in ["2026-6-9", "June 1", "2026-06-18T00:00:00Z", "2026-02-30"] {
149 let out = run_on("masked", &format!("checked: {value}\n"));
150 assert_eq!(out.len(), 1, "value {value}");
151 assert!(out[0].ends_with(&format!(": checked: {value} is not an ISO YYYY-MM-DD date")));
152 }
153 }
154
155 #[test]
156 fn rejects_a_date_in_the_future() {
157 let out = run_on("masked", "checked: 2026-07-01\n");
158 assert_eq!(out.len(), 1);
159 assert!(out[0].ends_with(": checked: 2026-07-01 is in the future"));
160 }
161
162 #[test]
163 fn rejects_an_undated_state_carrying_one() {
164 let out = run_on("mitigated", "checked: 2026-06-18\n");
165 assert_eq!(out.len(), 1);
166 assert!(out[0].ends_with(": a mitigated record states a checked:"));
167 }
168
169 #[test]
170 fn rejects_two_dates() {
171 let out = run_on("masked", "checked: 2026-06-18\nchecked: 2026-06-19\n");
172 assert_eq!(out.len(), 1);
173 assert!(out[0].ends_with(": 2 checked: lines, expected one"));
174 }
175
176 #[test]
177 fn leaves_an_unjudgeable_state_to_the_state_gate() {
178 assert!(run_on("closed", "").is_empty());
179 }
180
181 #[test]
182 fn the_real_clock_accepts_a_past_date() {
183 let dir = ki_fixture_checked("masked", "checked: 2020-01-01\n");
184 let ctx = GateCtx::new(dir.path().to_str().unwrap());
185 assert!(run(&ctx, &[]).unwrap().is_empty());
186 }
187}