Skip to main content

spec_driven_docs/gates/
comparison_dated_tables.rs

1//! Gate: a comparison document carrying tables carries a verification date.
2//!
3//! A comparison is a set of claims about someone else's software, so its
4//! tables decay; the `Verified:` line is what tells a reader how stale.
5//! Only presence is judged — freshness policy is the comparison spec's
6//! business.
7
8use crate::domain::finding::Finding;
9use crate::domain::rule_id::RuleId;
10use crate::gates::{GateCtx, GateResult, Violation, read_text};
11
12/// The rules this gate can cite.
13pub 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
38/// Judge every file pre-commit passed.
39///
40/// # Errors
41///
42/// [`crate::gates::GateError::Io`] when a file cannot be read.
43pub 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}