paper-gap 0.1.5

Local CLI for finding research papers with missing, weak, or stale public code
Documentation
use crate::{Result, analyse, arxiv, pwc, repos};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct ScanReport {
    pub paper_results: Vec<PaperResult>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct PaperResult {
    pub paper: arxiv::Paper,
    pub code_links: Vec<pwc::CodeLink>,
    pub repositories: Vec<repos::Repository>,
    pub gap_assessment: analyse::GapAssessment,
}

#[derive(Clone, Copy)]
pub enum OutputFormat {
    Markdown,
    Json,
}

pub fn render(report: &ScanReport, format: OutputFormat) -> Result<String> {
    Ok(match format {
        OutputFormat::Json => serde_json::to_string_pretty(report)?,
        OutputFormat::Markdown => markdown(report),
    })
}

fn markdown(report: &ScanReport) -> String {
    let mut results: Vec<_> = report.paper_results.iter().collect();
    results.sort_by_key(|result| std::cmp::Reverse(result.gap_assessment.implementation_gap_score));

    let mut out = format!(
        "# paper-gap report\n\n{} papers analysed. Ranked by implementation gap score.\n\n",
        results.len()
    );

    for (index, result) in results.iter().enumerate() {
        let p = &result.paper;
        out.push_str(&format!(
            "## {}. {}\n\n- Implementation gap score: {}\n- arXiv: {}\n- Published: {}\n- Authors: {}\n- Categories: {}\n",
            index + 1,
            p.title,
            result.gap_assessment.implementation_gap_score,
            p.arxiv_id.as_deref().unwrap_or(""),
            p.published_date.as_deref().unwrap_or("unknown"),
            p.authors.join(", "),
            p.categories.join(", ")
        ));

        push_code_links(&mut out, &result.code_links);
        push_repositories(&mut out, &result.repositories);
        push_score(&mut out, &result.gap_assessment);

        out.push_str(&format!("\n{}\n\n", p.abstract_text));
    }

    out
}

fn push_code_links(out: &mut String, links: &[pwc::CodeLink]) {
    if links.is_empty() {
        out.push_str("- Papers with Code links: none known\n");
        return;
    }
    out.push_str("- Papers with Code links:\n");
    for link in links {
        out.push_str(&format!("  - {}\n", link.repository_url));
    }
}

fn push_repositories(out: &mut String, repositories: &[repos::Repository]) {
    if repositories.is_empty() {
        out.push_str("- Repository search results: none found\n");
        return;
    }
    out.push_str("- Repository search results:\n");
    for repo in repositories {
        out.push_str(&format!("  - {} ({})\n", repo.url, repo.forge));
    }
}

fn push_score(out: &mut String, assessment: &analyse::GapAssessment) {
    let c = &assessment.components;
    out.push_str("- Score components:\n");
    out.push_str(&format!("  - no_code_score: {}\n", c.no_code_score));
    out.push_str(&format!(
        "  - prototype_only_score: {}\n",
        c.prototype_only_score
    ));
    out.push_str(&format!("  - stale_repo_score: {}\n", c.stale_repo_score));
    out.push_str(&format!(
        "  - packaging_gap_score: {}\n",
        c.packaging_gap_score
    ));
    out.push_str(&format!("  - docs_gap_score: {}\n", c.docs_gap_score));
    out.push_str(&format!("  - tests_gap_score: {}\n", c.tests_gap_score));
    out.push_str(&format!("  - license_gap_score: {}\n", c.license_gap_score));
    out.push_str(&format!(
        "  - reproducibility_gap_score: {}\n",
        c.reproducibility_gap_score
    ));
    out.push_str(&format!(
        "  - ecosystem_gap_score: {}\n",
        c.ecosystem_gap_score
    ));

    if assessment.gaps.is_empty() {
        out.push_str("- Gap reasons: none\n");
        return;
    }
    out.push_str("- Gap reasons:\n");
    for gap in &assessment.gaps {
        out.push_str(&format!(
            "  - {:?}: +{}{}\n",
            gap.gap_type, gap.points, gap.explanation
        ));
    }
}