1use crate::{Result, analyse, arxiv, pwc, repos};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Serialize, Deserialize)]
5pub struct ScanReport {
6 pub paper_results: Vec<PaperResult>,
7}
8
9#[derive(Debug, Serialize, Deserialize)]
10pub struct PaperResult {
11 pub paper: arxiv::Paper,
12 pub code_links: Vec<pwc::CodeLink>,
13 pub repositories: Vec<repos::Repository>,
14 pub gap_assessment: analyse::GapAssessment,
15}
16
17#[derive(Clone, Copy)]
18pub enum OutputFormat {
19 Markdown,
20 Json,
21}
22
23pub fn render(report: &ScanReport, format: OutputFormat) -> Result<String> {
24 Ok(match format {
25 OutputFormat::Json => serde_json::to_string_pretty(report)?,
26 OutputFormat::Markdown => markdown(report),
27 })
28}
29
30fn markdown(report: &ScanReport) -> String {
31 let mut results: Vec<_> = report.paper_results.iter().collect();
32 results.sort_by_key(|result| std::cmp::Reverse(result.gap_assessment.implementation_gap_score));
33
34 let mut out = format!(
35 "# paper-gap report\n\n{} papers analysed. Ranked by implementation gap score.\n\n",
36 results.len()
37 );
38
39 for (index, result) in results.iter().enumerate() {
40 let p = &result.paper;
41 out.push_str(&format!(
42 "## {}. {}\n\n- Implementation gap score: {}\n- arXiv: {}\n- Published: {}\n- Authors: {}\n- Categories: {}\n",
43 index + 1,
44 p.title,
45 result.gap_assessment.implementation_gap_score,
46 p.arxiv_id.as_deref().unwrap_or(""),
47 p.published_date.as_deref().unwrap_or("unknown"),
48 p.authors.join(", "),
49 p.categories.join(", ")
50 ));
51
52 push_code_links(&mut out, &result.code_links);
53 push_repositories(&mut out, &result.repositories);
54 push_score(&mut out, &result.gap_assessment);
55
56 out.push_str(&format!("\n{}\n\n", p.abstract_text));
57 }
58
59 out
60}
61
62fn push_code_links(out: &mut String, links: &[pwc::CodeLink]) {
63 if links.is_empty() {
64 out.push_str("- Papers with Code links: none known\n");
65 return;
66 }
67 out.push_str("- Papers with Code links:\n");
68 for link in links {
69 out.push_str(&format!(" - {}\n", link.repository_url));
70 }
71}
72
73fn push_repositories(out: &mut String, repositories: &[repos::Repository]) {
74 if repositories.is_empty() {
75 out.push_str("- Repository search results: none found\n");
76 return;
77 }
78 out.push_str("- Repository search results:\n");
79 for repo in repositories {
80 out.push_str(&format!(" - {} ({})\n", repo.url, repo.forge));
81 }
82}
83
84fn push_score(out: &mut String, assessment: &analyse::GapAssessment) {
85 let c = &assessment.components;
86 out.push_str("- Score components:\n");
87 out.push_str(&format!(" - no_code_score: {}\n", c.no_code_score));
88 out.push_str(&format!(
89 " - prototype_only_score: {}\n",
90 c.prototype_only_score
91 ));
92 out.push_str(&format!(" - stale_repo_score: {}\n", c.stale_repo_score));
93 out.push_str(&format!(
94 " - packaging_gap_score: {}\n",
95 c.packaging_gap_score
96 ));
97 out.push_str(&format!(" - docs_gap_score: {}\n", c.docs_gap_score));
98 out.push_str(&format!(" - tests_gap_score: {}\n", c.tests_gap_score));
99 out.push_str(&format!(" - license_gap_score: {}\n", c.license_gap_score));
100 out.push_str(&format!(
101 " - reproducibility_gap_score: {}\n",
102 c.reproducibility_gap_score
103 ));
104 out.push_str(&format!(
105 " - ecosystem_gap_score: {}\n",
106 c.ecosystem_gap_score
107 ));
108
109 if assessment.gaps.is_empty() {
110 out.push_str("- Gap reasons: none\n");
111 return;
112 }
113 out.push_str("- Gap reasons:\n");
114 for gap in &assessment.gaps {
115 out.push_str(&format!(
116 " - {:?}: +{} — {}\n",
117 gap.gap_type, gap.points, gap.explanation
118 ));
119 }
120}