Skip to main content

spec_driven_docs/gates/
gate_message_cites_a_rule.rs

1//! Gate: every rule ID a gate can print resolves to a requirement in the
2//! local specs.
3//!
4//! The id in a failure message is an address: the reader follows it to the
5//! sentence that binds, and a message naming an id no spec defines sends
6//! them nowhere. The citable set comes from the compiled gate registry —
7//! every rule a delivered gate declares — and the defined set from the
8//! instance's own specs, so a spec rewrite that renames a rule fails here
9//! before a gate ever prints the stale address.
10
11use std::collections::BTreeSet;
12
13use crate::domain::finding::Finding;
14use crate::domain::rule_id::RuleId;
15use crate::gates::paths::docs_root;
16use crate::gates::{GateCtx, GateResult, Violation, read_text};
17
18/// The rules this gate can cite.
19pub const CITES: &[RuleId] = &[RuleId::GateMessageCitesTheRule];
20
21/// Judge the registry's citable set against the local specs.
22///
23/// # Errors
24///
25/// [`crate::gates::GateError::Io`] when a local spec cannot be read.
26pub fn run(ctx: &GateCtx, _files: &[String]) -> GateResult {
27    let specs = docs_root(ctx).join("specs");
28    let Ok(entries) = ctx.path(&specs).read_dir_utf8() else {
29        return Ok(vec![Violation::Layout(format!(
30            "no specs directory at {specs}; the layout moved"
31        ))]);
32    };
33    let mut defined: BTreeSet<String> = BTreeSet::new();
34    for entry in entries.filter_map(Result::ok) {
35        let name = entry.file_name();
36        // The corpus convention is lowercase; `.MD` is not a spec.
37        #[allow(clippy::case_sensitive_file_extension_comparisons)]
38        if !name.ends_with(".md") {
39            continue;
40        }
41        defined.extend(crate::embedded::rule_ids_in(&read_text(
42            ctx,
43            specs.join(name),
44        )?));
45    }
46
47    let cited: BTreeSet<RuleId> = crate::gates::GATES
48        .iter()
49        .flat_map(|gate| gate.cites.iter().copied())
50        .collect();
51    Ok(cited
52        .into_iter()
53        .filter(|rule| !defined.contains(rule.as_str()))
54        .map(|rule| {
55            Violation::Finding(Finding::global(
56                RuleId::GateMessageCitesTheRule,
57                format!("{rule} resolves to no requirement"),
58            ))
59        })
60        .collect())
61}
62
63#[cfg(test)]
64mod tests {
65    use std::fmt::Write as _;
66
67    use super::*;
68
69    fn write_spec(dir: &tempfile::TempDir, name: &str, text: &str) {
70        let specs = dir.path().join("_docs/specs");
71        std::fs::create_dir_all(&specs).unwrap();
72        std::fs::write(specs.join(name), text).unwrap();
73    }
74
75    fn run_in(dir: &tempfile::TempDir) -> Vec<String> {
76        let ctx = GateCtx::new(dir.path().to_str().unwrap());
77        run(&ctx, &[])
78            .unwrap()
79            .iter()
80            .map(ToString::to_string)
81            .collect()
82    }
83
84    #[test]
85    fn accepts_specs_defining_every_citable_rule() {
86        let dir = tempfile::tempdir().unwrap();
87        let cited: BTreeSet<RuleId> = crate::gates::GATES
88            .iter()
89            .flat_map(|gate| gate.cites.iter().copied())
90            .collect();
91        let mut text = String::new();
92        for rule in &cited {
93            let _ = write!(text, "### `{rule}` — Rule\n\n");
94        }
95        write_spec(&dir, "SPEC-all.md", &text);
96        assert!(run_in(&dir).is_empty());
97    }
98
99    #[test]
100    fn rejects_a_citable_rule_the_local_specs_lost() {
101        let dir = tempfile::tempdir().unwrap();
102        write_spec(&dir, "SPEC-thin.md", "### `sample:works` — Works\n");
103        let out = run_in(&dir);
104        assert!(!out.is_empty());
105        assert!(out.iter().all(|line| {
106            line.starts_with("FAIL spec-to-code:a-gate-message-cites-the-rule: ")
107                && line.ends_with(" resolves to no requirement")
108        }));
109    }
110
111    #[test]
112    fn a_missing_specs_directory_is_a_layout_failure() {
113        let dir = tempfile::tempdir().unwrap();
114        let out = run_in(&dir);
115        assert_eq!(out.len(), 1);
116        assert!(out[0].starts_with("FAIL no specs directory at "));
117        assert!(out[0].ends_with("; the layout moved"));
118    }
119}