use crate::domain::finding::Finding;
use crate::domain::rule_id::RuleId;
use crate::gates::{GateCtx, GateResult, Violation, line_count, read_text, walk_files};
pub const CITES: &[RuleId] = &[RuleId::AuthorInstructionsStayWithinBudget];
pub fn run(ctx: &GateCtx, _files: &[String]) -> GateResult {
if !ctx.path("AGENTS.md").is_file() {
return Ok(vec![Violation::Layout("no root AGENTS.md".to_string())]);
}
let mut violations = Vec::new();
for file in walk_files(ctx) {
if file.file_name() != Some("AGENTS.md") {
continue;
}
let cap = if file == "./AGENTS.md" { 100 } else { 150 };
if line_count(&read_text(ctx, &file)?) > cap {
violations.push(Violation::Finding(Finding::on_file(
RuleId::AuthorInstructionsStayWithinBudget,
file,
"",
)));
}
}
Ok(violations)
}
#[cfg(test)]
mod tests {
use super::*;
fn write(dir: &tempfile::TempDir, path: &str, lines: usize) {
let path = dir.path().join(path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, "line\n".repeat(lines)).unwrap();
}
fn run_in(dir: &tempfile::TempDir) -> Vec<String> {
let ctx = GateCtx::new(dir.path().to_str().unwrap());
run(&ctx, &[])
.unwrap()
.iter()
.map(ToString::to_string)
.collect()
}
#[test]
fn accepts_digests_within_budget() {
let dir = tempfile::tempdir().unwrap();
write(&dir, "AGENTS.md", 100);
write(&dir, "method/AGENTS.md", 150);
assert!(run_in(&dir).is_empty());
}
#[test]
fn rejects_a_root_digest_over_its_budget() {
let dir = tempfile::tempdir().unwrap();
write(&dir, "AGENTS.md", 101);
let out = run_in(&dir);
assert_eq!(
out,
vec!["FAIL docs-format:author-instructions-stay-within-budget ./AGENTS.md".to_string()]
);
}
#[test]
fn ignores_vendored_digests() {
let dir = tempfile::tempdir().unwrap();
write(&dir, "AGENTS.md", 1);
write(&dir, "node_modules/pkg/AGENTS.md", 400);
assert!(run_in(&dir).is_empty());
}
#[test]
fn a_missing_root_digest_is_a_layout_failure() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(run_in(&dir), vec!["FAIL no root AGENTS.md".to_string()]);
}
}