Skip to main content

spec_driven_docs/gates/
agents_digest_size.rs

1//! Gate: agent digests stay within their line budgets, or within the
2//! ceiling its project recorded for one.
3//!
4//! The root `AGENTS.md` is loaded into every session, so its budget is 100
5//! lines; a subtree digest gets 150. Vendored trees are pruned — a gate a
6//! consumer cannot satisfy short of deleting `node_modules/` is a gate they
7//! turn off. What a digest says is review's business; only size is judged.
8
9use 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
17/// The rules this gate can cite.
18pub const CITES: &[RuleId] = &[RuleId::AuthorInstructionsStayWithinBudget];
19
20const RULE: RuleId = RuleId::AuthorInstructionsStayWithinBudget;
21
22/// Measure every agent digest: one `lines` count per file.
23///
24/// # Errors
25///
26/// [`GateError::Io`] when a digest cannot be read.
27pub 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
50/// Judge every agent digest in the repository.
51///
52/// # Errors
53///
54/// [`GateError::Io`] when a digest cannot be read, and [`GateError::Debt`]
55/// when the debt file cannot be trusted.
56pub 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}