spec_driven_docs/gates/
adr_filename_shape.rs1use crate::domain::finding::Finding;
9use crate::domain::rule_id::RuleId;
10use crate::gates::{GateCtx, GateResult, Violation};
11
12pub const CITES: &[RuleId] = &[RuleId::FilenameCarriesNoDigit];
14
15const RULE: RuleId = RuleId::FilenameCarriesNoDigit;
16
17pub fn run(_ctx: &GateCtx, files: &[String]) -> GateResult {
23 let mut violations = Vec::new();
24 for file in files {
25 let basename = file.rsplit('/').next().unwrap_or(file);
26 if basename == "TEMPLATE-adr.md" {
27 continue;
28 }
29 let Some(after_prefix) = basename.strip_prefix("ADR-") else {
30 violations.push(Violation::Finding(Finding::on_file(
31 RULE,
32 file,
33 "no ADR- prefix",
34 )));
35 continue;
36 };
37 let Some(slug) = after_prefix.strip_suffix(".md") else {
38 violations.push(Violation::Finding(Finding::on_file(
39 RULE,
40 file,
41 "not a markdown file",
42 )));
43 continue;
44 };
45 if slug.is_empty() || !slug.bytes().all(|b| b.is_ascii_lowercase() || b == b'-') {
46 violations.push(Violation::Finding(Finding::on_file(
47 RULE,
48 file,
49 "the slug is lowercase and hyphens, with no digit",
50 )));
51 }
52 }
53 Ok(violations)
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59
60 fn run_on(files: &[&str]) -> Vec<String> {
61 let ctx = GateCtx::new(".");
62 let files: Vec<String> = files.iter().map(ToString::to_string).collect();
63 run(&ctx, &files)
64 .unwrap()
65 .iter()
66 .map(ToString::to_string)
67 .collect()
68 }
69
70 #[test]
71 fn accepts_a_slugged_record_and_the_template() {
72 assert!(run_on(&["_docs/decisions/ADR-use-slugs.md"]).is_empty());
73 assert!(run_on(&["_docs/decisions/TEMPLATE-adr.md"]).is_empty());
74 }
75
76 #[test]
77 fn rejects_a_digit_in_the_slug() {
78 let out = run_on(&["_docs/decisions/ADR-use-v2.md"]);
79 assert_eq!(out.len(), 1);
80 assert!(out[0].contains("decision-records:filename-carries-no-digit"));
81 assert!(out[0].contains("the slug is lowercase and hyphens, with no digit"));
82 }
83
84 #[test]
85 fn rejects_missing_prefix_and_wrong_extension() {
86 assert!(run_on(&["_docs/decisions/RECORD-x.md"])[0].contains("no ADR- prefix"));
87 assert!(run_on(&["_docs/decisions/ADR-x.txt"])[0].contains("not a markdown file"));
88 }
89}