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(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}