Skip to main content

spec_driven_docs/gates/
ki_report_body.rs

1//! Gate: a record filed upstream carries the body it was filed with.
2//!
3//! Once `upstream:` names one issue rather than a tracker, the report exists
4//! in two places, and only one of them is under review here. What the gate
5//! can see is the pairing: a specific upstream reference and a `## Report`
6//! section. That the section is the filed text, in the tracker's markup, is
7//! held by review.
8
9use std::sync::LazyLock;
10
11use regex::Regex;
12
13use crate::domain::finding::Finding;
14use crate::domain::rule_id::RuleId;
15use crate::gates::paths::ki_records;
16use crate::gates::{GateCtx, GateResult, Violation, read_text};
17
18/// The rules this gate can cite.
19pub const CITES: &[RuleId] = &[RuleId::FiledRecordCarriesItsReport];
20
21/// A specific item: an id introduced by `/`, `#` or `=` anywhere in the
22/// value. The `=` form is a canonical Bugzilla link's `show_bug.cgi?id=`,
23/// and the right edge is unanchored because a citation routinely carries
24/// more than the id — a comment anchor, a status note, or a second link.
25static FILED: LazyLock<Regex> = LazyLock::new(|| {
26    Regex::new(r"^[ \t]*[Uu]pstream:.*[/#=][0-9]+").unwrap_or_else(|_| unreachable!())
27});
28
29/// Judge every known-issue record under the resolved roots.
30///
31/// # Errors
32///
33/// [`crate::gates::GateError::Io`] when a record cannot be read.
34pub fn run(ctx: &GateCtx, args: &[String]) -> GateResult {
35    let mut bad = String::new();
36    for record in ki_records(ctx, args) {
37        let text = read_text(ctx, &record)?;
38        if text.lines().any(|line| FILED.is_match(line))
39            && !text.lines().any(|line| line == "## Report")
40        {
41            bad.push(' ');
42            bad.push_str(record.as_str());
43        }
44    }
45    if bad.is_empty() {
46        Ok(vec![])
47    } else {
48        Ok(vec![Violation::Finding(Finding::global(
49            RuleId::FiledRecordCarriesItsReport,
50            bad.trim_start().to_string(),
51        ))])
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use crate::gates::tests_support::ki_fixture_upstream;
59
60    fn run_on(upstream: &str, body: &str) -> Vec<String> {
61        let dir = ki_fixture_upstream(upstream, body);
62        let ctx = GateCtx::new(dir.path().to_str().unwrap());
63        run(&ctx, &[])
64            .unwrap()
65            .iter()
66            .map(ToString::to_string)
67            .collect()
68    }
69
70    #[test]
71    fn a_tracker_reference_needs_no_report() {
72        assert!(run_on("https://example.invalid/issues", "# V\n").is_empty());
73    }
74
75    #[test]
76    fn a_filed_reference_demands_the_report_section() {
77        for upstream in [
78            "https://example.invalid/issues/123",
79            "https://example.invalid/issues/123 (open)",
80            "https://example.invalid/issues/123#c4",
81            "https://bugzilla.example/show_bug.cgi?id=123",
82        ] {
83            let out = run_on(upstream, "# V\n");
84            assert_eq!(out.len(), 1, "upstream {upstream:?}");
85            assert!(out[0].starts_with("FAIL known-issues:a-filed-record-carries-its-report: "));
86        }
87    }
88
89    #[test]
90    fn a_filed_record_with_its_report_passes() {
91        assert!(
92            run_on(
93                "https://example.invalid/issues/123",
94                "# V\n## Report\n```text\nbody\n```\n"
95            )
96            .is_empty()
97        );
98    }
99}