ripr 0.8.0

Find static mutation-exposure gaps before expensive mutation testing
Documentation
use std::collections::BTreeMap;

use super::{EVIDENCE_HEALTH_SCHEMA_VERSION, EvidenceHealthReport};

pub(crate) fn render_evidence_health_markdown(report: &EvidenceHealthReport) -> String {
    let mut out = String::new();
    push_header(&mut out, report);
    push_summary(&mut out, report);
    push_grip_classes(&mut out, report);
    push_missing_discriminators(&mut out, report);
    push_oracle_strength(&mut out, report);
    push_related_test_confidence(&mut out, report);
    push_evidence_quality(&mut out, report);
    push_largest_canonical_gap_groups(&mut out, report);
    push_actionability(&mut out, report);
    push_static_limitation_distribution(&mut out, report);
    push_calibration_coverage(&mut out, report);
    push_evidence_quality_risks(&mut out, report);
    push_calibration_availability(&mut out, report);
    push_top_static_limitations(&mut out, report);
    out
}

fn push_header(out: &mut String, report: &EvidenceHealthReport) {
    out.push_str("# RIPR evidence health report\n\n");
    out.push_str("| Field | Value |\n");
    out.push_str("| --- | --- |\n");
    push_metric(out, "Schema", EVIDENCE_HEALTH_SCHEMA_VERSION);
    push_metric(out, "Status", "advisory");
    push_metric(out, "Root", report.root.as_str());
    push_metric(
        out,
        "Calibration",
        report
            .calibration
            .source
            .as_deref()
            .unwrap_or("not provided"),
    );
    out.push('\n');
}

fn push_summary(out: &mut String, report: &EvidenceHealthReport) {
    out.push_str("## Summary\n\n");
    out.push_str("| Metric | Count |\n");
    out.push_str("| --- | ---: |\n");
    push_count(out, "Seams", report.metrics.seams_total);
    push_count(
        out,
        "Headline-eligible seams",
        report.metrics.headline_eligible_total,
    );
    push_count(
        out,
        "Weakly gripped seams",
        report.metrics.weakly_gripped_total,
    );
    push_count(out, "Ungripped seams", report.metrics.ungripped_total);
    push_count(
        out,
        "Missing discriminators",
        report.metrics.missing_discriminators_total,
    );
    push_count(out, "Observed values", report.metrics.observed_values_total);
    push_count(out, "Related tests", report.metrics.related_tests_total);
    push_count(
        out,
        "Opaque oracle classifications",
        report.metrics.opaque_oracle_count,
    );
    out.push('\n');
}

fn push_grip_classes(out: &mut String, report: &EvidenceHealthReport) {
    out.push_str("## Grip Classes\n\n");
    push_counts_table(out, "Grip class", &report.metrics.grip_class_counts);
}

fn push_missing_discriminators(out: &mut String, report: &EvidenceHealthReport) {
    out.push_str("## Missing Discriminators\n\n");
    if report.metrics.missing_discriminator_counts.is_empty() {
        out.push_str("No missing discriminators were reported.\n\n");
    } else {
        push_counts_table_limited(
            out,
            "Missing discriminator",
            &report.metrics.missing_discriminator_counts,
            25,
        );
    }
}

fn push_oracle_strength(out: &mut String, report: &EvidenceHealthReport) {
    out.push_str("## Oracle Strength\n\n");
    push_counts_table(
        out,
        "Oracle strength",
        &report.metrics.oracle_strength_counts,
    );
}

fn push_related_test_confidence(out: &mut String, report: &EvidenceHealthReport) {
    out.push_str("## Related Test Confidence\n\n");
    push_counts_table(
        out,
        "Relation confidence",
        &report.metrics.related_test_confidence_counts,
    );
}

fn push_evidence_quality(out: &mut String, report: &EvidenceHealthReport) {
    out.push_str("## Evidence Quality\n\n");
    out.push_str("| Metric | Count |\n");
    out.push_str("| --- | ---: |\n");
    push_count(
        out,
        "Canonical gap groups",
        report.evidence_quality.canonical_gap_groups_total,
    );
    push_count(
        out,
        "Duplicate-looking groups",
        report.evidence_quality.duplicate_looking_groups_total,
    );
    push_count(
        out,
        "Records with canonical gap identity",
        report
            .evidence_quality
            .movement_availability
            .records_with_canonical_gap_id,
    );
    push_count(
        out,
        "Records with complete evidence path",
        report
            .evidence_quality
            .movement_availability
            .records_with_complete_evidence_path,
    );
    push_count(
        out,
        "Records with verify command",
        report
            .evidence_quality
            .movement_availability
            .records_with_verify_command,
    );
    out.push('\n');
}

fn push_largest_canonical_gap_groups(out: &mut String, report: &EvidenceHealthReport) {
    out.push_str("## Largest Canonical Gap Groups\n\n");
    if report.evidence_quality.largest_canonical_groups.is_empty() {
        out.push_str("No canonical gap groups were reported.\n\n");
        return;
    }

    out.push_str("| Group | Count | Reported size | Owner | Seam kind | Flow sink | Missing discriminator | Assertion shape | Example seam | File |\n");
    out.push_str("| --- | ---: | ---: | --- | --- | --- | --- | --- | --- | --- |\n");
    for group in &report.evidence_quality.largest_canonical_groups {
        out.push_str(&format!(
            "| {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |\n",
            group.canonical_gap_id,
            group.count,
            group
                .reported_group_size
                .map_or_else(|| "n/a".to_string(), |size| size.to_string()),
            group.owner,
            group.seam_kind,
            group.flow_sink,
            group.missing_discriminator,
            group.assertion_shape,
            group.example_seam_id,
            group.example_file
        ));
    }
    out.push('\n');
}

fn push_actionability(out: &mut String, report: &EvidenceHealthReport) {
    out.push_str("## Actionability\n\n");
    push_counts_table(
        out,
        "Actionability class",
        &report.evidence_quality.actionability_class_counts,
    );
}

fn push_static_limitation_distribution(out: &mut String, report: &EvidenceHealthReport) {
    out.push_str("## Static Limitation Distribution\n\n");
    push_counts_table(
        out,
        "Static limitation stage",
        &report.evidence_quality.static_limitation_stage_counts,
    );
    push_counts_table_limited(
        out,
        "Static limitation reason",
        &report.evidence_quality.static_limitation_reason_counts,
        15,
    );
}

fn push_calibration_coverage(out: &mut String, report: &EvidenceHealthReport) {
    out.push_str("## Evidence-Record Calibration Coverage\n\n");
    push_counts_table(
        out,
        "Calibration availability",
        &report.evidence_quality.calibration_availability_counts,
    );
}

fn push_evidence_quality_risks(out: &mut String, report: &EvidenceHealthReport) {
    out.push_str("## Top Evidence Quality Risks\n\n");
    if report
        .evidence_quality
        .top_evidence_quality_risks
        .is_empty()
    {
        out.push_str("No evidence-quality risks were reported.\n\n");
        return;
    }

    out.push_str("| Risk | Count | Summary |\n");
    out.push_str("| --- | ---: | --- |\n");
    for risk in &report.evidence_quality.top_evidence_quality_risks {
        out.push_str(&format!(
            "| {} | {} | {} |\n",
            risk.kind, risk.count, risk.summary
        ));
    }
    out.push('\n');
}

fn push_calibration_availability(out: &mut String, report: &EvidenceHealthReport) {
    out.push_str("## Calibration Availability\n\n");
    out.push_str("| Metric | Count |\n");
    out.push_str("| --- | ---: |\n");
    push_count(
        out,
        "Matched calibration rows",
        report.calibration.matched_total,
    );
    push_count(
        out,
        "Static rows without runtime context",
        report.calibration.static_without_runtime_total,
    );
    push_count(
        out,
        "Runtime rows without static seam",
        report.calibration.runtime_without_static_total,
    );
    push_count(
        out,
        "Ambiguous file-line joins",
        report.calibration.ambiguous_file_line_total,
    );
    push_count(
        out,
        "Unmatched runtime rows",
        report.calibration.unmatched_runtime_total,
    );
    out.push('\n');
}

fn push_top_static_limitations(out: &mut String, report: &EvidenceHealthReport) {
    out.push_str("## Top Static Limitations\n\n");
    if report.top_static_limitations.is_empty() {
        out.push_str("No static limitations were reported.\n");
        return;
    }

    out.push_str("### Categories\n\n");
    push_counts_table(
        out,
        "Category",
        &report.evidence_quality.static_limitation_category_counts,
    );
    out.push('\n');

    out.push_str("### Largest Limitation Signals\n\n");
    out.push_str("| Limitation | Count | Example seam | Summary |\n");
    out.push_str("| --- | ---: | --- | --- |\n");
    for limitation in &report.top_static_limitations {
        out.push_str(&format!(
            "| {} | {} | {} | {} |\n",
            limitation.kind,
            limitation.count,
            limitation.example_seam_id.as_deref().unwrap_or("n/a"),
            limitation.summary
        ));
    }
}

fn push_metric(out: &mut String, name: &str, value: &str) {
    out.push_str(&format!("| {name} | {value} |\n"));
}

fn push_count(out: &mut String, name: &str, count: usize) {
    out.push_str(&format!("| {name} | {count} |\n"));
}

fn push_counts_table(out: &mut String, heading: &str, counts: &BTreeMap<String, usize>) {
    out.push_str(&format!("| {heading} | Count |\n"));
    out.push_str("| --- | ---: |\n");
    for (key, count) in counts {
        out.push_str(&format!("| {key} | {count} |\n"));
    }
    out.push('\n');
}

fn push_counts_table_limited(
    out: &mut String,
    heading: &str,
    counts: &BTreeMap<String, usize>,
    limit: usize,
) {
    out.push_str(&format!("| {heading} | Count |\n"));
    out.push_str("| --- | ---: |\n");
    let mut rows = counts.iter().collect::<Vec<_>>();
    rows.sort_by(|(left_key, left_count), (right_key, right_count)| {
        right_count
            .cmp(left_count)
            .then_with(|| left_key.cmp(right_key))
    });
    for (key, count) in rows.iter().take(limit) {
        out.push_str(&format!("| {key} | {count} |\n"));
    }
    if rows.len() > limit {
        out.push_str(&format!(
            "\n_{} additional {} rows omitted from Markdown; JSON contains the full count map._\n",
            rows.len() - limit,
            heading.to_ascii_lowercase()
        ));
    }
    out.push('\n');
}