From e33018eed39efc85b0412bbc40131f4be33684ef Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 27 Aug 2026 07:00:54 +0200 Subject: [PATCH] Type copy desk findings --- crates/ghostlight-dungeon/src/newspaper.rs | 34 +++++++++++++++------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/crates/ghostlight-dungeon/src/newspaper.rs b/crates/ghostlight-dungeon/src/newspaper.rs index 60ebda2..2a69a74 100644 --- a/crates/ghostlight-dungeon/src/newspaper.rs +++ b/crates/ghostlight-dungeon/src/newspaper.rs @@ -62,11 +62,20 @@ pub struct WorldNewspaperArticle { #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct WorldNewspaperGroundingFinding { pub article_index: u16, - pub category: String, + pub category: WorldNewspaperGroundingCategory, pub claim_or_phrase: String, pub reason: String, } +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum WorldNewspaperGroundingCategory { + UnsupportedFact, + UnearnedAttribution, + ProceduralLeakage, + MechanicalCopy, +} + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct WorldNewspaperGroundingVerdict { pub accepted: bool, @@ -775,15 +784,7 @@ fn validate_grounding_verdict( )); } for finding in &verdict.findings { - if usize::from(finding.article_index) >= draft.articles.len() - || ![ - "unsupported_fact", - "unearned_attribution", - "procedural_leakage", - "mechanical_copy", - ] - .contains(&finding.category.as_str()) - { + if usize::from(finding.article_index) >= draft.articles.len() { return Err(anyhow!("copy desk returned an invalid finding")); } validate_single_line(&finding.claim_or_phrase, 500, "copy-desk claim")?; @@ -1007,6 +1008,19 @@ mod tests { const ACCEPTED_PAGE: &str = r#"{"edition_label":"Evening Edition","articles":[{"section":"Front Page","headline":"Court Sells the Crown's Seal, Then the Treasurer","deck":"A gambling debt reaches the throne room and leaves one official carrying the blame.","byline":"By the political editor","dateline":"","source_news_ids":["news:seal-scandal"],"paragraphs":["The Thorn Court has admitted that its royal seal was pawned to cover a dragon's gambling debt, a confession that turns private embarrassment into a public question of custody.","The treasurer who delivered that admission in open court was dismissed soon afterward. The court has explained the firing; it has not made the seal any less pawned."]}]}"#; const ACCEPTING_COPY_DESK: &str = r#"{"accepted":true,"assessment":"The copy is fully supported by its cited public source and reads as attributed court reporting.","findings":[]}"#; + #[test] + fn copy_desk_schema_owns_the_complete_finding_category_set() { + let schema = serde_json::to_string(&schema_for!(GroundingVerdictDraft)).unwrap(); + for category in [ + "unsupported_fact", + "unearned_attribution", + "procedural_leakage", + "mechanical_copy", + ] { + assert!(schema.contains(category)); + } + } + #[tokio::test] async fn newspaper_is_editorial_copy_with_separate_provenance() { let campaign = campaign_with_news();