Skip to main content

spec_driven_docs/gates/
ki_retire_when.rs

1//! Gate: a masked known-issue record carries the condition under which it
2//! is removed, and a mitigated one carries none.
3//!
4//! A mask whose workaround has no exit becomes permanent by default, and
5//! the next reader takes it for a design choice. A mitigation is the
6//! opposite case: it is part of the design and stays after the upstream
7//! fix, so a retire condition on one states an exit that never arrives.
8//! The key must carry a value: an empty `retire_when:` states no
9//! condition. Whether the condition is a good one is review's business.
10
11use crate::domain::finding::Finding;
12use crate::domain::rule_id::RuleId;
13use crate::gates::ki_record::{STATES, axis};
14use crate::gates::paths::ki_records_judged;
15use crate::gates::{GateCtx, GateResult, Violation, front_matter_values, read_text};
16
17/// The rules this gate can cite.
18pub const CITES: &[RuleId] = &[RuleId::RecordCarriesItsRetirementCondition];
19
20const RULE: RuleId = RuleId::RecordCarriesItsRetirementCondition;
21
22/// Judge every known-issue record under the resolved roots.
23///
24/// # Errors
25///
26/// [`crate::gates::GateError::Io`] when a record cannot be read.
27pub fn run(ctx: &GateCtx, args: &[String]) -> GateResult {
28    let mut violations = Vec::new();
29    for record in ki_records_judged(ctx, args)? {
30        let text = read_text(ctx, &record)?;
31        // A record whose state is missing or invalid is `ki-state`'s to
32        // report; judging its condition here would name the same defect
33        // twice under a rule that does not own it.
34        let Some(state) = axis(&text, "state", &STATES) else {
35            continue;
36        };
37        let carries = front_matter_values(&text, "retire_when")
38            .iter()
39            .any(|value| !value.is_empty());
40        let detail = match (state.as_str(), carries) {
41            ("masked", false) => "a masked record states no retire_when:",
42            ("mitigated", true) => "a mitigated record states a retire_when:",
43            _ => continue,
44        };
45        violations.push(Violation::Finding(Finding::on_file(RULE, &record, detail)));
46    }
47    Ok(violations)
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use crate::gates::tests_support::ki_fixture_state;
54
55    fn run_on(state: &str, retire_line: &str) -> Vec<String> {
56        let dir = ki_fixture_state(state, retire_line);
57        let ctx = GateCtx::new(dir.path().to_str().unwrap());
58        run(&ctx, &[])
59            .unwrap()
60            .iter()
61            .map(ToString::to_string)
62            .collect()
63    }
64
65    #[test]
66    fn accepts_a_masked_record_with_a_condition() {
67        assert!(run_on("masked", "retire_when: release >= 2.0\n").is_empty());
68    }
69
70    #[test]
71    fn accepts_a_mitigated_record_without_one() {
72        assert!(run_on("mitigated", "").is_empty());
73    }
74
75    #[test]
76    fn rejects_a_masked_record_with_a_missing_or_empty_condition() {
77        for retire_line in ["retire_when:\n", ""] {
78            let out = run_on("masked", retire_line);
79            assert_eq!(out.len(), 1, "retire line {retire_line:?}");
80            assert!(
81                out[0].starts_with("FAIL known-issues:a-record-carries-its-retirement-condition ")
82            );
83            assert!(out[0].ends_with(": a masked record states no retire_when:"));
84        }
85    }
86
87    #[test]
88    fn rejects_a_mitigated_record_carrying_a_condition() {
89        let out = run_on("mitigated", "retire_when: release >= 2.0\n");
90        assert_eq!(out.len(), 1);
91        assert!(out[0].ends_with(": a mitigated record states a retire_when:"));
92    }
93
94    #[test]
95    fn leaves_an_unjudgeable_state_to_the_state_gate() {
96        assert!(run_on("closed", "").is_empty());
97    }
98}