use serde::{Deserialize, Serialize};
use crate::csl::CslDocument;
use crate::report::{ReferenceReport, ReferenceReportSeverity};
use crate::sidecar::VerificationSidecar;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JournalScreeningRequest {
pub submission_id: String,
pub platform: JournalPlatform,
pub manuscript_label: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum JournalPlatform {
GenericWebhook,
Ojs,
ScholarOne,
EditorialManager,
EJournalPress,
ManuscriptManager,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JournalScreeningReport {
pub schema_version: String,
pub submission_id: String,
pub platform: JournalPlatform,
pub status: JournalScreeningStatus,
pub editorial_summary: String,
pub author_action_checklist: Vec<String>,
pub reference_report: crate::report::ReferenceReportJsonOutput,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum JournalScreeningStatus {
Accepted,
Screened,
BlockedForExtraction,
ScreenedWithWarnings,
ScreenedWithErrors,
}
pub const JOURNAL_SCREENING_SCHEMA_VERSION: &str = "sourceright.journal_screening.v1";
pub fn screen_journal_submission(
request: JournalScreeningRequest,
csl: &CslDocument,
sidecar: &VerificationSidecar,
) -> JournalScreeningReport {
let reference_report = ReferenceReport::from_documents(csl, sidecar);
let status = status_from_reference_report(&reference_report);
let editorial_summary = editorial_summary(&request, &reference_report, status);
let author_action_checklist = author_checklist(&reference_report);
JournalScreeningReport {
schema_version: JOURNAL_SCREENING_SCHEMA_VERSION.to_string(),
submission_id: request.submission_id,
platform: request.platform,
status,
editorial_summary,
author_action_checklist,
reference_report: reference_report.to_json_output(),
}
}
fn status_from_reference_report(report: &ReferenceReport) -> JournalScreeningStatus {
if report.total_references == 0 {
return JournalScreeningStatus::BlockedForExtraction;
}
if report
.issues
.iter()
.any(|issue| issue.severity == ReferenceReportSeverity::Error)
{
return JournalScreeningStatus::ScreenedWithErrors;
}
if report
.issues
.iter()
.any(|issue| issue.severity == ReferenceReportSeverity::Warning)
{
return JournalScreeningStatus::ScreenedWithWarnings;
}
JournalScreeningStatus::Screened
}
fn editorial_summary(
request: &JournalScreeningRequest,
report: &ReferenceReport,
status: JournalScreeningStatus,
) -> String {
format!(
"Submission {} ({}) screened with status {:?}: {} references, {} issues, {} AI-risk citation-error signals.",
request.submission_id,
request.manuscript_label,
status,
report.total_references,
report.issues.len(),
report.ai_risk_issue_count
)
}
fn author_checklist(report: &ReferenceReport) -> Vec<String> {
let mut checklist = Vec::new();
if report.error_count > 0 {
checklist.push("Correct reference records that fail CSL integrity checks.".to_string());
}
if report.warning_count > 0 {
checklist.push(
"Review missing identifiers, unverified references, and provider conflicts."
.to_string(),
);
}
if report.review_queue_count > 0 || report.unresolved_count > 0 {
checklist.push("Resolve references currently queued for manual review.".to_string());
}
if checklist.is_empty() {
checklist.push(
"No citation-integrity actions were generated by this screening run.".to_string(),
);
}
checklist
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::*;
use crate::csl::CslItem;
use crate::sidecar::{ReferenceVerification, VerificationSidecar};
#[test]
fn journal_screening_returns_editorial_and_author_outputs_without_ai_claims() {
let csl = CslDocument {
items: vec![CslItem {
id: "smith-2024".to_string(),
item_type: "article-journal".to_string(),
title: Some("Unchecked reference".to_string()),
doi: None,
extra: BTreeMap::new(),
}],
};
let mut sidecar = VerificationSidecar::empty();
sidecar
.references
.insert("smith-2024".to_string(), ReferenceVerification::default());
let report = screen_journal_submission(
JournalScreeningRequest {
submission_id: "SUB-1".to_string(),
platform: JournalPlatform::Ojs,
manuscript_label: "manuscript.docx".to_string(),
},
&csl,
&sidecar,
);
assert_eq!(report.schema_version, JOURNAL_SCREENING_SCHEMA_VERSION);
assert_eq!(report.status, JournalScreeningStatus::ScreenedWithWarnings);
assert!(
report
.editorial_summary
.contains("AI-risk citation-error signals")
);
assert!(!report.editorial_summary.contains("AI-generated"));
assert_eq!(report.author_action_checklist.len(), 1);
}
#[test]
fn journal_screening_blocks_when_extraction_has_no_references() {
let report = screen_journal_submission(
JournalScreeningRequest {
submission_id: "SUB-2".to_string(),
platform: JournalPlatform::GenericWebhook,
manuscript_label: "empty.txt".to_string(),
},
&CslDocument::empty(),
&VerificationSidecar::empty(),
);
assert_eq!(report.status, JournalScreeningStatus::BlockedForExtraction);
}
}