Skip to main content

spec_driven_docs/gates/
tracking_registry.rs

1//! Gate: the tracking registry is valid, current, and its dependents exist.
2//!
3//! The registry is `<docs_root>/reference/tracking.yaml`. This gate parses
4//! and bounds it, resolves every declared path, and fails an overdue entry
5//! naming the due date and the recovery steps. It reads current bytes only
6//! and makes no claim about edit history, and it needs no network — comparing
7//! a pinned revision to its upstream is `sdd track check`. A repository with
8//! no registry has nothing to check.
9
10use crate::domain::finding::Finding;
11use crate::domain::rule_id::RuleId;
12use crate::gates::paths::docs_root;
13use crate::gates::{GateCtx, GateResult, Violation};
14use crate::services::tracking::{evaluate, today_utc};
15
16/// The rules this gate can cite.
17pub const CITES: &[RuleId] = &[
18    RuleId::RegistryHasOneReadableShape,
19    RuleId::PerishableSourceIsRegistered,
20    RuleId::UpstreamDerivationPinsARevision,
21    RuleId::EntryDeclaresHowToRevalidate,
22    RuleId::OverdueEntryBlocks,
23    RuleId::DeclaredDependentExists,
24];
25
26/// Judge the tracking registry.
27///
28/// # Errors
29///
30/// [`crate::gates::GateError::Io`] when the registry is present but cannot be
31/// read.
32pub fn run(ctx: &GateCtx, _files: &[String]) -> GateResult {
33    let root = docs_root(ctx);
34    let registry = ctx.path(&root).join("reference/tracking.yaml");
35    if !registry.is_file() {
36        return Ok(vec![]);
37    }
38    let report = evaluate(&ctx.repo_root, &root, today_utc()).map_err(|source| {
39        crate::gates::GateError::io(root.join("reference/tracking.yaml"), source)
40    })?;
41
42    let mut violations = Vec::new();
43    if let Some(fatal) = &report.fatal {
44        violations.push(Violation::Finding(Finding::global(
45            fatal.rule,
46            fatal.detail.clone(),
47        )));
48        return Ok(violations);
49    }
50    for assessed in &report.entries {
51        // Overdue and every other problem is already carried on the entry,
52        // each citing its own rule; a current, clean entry adds nothing.
53        for problem in &assessed.problems {
54            violations.push(Violation::Finding(Finding::global(
55                problem.rule,
56                problem.detail.clone(),
57            )));
58        }
59    }
60    Ok(violations)
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    fn run_in(dir: &tempfile::TempDir) -> Vec<String> {
68        let ctx = GateCtx::new(dir.path().to_str().unwrap());
69        run(&ctx, &[])
70            .unwrap()
71            .iter()
72            .map(ToString::to_string)
73            .collect()
74    }
75
76    fn write(dir: &tempfile::TempDir, rel: &str, text: &str) {
77        let p = dir.path().join(rel);
78        std::fs::create_dir_all(p.parent().unwrap()).unwrap();
79        std::fs::write(p, text).unwrap();
80    }
81
82    #[test]
83    fn a_repository_without_a_registry_passes() {
84        let dir = tempfile::tempdir().unwrap();
85        assert!(run_in(&dir).is_empty());
86    }
87
88    #[test]
89    fn an_overdue_entry_fails_naming_the_rule() {
90        let dir = tempfile::tempdir().unwrap();
91        write(
92            &dir,
93            "_docs/reference/tracking.yaml",
94            "schema_version: 1\ntracked:\n  - id: sample\n    path: _docs/reference/x.md\n    last_checked: 2000-01-01\n    cadence_days: 30\n    why: it moves\n    revalidate:\n      - re-fetch it\n    dependents: []\n",
95        );
96        write(&dir, "_docs/reference/x.md", "# x\n");
97        let out = run_in(&dir);
98        assert_eq!(out.len(), 1);
99        assert!(out[0].contains("tracking:an-overdue-entry-blocks"));
100    }
101
102    #[test]
103    fn a_broken_registry_fails_once() {
104        let dir = tempfile::tempdir().unwrap();
105        write(
106            &dir,
107            "_docs/reference/tracking.yaml",
108            "schema_version: 1\ntracked: &all []\nother: *all\n",
109        );
110        let out = run_in(&dir);
111        assert_eq!(out.len(), 1);
112        assert!(out[0].contains("tracking:the-registry-has-one-readable-shape"));
113    }
114}