spec_driven_docs/gates/
agents_digest_size.rs1use crate::domain::debt::Measurement;
10use crate::domain::finding::Finding;
11use crate::domain::gate_id::GateId;
12use crate::domain::paths::AGENTS_DIGEST_PATH;
13use crate::domain::rule_id::RuleId;
14use crate::gates::budget;
15use crate::gates::{GateCtx, GateError, GateResult, Violation, line_count, read_text, walk_files};
16
17pub const CITES: &[RuleId] = &[RuleId::AuthorInstructionsStayWithinBudget];
19
20const RULE: RuleId = RuleId::AuthorInstructionsStayWithinBudget;
21
22pub fn measure(ctx: &GateCtx) -> Result<Vec<Measurement>, GateError> {
28 let mut measurements = Vec::new();
29 for file in walk_files(ctx) {
30 if file.file_name() != Some(AGENTS_DIGEST_PATH) {
31 continue;
32 }
33 let cap = if file.as_str() == format!("./{AGENTS_DIGEST_PATH}") {
34 100
35 } else {
36 150
37 };
38 let lines = line_count(&read_text(ctx, &file)?);
39 measurements.push(Measurement::count(
40 GateId::AgentsDigestSize,
41 file.as_str(),
42 "lines",
43 lines,
44 cap,
45 ));
46 }
47 Ok(measurements)
48}
49
50pub fn run(ctx: &GateCtx, _files: &[String]) -> GateResult {
57 if !ctx.path(AGENTS_DIGEST_PATH).is_file() {
58 return Ok(vec![Violation::Layout(format!(
59 "no root {AGENTS_DIGEST_PATH}"
60 ))]);
61 }
62 let debt = budget::read_debt(ctx)?;
63 let measurements = measure(ctx)?;
64 Ok(budget::judge(
65 &debt,
66 GateId::AgentsDigestSize,
67 RULE,
68 &measurements,
69 |m| Violation::Finding(Finding::on_file(RULE, m.path.as_str(), "")),
70 ))
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 fn write(dir: &tempfile::TempDir, path: &str, lines: usize) {
78 let path = dir.path().join(path);
79 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
80 std::fs::write(path, "line\n".repeat(lines)).unwrap();
81 }
82
83 fn run_in(dir: &tempfile::TempDir) -> Vec<String> {
84 let ctx = GateCtx::new(dir.path().to_str().unwrap());
85 run(&ctx, &[])
86 .unwrap()
87 .iter()
88 .map(ToString::to_string)
89 .collect()
90 }
91
92 #[test]
93 fn accepts_digests_within_budget() {
94 let dir = tempfile::tempdir().unwrap();
95 write(&dir, "AGENTS.md", 100);
96 write(&dir, "method/AGENTS.md", 150);
97 assert!(run_in(&dir).is_empty());
98 }
99
100 #[test]
101 fn rejects_a_root_digest_over_its_budget() {
102 let dir = tempfile::tempdir().unwrap();
103 write(&dir, "AGENTS.md", 101);
104 let out = run_in(&dir);
105 assert_eq!(
106 out,
107 vec!["FAIL docs-format:author-instructions-stay-within-budget ./AGENTS.md".to_string()]
108 );
109 }
110
111 #[test]
112 fn ignores_vendored_digests() {
113 let dir = tempfile::tempdir().unwrap();
114 write(&dir, "AGENTS.md", 1);
115 write(&dir, "node_modules/pkg/AGENTS.md", 400);
116 assert!(run_in(&dir).is_empty());
117 }
118
119 #[test]
120 fn a_missing_root_digest_is_a_layout_failure() {
121 let dir = tempfile::tempdir().unwrap();
122 assert_eq!(run_in(&dir), vec!["FAIL no root AGENTS.md".to_string()]);
123 }
124
125 #[test]
126 fn a_recorded_ceiling_carries_an_oversize_digest() {
127 let dir = tempfile::tempdir().unwrap();
128 write(&dir, "AGENTS.md", 120);
129 std::fs::create_dir_all(dir.path().join(".spec-driven-docs")).unwrap();
130 std::fs::write(
131 dir.path().join(".spec-driven-docs/debt.yaml"),
132 "schema_version: 1\nagents-digest-size:\n AGENTS.md:\n lines:\n ceiling: 120\n",
133 )
134 .unwrap();
135 assert!(run_in(&dir).is_empty());
136 }
137}