use crate::solve_report::SolveReport;
use pounce_studio_core::glossary::{citation_by_key, solve_feature_keys, Citation};
pub struct Selected {
pub citation: &'static Citation,
pub reason: &'static str,
}
pub fn select(report: Option<&SolveReport>) -> Vec<Selected> {
let mut out: Vec<Selected> = Vec::new();
let mut seen: Vec<&'static str> = Vec::new();
let push = |feature: &str,
reason: &'static str,
out: &mut Vec<Selected>,
seen: &mut Vec<&'static str>| {
let Some(keys) = solve_feature_keys(feature) else {
return;
};
for &k in keys {
if seen.contains(&k) {
continue;
}
if let Some(citation) = citation_by_key(k) {
seen.push(k);
out.push(Selected { citation, reason });
}
}
};
push(
"core",
"core solver (cite for any pounce result)",
&mut out,
&mut seen,
);
if let Some(r) = report {
if r.statistics.restoration_calls > 0 {
push(
"restoration",
"the restoration phase was entered during this solve",
&mut out,
&mut seen,
);
}
}
out
}
pub fn render_human(selected: &[Selected]) -> String {
let mut s = String::new();
s.push_str("Please cite the following when publishing results obtained with pounce:\n");
for (i, sel) in selected.iter().enumerate() {
let c = sel.citation;
s.push_str(&format!("\n[{}] {}\n", i + 1, c.title));
s.push_str(&format!(" {} ({})\n", c.author, c.year));
if !c.venue.is_empty() {
s.push_str(&format!(" {}\n", c.venue));
}
if !c.doi.is_empty() {
s.push_str(&format!(" doi:{}\n", c.doi));
}
s.push_str(&format!(" — {}\n", sel.reason));
}
s
}
pub fn render_bibtex(selected: &[Selected]) -> String {
let mut s = String::new();
for (i, sel) in selected.iter().enumerate() {
let c = sel.citation;
if i > 0 {
s.push('\n');
}
s.push_str(&format!("@{}{{{},\n", c.entry_type, c.key));
s.push_str(&format!(" title = {{{}}},\n", c.title));
if !c.author.is_empty() {
s.push_str(&format!(" author = {{{}}},\n", c.author));
}
if !c.year.is_empty() {
s.push_str(&format!(" year = {{{}}},\n", c.year));
}
if !c.venue.is_empty() {
let venue_field = if c.entry_type == "article" {
"journal"
} else {
"howpublished"
};
s.push_str(&format!(" {venue_field} = {{{}}},\n", c.venue));
}
if !c.doi.is_empty() {
s.push_str(&format!(" doi = {{{}}},\n", c.doi));
}
s.push_str("}\n");
}
s
}
#[cfg(test)]
mod tests {
use super::*;
use crate::solve_report::{InputDescriptor, ReportBuilder, ReportDetail};
fn report(restoration_calls: i32) -> SolveReport {
let mut r = ReportBuilder::new(
ReportDetail::Summary,
InputDescriptor::Builtin {
name: "test".into(),
},
)
.finish();
r.statistics.restoration_calls = restoration_calls;
r
}
#[test]
fn core_always_present_without_report() {
let sel = select(None);
let keys: Vec<_> = sel.iter().map(|s| s.citation.key).collect();
assert!(keys.contains(&"pounce2026"));
assert!(keys.contains(&"wachter2006"));
assert!(!keys.contains(&"byrd2010"));
}
#[test]
fn restoration_adds_byrd_when_report_shows_it() {
let r = report(3);
let sel = select(Some(&r));
let keys: Vec<_> = sel.iter().map(|s| s.citation.key).collect();
assert!(keys.contains(&"pounce2026"));
assert!(keys.contains(&"byrd2010"));
}
#[test]
fn no_restoration_no_byrd() {
let r = report(0);
let sel = select(Some(&r));
let keys: Vec<_> = sel.iter().map(|s| s.citation.key).collect();
assert!(!keys.contains(&"byrd2010"));
}
#[test]
fn human_render_mentions_pounce_and_doi() {
let out = render_human(&select(None));
assert!(out.contains("POUNCE"));
assert!(out.contains("10.5281/zenodo.20387011"));
}
#[test]
fn bibtex_render_is_pasteable() {
let out = render_bibtex(&select(None));
assert!(out.contains("@software{pounce2026,"));
assert!(out.contains("@article{wachter2006,"));
assert!(out.contains("doi = {10.5281/zenodo.20387011}"));
}
}