paper-gap 0.3.2

Local CLI for finding research papers with missing, weak, or stale public code
Documentation
use paper_gap::{analyse, repos::Repository};

fn repo(url: &str) -> Repository {
    Repository {
        forge: "github".into(),
        url: url.into(),
        owner: "a".into(),
        name: "b".into(),
        description: None,
        primary_language: Some("Rust".into()),
        stars: Some(1),
        forks: Some(0),
        license: Some("MIT".into()),
        archived: Some(false),
        default_branch: Some("main".into()),
        last_commit_date: Some("2026-01-01T00:00:00Z".into()),
        source_provider: "github".into(),
    }
}

#[test]
fn deduplicates_repositories_by_normalized_url() {
    let repos = analyse::dedup_repositories(vec![
        repo("https://github.com/a/b"),
        repo("https://github.com/a/b/"),
        repo("https://github.com/a/b.git"),
    ]);
    assert_eq!(repos.len(), 1);
}

#[test]
fn scores_no_public_implementation() {
    let assessment = analyse::assess(&[], &[]);
    assert_eq!(assessment.components.no_code_score, 50);
    assert_eq!(assessment.implementation_gap_score, 50);
    assert_eq!(assessment.gaps.len(), 2);
}

#[test]
fn scores_unclear_license_when_all_repos_lack_license() {
    let mut repository = repo("https://github.com/a/b");
    repository.license = None;
    let assessment = analyse::assess(&[], &[repository]);
    assert_eq!(assessment.components.license_gap_score, 10);
    assert!(assessment.implementation_gap_score >= 10);
}

#[test]
fn detects_archived_repository() {
    let mut repository = repo("https://github.com/a/b");
    repository.archived = Some(true);
    let analysis = analyse::analyze_repository(&repository);
    assert!(analysis.is_archived);
    let assessment = analyse::assess(&[], &[repository]);
    assert_eq!(assessment.components.stale_repo_score, 15);
}

#[test]
fn detects_quality_signals_from_paths() {
    let paths = vec![
        "README.md".to_string(),
        "LICENSE".to_string(),
        ".github/workflows/ci.yml".to_string(),
        "Cargo.toml".to_string(),
        "tests/integration.rs".to_string(),
        "docs/usage.md".to_string(),
        "examples/demo.rs".to_string(),
        "scripts/reproduce.sh".to_string(),
        "benches/main.rs".to_string(),
    ];
    let analysis = analyse::analyze_repository_with_paths(&repo("https://github.com/a/b"), &paths);
    assert!(analysis.has_readme);
    assert!(analysis.has_license);
    assert!(analysis.has_ci);
    assert!(analysis.has_package_manifest);
    assert!(analysis.has_tests);
    assert!(analysis.has_docs);
    assert!(analysis.has_examples);
    assert!(analysis.has_reproducibility_scripts);
    assert!(analysis.has_benchmarks);
    assert!(!analysis.is_notebook_only);
}

#[test]
fn scores_missing_quality_signals_from_analyses() {
    let repository = repo("https://github.com/a/b");
    let analysis = analyse::analyze_repository_with_paths(&repository, &[]);
    let assessment = analyse::assess_with_analyses(&[], &[repository], &[analysis]);
    assert_eq!(assessment.components.packaging_gap_score, 10);
    assert_eq!(assessment.components.docs_gap_score, 10);
    assert_eq!(assessment.components.tests_gap_score, 10);
    assert_eq!(assessment.components.reproducibility_gap_score, 10);
}

#[test]
fn detects_notebook_only_repository() {
    let paths = vec!["notebooks/demo.ipynb".to_string()];
    let analysis = analyse::analyze_repository_with_paths(&repo("https://github.com/a/b"), &paths);
    assert!(analysis.is_notebook_only);
}

#[test]
fn provider_failures_do_not_inflate_no_code_score() {
    let assessment = analyse::assess_with_evidence(&[], &[], &[], false, false);
    assert_eq!(assessment.components.no_code_score, 0);
    assert_eq!(assessment.implementation_gap_score, 0);
    assert!(assessment.gaps.is_empty());
}

#[test]
fn failed_inspection_does_not_create_quality_gaps() {
    let repository = repo("https://github.com/a/b");
    let analysis = analyse::analyze_repository(&repository);
    assert!(!analysis.inspection_succeeded);
    let assessment = analyse::assess_with_analyses(&[], &[repository], &[analysis]);
    assert_eq!(assessment.components.packaging_gap_score, 0);
    assert_eq!(assessment.components.docs_gap_score, 0);
    assert_eq!(assessment.components.tests_gap_score, 0);
    assert_eq!(assessment.components.reproducibility_gap_score, 0);
}

#[test]
fn disabled_repository_search_does_not_prove_absence() {
    let assessment = analyse::assess_with_evidence(&[], &[], &[], true, false);
    assert_eq!(assessment.components.no_code_score, 10);
    assert!(
        !assessment
            .gaps
            .iter()
            .any(|gap| matches!(gap.gap_type, analyse::GapType::NoPublicImplementationFound))
    );
}