spec_driven_docs/gates/
gate_message_cites_a_rule.rs1use 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
18pub const CITES: &[RuleId] = &[RuleId::GateMessageCitesTheRule];
20
21pub 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 #[allow(
37 clippy::case_sensitive_file_extension_comparisons,
38 reason = "the corpus convention is lowercase, and `.MD` is not a spec"
39 )]
40 if !name.ends_with(".md") {
41 continue;
42 }
43 defined.extend(crate::embedded::rule_ids_in(&read_text(
44 ctx,
45 specs.join(name),
46 )?));
47 }
48
49 let cited: BTreeSet<RuleId> = crate::gates::GATES
50 .iter()
51 .flat_map(|gate| gate.cites.iter().copied())
52 .collect();
53 Ok(cited
54 .into_iter()
55 .filter(|rule| !defined.contains(rule.as_str()))
56 .map(|rule| {
57 Violation::Finding(Finding::global(
58 RuleId::GateMessageCitesTheRule,
59 format!("{rule} resolves to no requirement"),
60 ))
61 })
62 .collect())
63}
64
65#[cfg(test)]
66mod tests {
67 use std::fmt::Write as _;
68
69 use super::*;
70
71 fn write_spec(dir: &tempfile::TempDir, name: &str, text: &str) {
72 let specs = dir.path().join("_docs/specs");
73 std::fs::create_dir_all(&specs).unwrap();
74 std::fs::write(specs.join(name), text).unwrap();
75 }
76
77 fn run_in(dir: &tempfile::TempDir) -> Vec<String> {
78 let ctx = GateCtx::new(dir.path().to_str().unwrap());
79 run(&ctx, &[])
80 .unwrap()
81 .iter()
82 .map(ToString::to_string)
83 .collect()
84 }
85
86 #[test]
87 fn accepts_specs_defining_every_citable_rule() {
88 let dir = tempfile::tempdir().unwrap();
89 let cited: BTreeSet<RuleId> = crate::gates::GATES
90 .iter()
91 .flat_map(|gate| gate.cites.iter().copied())
92 .collect();
93 let mut text = String::new();
94 for rule in &cited {
95 let _ = write!(text, "### `{rule}` — Rule\n\n");
96 }
97 write_spec(&dir, "SPEC-all.md", &text);
98 assert!(run_in(&dir).is_empty());
99 }
100
101 #[test]
102 fn rejects_a_citable_rule_the_local_specs_lost() {
103 let dir = tempfile::tempdir().unwrap();
104 write_spec(&dir, "SPEC-thin.md", "### `sample:works` — Works\n");
105 let out = run_in(&dir);
106 assert!(!out.is_empty());
107 assert!(out.iter().all(|line| {
108 line.starts_with("FAIL spec-to-code:a-gate-message-cites-the-rule: ")
109 && line.ends_with(" resolves to no requirement")
110 }));
111 }
112
113 #[test]
114 fn a_missing_specs_directory_is_a_layout_failure() {
115 let dir = tempfile::tempdir().unwrap();
116 let out = run_in(&dir);
117 assert_eq!(out.len(), 1);
118 assert!(out[0].starts_with("FAIL no specs directory at "));
119 assert!(out[0].ends_with("; the layout moved"));
120 }
121}