ailint-core 1.0.1

Core library for ailint: discovery, parsing, rule engine, and reporters for AI agent guidance files.
Documentation
use std::path::PathBuf;

use ailint_core::{lint, Config};

fn fixture(rule_dir: &str, kind: &str, name: &str) -> PathBuf {
    let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    p.push("tests/fixtures/structural");
    p.push(rule_dir);
    p.push(kind);
    p.push(name);
    p
}

#[test]
fn ail001_fires_on_invalid_frontmatter() {
    let path = fixture("frontmatter_schema", "bad", "AGENTS.md");
    let violations = lint(&path, &Config::default()).unwrap();
    let hits: Vec<_> = violations.iter().filter(|v| v.rule_id.code == 1).collect();
    assert_eq!(hits.len(), 1, "expected one AIL001, got {violations:?}");
    assert!(hits[0].message.starts_with("invalid YAML frontmatter"));
    assert!(hits[0].fix_hint.is_some());
}

#[test]
fn ail001_silent_on_valid_frontmatter() {
    let path = fixture("frontmatter_schema", "good", "AGENTS.md");
    let violations = lint(&path, &Config::default()).unwrap();
    let hits: Vec<_> = violations.iter().filter(|v| v.rule_id.code == 1).collect();
    assert!(hits.is_empty(), "expected no AIL001, got {violations:?}");
}

fn config_with_required_setup() -> Config {
    let mut cfg = Config::default();
    let opts: serde_yaml::Value = serde_yaml::from_str("required: [\"Setup\"]").unwrap();
    cfg.rules
        .options
        .insert("missing-required-section".to_string(), opts);
    cfg
}

#[test]
fn ail003_fires_when_required_section_missing() {
    let path = fixture("required_section", "bad", "AGENTS.md");
    let cfg = config_with_required_setup();
    let violations = lint(&path, &cfg).unwrap();
    let hits: Vec<_> = violations.iter().filter(|v| v.rule_id.code == 3).collect();
    assert_eq!(hits.len(), 1, "expected one AIL003, got {violations:?}");
    assert!(hits[0].message.contains("Setup"));
}

#[test]
fn ail003_silent_when_required_section_present() {
    let path = fixture("required_section", "good", "AGENTS.md");
    let cfg = config_with_required_setup();
    let violations = lint(&path, &cfg).unwrap();
    let hits: Vec<_> = violations.iter().filter(|v| v.rule_id.code == 3).collect();
    assert!(hits.is_empty(), "expected no AIL003, got {violations:?}");
}

#[test]
fn ail040_fires_on_broken_link() {
    let path = fixture("broken_local_link", "bad", "doc.md");
    let violations = lint(&path, &Config::default()).unwrap();
    let hits: Vec<_> = violations.iter().filter(|v| v.rule_id.code == 40).collect();
    assert_eq!(hits.len(), 1, "expected one AIL040, got {violations:?}");
    assert!(hits[0].message.contains("does-not-exist.md"));
    assert!(hits[0].fix_hint.is_some());
}

#[test]
fn ail040_silent_on_valid_links() {
    let dir = {
        let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        p.push("tests/fixtures/structural/broken_local_link/good");
        p
    };
    let violations = lint(&dir, &Config::default()).unwrap();
    let hits: Vec<_> = violations.iter().filter(|v| v.rule_id.code == 40).collect();
    assert!(hits.is_empty(), "expected no AIL040, got {violations:?}");
}

#[test]
fn ail041_fires_on_malformed_yaml() {
    let path = fixture("malformed_yaml", "bad", "config.yaml");
    let violations = lint(&path, &Config::default()).unwrap();
    let hits: Vec<_> = violations.iter().filter(|v| v.rule_id.code == 41).collect();
    assert_eq!(hits.len(), 1, "expected one AIL041, got {violations:?}");
    assert!(hits[0].message.contains("parse error"));
}

#[test]
fn ail041_silent_on_valid_yaml() {
    let path = fixture("malformed_yaml", "good", "config.yaml");
    let violations = lint(&path, &Config::default()).unwrap();
    let hits: Vec<_> = violations.iter().filter(|v| v.rule_id.code == 41).collect();
    assert!(hits.is_empty(), "expected no AIL041, got {violations:?}");
}