Skip to main content

spec_driven_docs/gates/
agents_digest_size.rs

1//! Gate: agent digests stay within their line budgets.
2//!
3//! The root `AGENTS.md` is loaded into every session, so its budget is 100
4//! lines; a subtree digest gets 150. Vendored trees are pruned — a gate a
5//! consumer cannot satisfy short of deleting `node_modules/` is a gate they
6//! turn off. What a digest says is review's business; only size is judged.
7
8use crate::domain::finding::Finding;
9use crate::domain::rule_id::RuleId;
10use crate::gates::{GateCtx, GateResult, Violation, line_count, read_text, walk_files};
11
12/// The rules this gate can cite.
13pub const CITES: &[RuleId] = &[RuleId::AuthorInstructionsStayWithinBudget];
14
15/// Judge every agent digest in the repository.
16///
17/// # Errors
18///
19/// [`crate::gates::GateError::Io`] when a digest cannot be read.
20pub fn run(ctx: &GateCtx, _files: &[String]) -> GateResult {
21    if !ctx.path("AGENTS.md").is_file() {
22        return Ok(vec![Violation::Layout("no root AGENTS.md".to_string())]);
23    }
24    let mut violations = Vec::new();
25    for file in walk_files(ctx) {
26        if file.file_name() != Some("AGENTS.md") {
27            continue;
28        }
29        let cap = if file == "./AGENTS.md" { 100 } else { 150 };
30        if line_count(&read_text(ctx, &file)?) > cap {
31            violations.push(Violation::Finding(Finding::on_file(
32                RuleId::AuthorInstructionsStayWithinBudget,
33                file,
34                "",
35            )));
36        }
37    }
38    Ok(violations)
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44
45    fn write(dir: &tempfile::TempDir, path: &str, lines: usize) {
46        let path = dir.path().join(path);
47        std::fs::create_dir_all(path.parent().unwrap()).unwrap();
48        std::fs::write(path, "line\n".repeat(lines)).unwrap();
49    }
50
51    fn run_in(dir: &tempfile::TempDir) -> Vec<String> {
52        let ctx = GateCtx::new(dir.path().to_str().unwrap());
53        run(&ctx, &[])
54            .unwrap()
55            .iter()
56            .map(ToString::to_string)
57            .collect()
58    }
59
60    #[test]
61    fn accepts_digests_within_budget() {
62        let dir = tempfile::tempdir().unwrap();
63        write(&dir, "AGENTS.md", 100);
64        write(&dir, "method/AGENTS.md", 150);
65        assert!(run_in(&dir).is_empty());
66    }
67
68    #[test]
69    fn rejects_a_root_digest_over_its_budget() {
70        let dir = tempfile::tempdir().unwrap();
71        write(&dir, "AGENTS.md", 101);
72        let out = run_in(&dir);
73        assert_eq!(
74            out,
75            vec!["FAIL docs-format:author-instructions-stay-within-budget ./AGENTS.md".to_string()]
76        );
77    }
78
79    #[test]
80    fn ignores_vendored_digests() {
81        let dir = tempfile::tempdir().unwrap();
82        write(&dir, "AGENTS.md", 1);
83        write(&dir, "node_modules/pkg/AGENTS.md", 400);
84        assert!(run_in(&dir).is_empty());
85    }
86
87    #[test]
88    fn a_missing_root_digest_is_a_layout_failure() {
89        let dir = tempfile::tempdir().unwrap();
90        assert_eq!(run_in(&dir), vec!["FAIL no root AGENTS.md".to_string()]);
91    }
92}