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