oratos-report 0.2.0

Report formatters for Oratos audit results
Documentation
use oratos_core::{AuditReport, Category, Severity};

pub fn format_console(report: &AuditReport) -> String {
    let mut out = String::new();
    out.push_str(&format!("Oratos Audit Report v{}\n", report.core_version));
    out.push_str(&format!("Target: {}\n", report.target.path_or_url));
    out.push_str(&format!("Pages: {}\n\n", report.page_count));

    out.push_str("Scores:\n");
    out.push_str(&format!(
        "  Overall:         {:.1}\n",
        report.scores.overall
    ));
    out.push_str(&format!("  SEO:             {:.1}\n", report.scores.seo));
    out.push_str(&format!(
        "  Accessibility:   {:.1}\n",
        report.scores.accessibility
    ));
    out.push_str(&format!(
        "  Structured Data: {:.1}\n",
        report.scores.structured_data
    ));
    out.push_str(&format!(
        "  LLM Readiness:   {:.1}\n\n",
        report.scores.llm_readiness
    ));

    let errors = report
        .findings
        .iter()
        .filter(|f| f.severity == Severity::Error)
        .count();
    let warnings = report
        .findings
        .iter()
        .filter(|f| f.severity == Severity::Warning)
        .count();
    let infos = report
        .findings
        .iter()
        .filter(|f| f.severity == Severity::Info)
        .count();

    out.push_str(&format!(
        "Findings: {errors} errors, {warnings} warnings, {infos} info\n\n"
    ));

    for page in &report.pages {
        if page.findings.is_empty() {
            continue;
        }
        out.push_str(&format!("── {} ──\n", page.page.url_or_path));
        for finding in &page.findings {
            let sev = match finding.severity {
                Severity::Error => "ERROR",
                Severity::Warning => "WARN ",
                Severity::Info => "INFO ",
            };
            let cat = category_label(finding.category);
            out.push_str(&format!(
                "  [{sev}] [{cat}] {}: {}\n",
                finding.rule_id, finding.message
            ));
            if let Some(rec) = &finding.recommendation {
                out.push_str(&format!("{rec}\n"));
            }
        }
        out.push('\n');
    }

    out
}

fn category_label(cat: Category) -> &'static str {
    match cat {
        Category::Seo => "SEO",
        Category::Accessibility => "A11Y",
        Category::StructuredData => "DATA",
        Category::LlmReadiness => "LLM ",
        Category::PerformanceHint => "PERF",
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use insta::assert_snapshot;
    use oratos_core::{
        AuditReport, AuditTarget, Category, Finding, PageAudit, PageRef, Severity, TargetKind,
    };

    fn sample_report() -> AuditReport {
        let finding = Finding::new(
            "seo.missing-title",
            Severity::Error,
            Category::Seo,
            "Page is missing a title.",
        );
        let page = PageAudit {
            page: PageRef::new("/index.html"),
            findings: vec![finding],
            scores: oratos_core::CategoryScores::from_findings(&[]),
        };
        AuditReport::new(
            AuditTarget {
                path_or_url: "./examples/static_site".into(),
                kind: TargetKind::Directory,
            },
            vec![page],
        )
    }

    #[test]
    fn console_output_snapshot() {
        assert_snapshot!("sample_console_report", format_console(&sample_report()));
    }
}