spec_driven_docs/gates/
tracking_registry.rs1use 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
16pub const CITES: &[RuleId] = &[
18 RuleId::RegistryHasOneReadableShape,
19 RuleId::PerishableSourceIsRegistered,
20 RuleId::UpstreamDerivationPinsARevision,
21 RuleId::EntryDeclaresHowToRevalidate,
22 RuleId::OverdueEntryBlocks,
23 RuleId::DeclaredDependentExists,
24];
25
26pub 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 if ctx
41 .retained([root.join("reference/tracking.yaml")])
42 .is_empty()
43 {
44 return Ok(vec![]);
45 }
46 let report = evaluate(&ctx.repo_root, &root, today_utc()).map_err(|source| {
47 crate::gates::GateError::io(root.join("reference/tracking.yaml"), source)
48 })?;
49
50 let mut violations = Vec::new();
51 if let Some(fatal) = &report.fatal {
52 violations.push(Violation::Finding(Finding::global(
53 fatal.rule,
54 fatal.detail.clone(),
55 )));
56 return Ok(violations);
57 }
58 for assessed in &report.entries {
59 for problem in &assessed.problems {
62 violations.push(Violation::Finding(Finding::global(
63 problem.rule,
64 problem.detail.clone(),
65 )));
66 }
67 }
68 Ok(violations)
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
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 fn write(dir: &tempfile::TempDir, rel: &str, text: &str) {
85 let p = dir.path().join(rel);
86 std::fs::create_dir_all(p.parent().unwrap()).unwrap();
87 std::fs::write(p, text).unwrap();
88 }
89
90 #[test]
91 fn a_repository_without_a_registry_passes() {
92 let dir = tempfile::tempdir().unwrap();
93 assert!(run_in(&dir).is_empty());
94 }
95
96 #[test]
97 fn an_overdue_entry_fails_naming_the_rule() {
98 let dir = tempfile::tempdir().unwrap();
99 write(
100 &dir,
101 "_docs/reference/tracking.yaml",
102 "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",
103 );
104 write(&dir, "_docs/reference/x.md", "# x\n");
105 let out = run_in(&dir);
106 assert_eq!(out.len(), 1);
107 assert!(out[0].contains("tracking:an-overdue-entry-blocks"));
108 }
109
110 #[test]
111 fn a_broken_registry_fails_once() {
112 let dir = tempfile::tempdir().unwrap();
113 write(
114 &dir,
115 "_docs/reference/tracking.yaml",
116 "schema_version: 1\ntracked: &all []\nother: *all\n",
117 );
118 let out = run_in(&dir);
119 assert_eq!(out.len(), 1);
120 assert!(out[0].contains("tracking:the-registry-has-one-readable-shape"));
121 }
122}