Skip to main content

spec_driven_docs/gates/
ki_report_body.rs

1//! Gate: a filed known-issue record carries the body it was filed with,
2//! and the issue it was filed as.
3//!
4//! Once `filing:` reads `filed`, the report exists in two places and only
5//! one of them is under review here. What the gate can see is the pairing:
6//! a named upstream issue and a `## Report` section. That the section is
7//! the filed text, in the tracker's markup, is held by review.
8
9use crate::domain::finding::Finding;
10use crate::domain::rule_id::RuleId;
11use crate::gates::ki_record::{FILINGS, axis};
12use crate::gates::paths::ki_records;
13use crate::gates::{GateCtx, GateResult, Violation, front_matter_values, read_text};
14
15/// The rules this gate can cite.
16pub const CITES: &[RuleId] = &[RuleId::FiledRecordCarriesItsReport];
17
18const RULE: RuleId = RuleId::FiledRecordCarriesItsReport;
19
20/// Judge every known-issue record under the resolved roots.
21///
22/// # Errors
23///
24/// [`crate::gates::GateError::Io`] when a record cannot be read.
25pub fn run(ctx: &GateCtx, args: &[String]) -> GateResult {
26    let mut violations = Vec::new();
27    for record in ki_records(ctx, args)? {
28        let text = read_text(ctx, &record)?;
29        if axis(&text, "filing", &FILINGS).as_deref() != Some("filed") {
30            continue;
31        }
32        let named = front_matter_values(&text, "upstream")
33            .iter()
34            .any(|value| !value.is_empty());
35        if !named {
36            violations.push(Violation::Finding(Finding::on_file(
37                RULE,
38                &record,
39                "a filed record names no upstream:",
40            )));
41        }
42        if !text.lines().any(|line| line == "## Report") {
43            violations.push(Violation::Finding(Finding::on_file(
44                RULE,
45                &record,
46                "a filed record carries no ## Report section",
47            )));
48        }
49    }
50    Ok(violations)
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56    use crate::gates::tests_support::ki_fixture_filing;
57
58    fn run_on(filing: &str, upstream: &str, body: &str) -> Vec<String> {
59        let dir = ki_fixture_filing(filing, upstream, body);
60        let ctx = GateCtx::new(dir.path().to_str().unwrap());
61        run(&ctx, &[])
62            .unwrap()
63            .iter()
64            .map(ToString::to_string)
65            .collect()
66    }
67
68    #[test]
69    fn an_unfiled_record_needs_no_report() {
70        for filing in ["gathering", "ready", "deferred"] {
71            assert!(
72                run_on(filing, "https://example.invalid/issues", "# V\n").is_empty(),
73                "filing {filing:?}"
74            );
75        }
76    }
77
78    #[test]
79    fn a_filed_record_demands_the_report_section() {
80        let out = run_on("filed", "https://example.invalid/issues/123", "# V\n");
81        assert_eq!(out.len(), 1);
82        assert!(out[0].starts_with("FAIL known-issues:a-filed-record-carries-its-report "));
83        assert!(out[0].ends_with(": a filed record carries no ## Report section"));
84    }
85
86    #[test]
87    fn a_filed_record_demands_the_issue_it_was_filed_as() {
88        let out = run_on("filed", "", "# V\n## Report\n```text\nbody\n```\n");
89        assert_eq!(out.len(), 1);
90        assert!(out[0].ends_with(": a filed record names no upstream:"));
91    }
92
93    #[test]
94    fn a_filed_record_with_its_report_passes() {
95        assert!(
96            run_on(
97                "filed",
98                "https://example.invalid/issues/123",
99                "# V\n## Report\n```text\nbody\n```\n"
100            )
101            .is_empty()
102        );
103    }
104}