oratos-report 0.2.0

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

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

    out.push_str("## Scores\n\n");
    out.push_str("| Category | Score |\n");
    out.push_str("|----------|-------|\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
    ));

    out.push_str("## Findings\n\n");
    if report.findings.is_empty() {
        out.push_str("_No findings._\n");
        return out;
    }

    for page in &report.pages {
        if page.findings.is_empty() {
            continue;
        }
        out.push_str(&format!("### {}\n\n", page.page.url_or_path));
        for f in &page.findings {
            let badge = match f.severity {
                Severity::Error => "Error",
                Severity::Warning => "Warning",
                Severity::Info => "Info",
            };
            out.push_str(&format!(
                "- **{badge}** `{rule}` — {msg}\n",
                rule = f.rule_id,
                msg = f.message
            ));
            if let Some(rec) = &f.recommendation {
                out.push_str(&format!("  - Recommendation: {rec}\n"));
            }
        }
        out.push('\n');
    }

    out
}

#[cfg(test)]
mod tests {
    use super::*;
    use oratos_core::{AuditReport, AuditTarget, TargetKind};

    #[test]
    fn markdown_contains_title() {
        let report = AuditReport::new(
            AuditTarget {
                path_or_url: "./test".into(),
                kind: TargetKind::Directory,
            },
            vec![],
        );
        let md = format_markdown(&report);
        assert!(md.contains("# Oratos Audit Report"));
        assert!(md.contains("## Scores"));
    }

    #[test]
    fn markdown_output_snapshot() {
        use insta::assert_snapshot;
        use oratos_core::{
            AuditReport, AuditTarget, Category, Finding, PageAudit, PageRef, Severity, TargetKind,
        };

        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(&[]),
        };
        let report = AuditReport::new(
            AuditTarget {
                path_or_url: "./examples/static_site".into(),
                kind: TargetKind::Directory,
            },
            vec![page],
        );
        assert_snapshot!("sample_markdown_report", format_markdown(&report));
    }
}