sourceright 0.1.15

Reference verification infrastructure for academic and legal citation workflows.
Documentation
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);
    }
}