spec_driven_docs/gates/
comparison_dated_tables.rs1use crate::domain::finding::Finding;
9use crate::domain::rule_id::RuleId;
10use crate::gates::{GateCtx, GateResult, Violation, read_text};
11
12pub const CITES: &[RuleId] = &[RuleId::EveryTableIsDated];
14
15fn has_verified_line(text: &str) -> bool {
16 text.lines().any(|line| {
17 let Some(rest) = line.strip_prefix("Verified: ") else {
18 return false;
19 };
20 if rest.starts_with("`<YYYY-MM-DD>`") {
21 return true;
22 }
23 let bytes = rest.as_bytes();
24 bytes.len() >= 10
25 && bytes[..4].iter().all(u8::is_ascii_digit)
26 && bytes[4] == b'-'
27 && bytes[5..7].iter().all(u8::is_ascii_digit)
28 && bytes[7] == b'-'
29 && bytes[8..10].iter().all(u8::is_ascii_digit)
30 })
31}
32
33pub(crate) fn has_table(text: &str) -> bool {
34 text.lines()
35 .any(|line| line.len() >= 2 && line.starts_with('|') && line.ends_with('|'))
36}
37
38pub fn run(ctx: &GateCtx, files: &[String]) -> GateResult {
44 let mut violations = Vec::new();
45 for file in files {
46 let text = read_text(ctx, file)?;
47 if has_table(&text) && !has_verified_line(&text) {
48 violations.push(Violation::Finding(Finding::on_file(
49 RuleId::EveryTableIsDated,
50 file,
51 "",
52 )));
53 }
54 }
55 Ok(violations)
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 const DATED: &str = "Legend: ✅ yes.\n\nVerified: 2026-08-24 — Subject 1.0.\n\n| [Case](#case) | Subject |\n| --- | --- |\n| Runs | ✅ yes |\n";
63
64 fn run_on(text: &str) -> Vec<String> {
65 let dir = tempfile::tempdir().unwrap();
66 std::fs::write(dir.path().join("comparison.md"), text).unwrap();
67 let ctx = GateCtx::new(dir.path().to_str().unwrap());
68 run(&ctx, &["comparison.md".to_string()])
69 .unwrap()
70 .iter()
71 .map(ToString::to_string)
72 .collect()
73 }
74
75 #[test]
76 fn accepts_a_dated_comparison_and_the_template_placeholder() {
77 assert!(run_on(DATED).is_empty());
78 assert!(run_on("Verified: `<YYYY-MM-DD>` — Subject.\n\n| a |\n").is_empty());
79 }
80
81 #[test]
82 fn rejects_tables_with_no_date() {
83 let undated: String = DATED
84 .lines()
85 .filter(|l| !l.starts_with("Verified:"))
86 .collect::<Vec<_>>()
87 .join("\n");
88 let out = run_on(&undated);
89 assert_eq!(out.len(), 1);
90 assert!(out[0].contains("comparison-docs:every-table-is-dated"));
91 }
92
93 #[test]
94 fn a_tableless_document_needs_no_date() {
95 assert!(run_on("# Notes\n\nProse only.\n").is_empty());
96 }
97}