From c0a001c55c7ee8bbff792a437479b38f76e75c11 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 30 Aug 2026 06:16:01 +0200 Subject: [PATCH] Remove legacy newsroom reconciliation authority --- .../src/bin/ghostlight-strategic-smoke.rs | 200 +--- crates/ghostlight-dungeon/src/newspaper.rs | 954 +----------------- crates/ghostlight-dungeon/src/registry.rs | 12 +- notes/fresh-workspace-handoff.md | 20 +- notes/ghostlight-current-system-map.md | 116 +-- state/map.yaml | 6 +- 6 files changed, 121 insertions(+), 1187 deletions(-) diff --git a/crates/ghostlight-dungeon/src/bin/ghostlight-strategic-smoke.rs b/crates/ghostlight-dungeon/src/bin/ghostlight-strategic-smoke.rs index b9841dc..859a508 100644 --- a/crates/ghostlight-dungeon/src/bin/ghostlight-strategic-smoke.rs +++ b/crates/ghostlight-dungeon/src/bin/ghostlight-strategic-smoke.rs @@ -62,6 +62,43 @@ struct HistoricalWorldNewspaperArticleV2 { event_ids: Vec, } +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct HistoricalWorldNewspaperGroundingVerdict { + accepted: bool, + assessment: String, + findings: Vec, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct HistoricalWorldNewspaperEditorialVerdict { + accepted: bool, + assessment: String, + findings: Vec, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct HistoricalWorldNewspaperEditorialFinding { + article_index: u16, + category: HistoricalWorldNewspaperEditorialCategory, + passage: String, + reason: String, + rewrite_goal: String, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +enum HistoricalWorldNewspaperEditorialCategory { + BuriedLede, + ProceduralForeground, + ThroughlineDropped, + StakesAbstracted, + TensionFlattened, + RepetitiveUpdate, +} + #[allow(clippy::too_many_arguments)] fn validate_completed_newspaper_recomposition_receipt( recomposition: &serde_json::Value, @@ -180,7 +217,7 @@ fn validate_completed_newspaper_recomposition_receipt( ) } } else if current_schema { - let grounding: ghostlight_dungeon::newspaper::WorldNewspaperGroundingVerdict = + let grounding: HistoricalWorldNewspaperGroundingVerdict = serde_json::from_value(recomposition["newspaper_grounding"].clone()).map_err( |error| { anyhow::anyhow!( @@ -194,7 +231,7 @@ fn validate_completed_newspaper_recomposition_receipt( ) } if recomposition_schema == Some("ghostlight.newspaper_wave_recomposition.v2") { - let editorial: ghostlight_dungeon::newspaper::WorldNewspaperEditorialVerdict = + let editorial: HistoricalWorldNewspaperEditorialVerdict = serde_json::from_value(recomposition["newspaper_editorial"].clone()).map_err( |error| { anyhow::anyhow!( @@ -293,16 +330,6 @@ struct FoundationCheckpoint { model_receipts: Vec, } -#[derive(serde::Deserialize)] -#[serde(deny_unknown_fields)] -struct NewspaperReconciliationImportEnvelope { - schema: String, - wave_index: u64, - recovery_start_wave: usize, - world_revision: u64, - import: ghostlight_dungeon::newspaper::WorldNewspaperReconciliationImport, -} - #[derive(serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] struct ClockBindingProposalCheckpoint { @@ -974,10 +1001,6 @@ async fn main() -> anyhow::Result<()> { let initial_location_ids = campaign.locations.keys().cloned().collect::>(); (campaign, vec![], vec![], None, initial_location_ids) }; - let newspaper_reconciliation_import_path = - std::env::var_os("GHOSTLIGHT_STRATEGIC_NEWSPAPER_RECONCILIATION_IMPORT") - .map(std::path::PathBuf::from); - let mut newspaper_reconciliation_import_consumed = false; if resume { ghostlight_dungeon::compiler::validate_campaign_runtime(&campaign)?; } else { @@ -1903,9 +1926,8 @@ async fn main() -> anyhow::Result<()> { issue_path, issue_audit_path, newspaper_error, - newspaper_reconciliation_checkpoint, ) = match issue_composition { - Ok(ghostlight_dungeon::newspaper::WorldNewspaperAdvance::Accepted { composition }) => { + Ok(composition) => { let issue_path = root.join(format!("newspaper-wave-{wave_index:02}.md")); let issue_audit_path = root.join(format!("newspaper-wave-{wave_index:02}.audit.md")); @@ -1929,22 +1951,8 @@ async fn main() -> anyhow::Result<()> { Some(issue_path), Some(issue_audit_path), None, - None, ) } - Ok(ghostlight_dungeon::newspaper::WorldNewspaperAdvance::Pending { - checkpoint, - model_receipts, - }) => ( - None, - None, - None, - model_receipts, - None::, - None::, - None, - Some(checkpoint), - ), Err(error) => { let Some(failure) = error.downcast_ref::< ghostlight_dungeon::newspaper::WorldNewspaperCompositionFailure, @@ -1959,7 +1967,6 @@ async fn main() -> anyhow::Result<()> { None::, None::, Some(error.to_string()), - None, ) } }; @@ -1978,7 +1985,6 @@ async fn main() -> anyhow::Result<()> { "newspaper_press_close":newspaper_press_close, "newspaper_model_receipts":newspaper_model_receipts, "newspaper_error":newspaper_error, - "newspaper_reconciliation_checkpoint":newspaper_reconciliation_checkpoint, "issue_path":issue_path, "issue_audit_path":issue_audit_path, }); @@ -2025,31 +2031,6 @@ async fn main() -> anyhow::Result<()> { let recomposition = if recomposition_path.is_file() { read_checkpoint::(&recomposition_path)? } else { - if let Some(import_path) = newspaper_reconciliation_import_path.as_deref() { - let import = - read_checkpoint::(import_path)?; - if import.wave_index == wave_index { - if import.schema - != "ghostlight.strategic_newspaper_reconciliation_import.v1" - || import.recovery_start_wave != newspaper_recovery_start_wave - || import.world_revision != issue_campaign.revision - { - anyhow::bail!( - "strategic newspaper reconciliation import does not bind this missing issue" - ) - } - ghostlight_dungeon::newspaper::admit_world_newspaper_reconciliation_import( - &issue_campaign, - &issue_title, - &newspaper_voice, - &newsroom, - 5, - &store, - import.import, - )?; - newspaper_reconciliation_import_consumed = true; - } - } let composition = match compose_persisted_newspaper( model.as_ref(), &issue_campaign, @@ -2060,40 +2041,7 @@ async fn main() -> anyhow::Result<()> { ) .await { - Ok(ghostlight_dungeon::newspaper::WorldNewspaperAdvance::Accepted { - composition, - }) => composition, - Ok(ghostlight_dungeon::newspaper::WorldNewspaperAdvance::Pending { - checkpoint, - model_receipts, - }) => { - let checkpoint_id = checkpoint.id().to_owned(); - let checkpoint_digest = strategic_smoke_digest(&checkpoint)?; - let checkpoint_suffix = checkpoint_digest - .strip_prefix("sha256:") - .ok_or_else(|| anyhow::anyhow!("invalid checkpoint digest"))?; - publish_immutable_checkpoint( - &root.join(format!( - "newspaper-wave-{wave_index:02}-recomposition-pending-{checkpoint_suffix}.json" - )), - &serde_json::json!({ - "schema":"ghostlight.newspaper_wave_recomposition_pending.v1", - "wave_index":wave_index, - "recovery_start_wave":newspaper_recovery_start_wave, - "world_revision":issue_campaign.revision, - "source_campaign_digest":source_campaign_digest, - "editorial_contract_digest":editorial_contract_digest, - "checkpoint_digest":checkpoint_digest, - "checkpoint":checkpoint, - "model_receipt_set_digest":strategic_smoke_digest(&model_receipts)?, - "model_receipts":model_receipts, - }), - )?; - anyhow::bail!( - "newspaper reconciliation is pending at checkpoint {}", - checkpoint_id - ) - } + Ok(composition) => composition, Err(error) => { let model_receipts = error .downcast_ref::< @@ -2193,11 +2141,6 @@ async fn main() -> anyhow::Result<()> { ); report.insert("newspaper_error".into(), serde_json::Value::Null); } - if newspaper_reconciliation_import_path.is_some() - && !newspaper_reconciliation_import_consumed - { - anyhow::bail!("configured newspaper reconciliation import was not consumed") - } } let final_plan = final_wave_field(&wave_reports, "plan")?; let final_commit = final_wave_field(&wave_reports, "commit")?; @@ -2222,57 +2165,7 @@ async fn main() -> anyhow::Result<()> { ) .await { - Ok(ghostlight_dungeon::newspaper::WorldNewspaperAdvance::Accepted { composition }) => { - composition - } - Ok(ghostlight_dungeon::newspaper::WorldNewspaperAdvance::Pending { - checkpoint, - model_receipts, - }) => { - let checkpoint_id = checkpoint.id().to_owned(); - let pending_result = serde_json::json!({ - "schema":"ghostlight.live_strategic_smoke_pending.v1", - "scenario_id":scenario_id, - "pressure":pressure, - "wave_count":wave_count, - "newspaper_recovery_start_wave":newspaper_recovery_start_wave, - "campaign_id":campaign.id, - "elapsed_seconds":started.elapsed().as_secs_f64(), - "model_runtime":model_selection.status("configured"), - "world_compile":&world_compile, - "model_receipt_hash":&final_model_receipt_hash, - "model_stage_receipts":&model_stage_receipts, - "plan":&final_plan, - "commit":&final_commit, - "waves":&wave_reports, - "event_count":campaign.events.len(), - "news_count":campaign.news.len(), - "final_newspaper_checkpoint":checkpoint, - "final_newspaper_model_receipts":model_receipts, - "player_location_unchanged":true, - "player_state_unchanged":true, - "store":root.join("campaign.cc") - }); - let result_path = root.join("result.json"); - std::fs::write(&result_path, serde_json::to_vec_pretty(&pending_result)?)?; - std::fs::write( - root.join("status.json"), - serde_json::to_vec_pretty(&serde_json::json!({ - "schema":"ghostlight.live_strategic_smoke_status.v1", - "state":"pending_newspaper_reconciliation", - "waves_completed":wave_count, - "waves_requested":wave_count, - "newspaper_recovery_start_wave":newspaper_recovery_start_wave, - "world_revision":campaign.revision, - "event_count":campaign.events.len(), - "news_count":campaign.news.len(), - "updated_at":Utc::now(), - "result_path":result_path, - "newspaper_checkpoint_id":checkpoint_id, - }))?, - )?; - anyhow::bail!("final newspaper reconciliation is pending at checkpoint {checkpoint_id}") - } + Ok(composition) => composition, Err(error) => { let final_newspaper_model_receipts = error .downcast_ref::() @@ -2394,7 +2287,7 @@ async fn compose_persisted_newspaper( editorial_voice: &str, max_articles: usize, store: &ghostlight_dungeon::persistence::CampaignStore, -) -> anyhow::Result { +) -> anyhow::Result { ghostlight_dungeon::newspaper::advance_world_newspaper( model, campaign, @@ -2814,7 +2707,8 @@ fn strategic_campaign() -> ghostlight_dungeon::domain::Campaign { #[cfg(test)] mod tests { use super::{ - HistoricalWorldNewspaperArticleV2, HistoricalWorldNewspaperIssueV2, + HistoricalWorldNewspaperArticleV2, HistoricalWorldNewspaperEditorialVerdict, + HistoricalWorldNewspaperGroundingVerdict, HistoricalWorldNewspaperIssueV2, admitted_public_channel, civic_manifest_is_committed_candidate, committed_elaboration_mutation_proof, completed_wave_issue_campaign, final_wave_field, latest_partial_wave_checkpoint, missing_newspaper_report_indices, @@ -3008,12 +2902,12 @@ mod tests { let audit_copy = ghostlight_dungeon::newspaper::render_world_newspaper_audit_markdown(&issue); let receipts: Vec = Vec::new(); - let grounding = ghostlight_dungeon::newspaper::WorldNewspaperGroundingVerdict { + let grounding = HistoricalWorldNewspaperGroundingVerdict { accepted: true, assessment: "Exact current grounding".into(), findings: Vec::new(), }; - let editorial = ghostlight_dungeon::newspaper::WorldNewspaperEditorialVerdict { + let editorial = HistoricalWorldNewspaperEditorialVerdict { accepted: true, assessment: "Exact current editorial acceptance".into(), findings: Vec::new(), diff --git a/crates/ghostlight-dungeon/src/newspaper.rs b/crates/ghostlight-dungeon/src/newspaper.rs index 4e7eec7..2e8d42b 100644 --- a/crates/ghostlight-dungeon/src/newspaper.rs +++ b/crates/ghostlight-dungeon/src/newspaper.rs @@ -28,7 +28,6 @@ const MAX_FRONT_PAGE_ARTICLES: usize = 6; const MAX_PUBLIC_RECORD_QUERY_RESULTS: usize = 24; const MAX_NARRATIVE_SELECTION_STEPS: usize = 12; const NEWSROOM_CONTRACT_VERSION: &str = "character-newsroom.v9"; -const LEGACY_RECONCILIATION_CONTRACT_VERSION: &str = "canopy-ledger-night-editor.v7"; const EDITION_LABEL: &str = "Current Edition"; const ALLOWED_SECTIONS: [&str; 6] = [ "Front Page", @@ -247,13 +246,6 @@ pub enum WorldNewspaperGroundingCategory { MechanicalCopy, } -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -pub struct WorldNewspaperGroundingVerdict { - pub accepted: bool, - pub assessment: String, - pub findings: Vec, -} - #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct WorldNewspaperComposition { pub schema: String, @@ -285,109 +277,10 @@ pub struct WorldNewspaperPressClose { pub printed_page_digest: String, } -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -pub struct WorldNewspaperEditorialFinding { - pub article_index: u16, - pub category: WorldNewspaperEditorialCategory, - #[schemars(length(min = 1, max = 500))] - pub passage: String, - #[schemars(length(min = 1, max = 500))] - pub reason: String, - #[schemars(length(min = 1, max = 500))] - pub rewrite_goal: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum WorldNewspaperEditorialCategory { - BuriedLede, - ProceduralForeground, - ThroughlineDropped, - StakesAbstracted, - TensionFlattened, - RepetitiveUpdate, -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -pub struct WorldNewspaperEditorialVerdict { - pub accepted: bool, - #[schemars(length(min = 1, max = 500))] - pub assessment: String, - #[schemars(length(max = 24))] - pub findings: Vec, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(tag = "state", rename_all = "snake_case")] -pub enum WorldNewspaperAdvance { - Accepted { - composition: WorldNewspaperComposition, - }, - Pending { - checkpoint: WorldNewspaperReconciliationCheckpoint, - model_receipts: Vec, - }, -} - -impl WorldNewspaperAdvance { - pub fn model_receipts(&self) -> &[ModelStageReceipt] { - match self { - Self::Accepted { composition } => &composition.model_receipts, - Self::Pending { model_receipts, .. } => model_receipts, - } - } - - pub fn into_accepted(self) -> Option { - match self { - Self::Accepted { composition } => Some(composition), - Self::Pending { .. } => None, - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum WorldNewspaperCheckpointOrigin { - InitialReview, - Reconciliation, - LegacyTerminalImport, -} - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] enum WorldNewspaperCloseOrigin { InitialCopyDesk, - LegacyV7Checkpoint, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -#[serde(tag = "owner", rename_all = "snake_case")] -enum WorldNewspaperDeskRejection { - CopyDesk { - verdict: WorldNewspaperGroundingVerdict, - }, - NightEditor { - verdict: WorldNewspaperEditorialVerdict, - }, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(deny_unknown_fields)] -pub struct WorldNewspaperReconciliationCheckpoint { - schema: String, - id: String, - publication_task_binding: String, - editorial_binding: String, - generation: u32, - previous_checkpoint_id: Option, - origin: WorldNewspaperCheckpointOrigin, - source_witness_digest: Option, - #[serde(default)] - editorial_agenda: Option, - draft: EditorialPageDraft, - rejection: WorldNewspaperDeskRejection, - model_receipt_ids: Vec, - receipt_chain_digest: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] @@ -406,40 +299,6 @@ struct WorldNewspaperCloseCheckpoint { receipt_chain_digest: String, } -impl WorldNewspaperReconciliationCheckpoint { - pub fn id(&self) -> &str { - &self.id - } - - pub fn editorial_binding(&self) -> &str { - &self.editorial_binding - } - - pub fn generation(&self) -> u32 { - self.generation - } - - pub fn previous_checkpoint_id(&self) -> Option<&str> { - self.previous_checkpoint_id.as_deref() - } - - pub fn model_receipt_ids(&self) -> &[String] { - &self.model_receipt_ids - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(deny_unknown_fields)] -pub struct WorldNewspaperReconciliationImport { - schema: String, - source_witness_digest: String, - #[serde(default)] - editorial_agenda: Option, - draft: EditorialPageDraft, - verdict: WorldNewspaperGroundingVerdict, - model_receipts: Vec, -} - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(deny_unknown_fields)] struct PersistedWorldNewspaperComposition { @@ -1135,28 +994,6 @@ fn journalist_by_id<'a>( .ok_or_else(|| anyhow!("editorial pitch assigned an unknown journalist {journalist_id}")) } -fn normalize_legacy_agenda( - agenda: &mut WorldNewspaperEditorialAgenda, - newsroom: &WorldNewspaperNewsroom, -) { - for (index, pitch) in agenda.story_pitches.iter_mut().enumerate() { - if pitch.section.is_empty() { - pitch.section = if index == 0 { - "Front Page" - } else { - "Dispatches" - } - .into(); - } - if pitch.journalist_id.is_empty() { - pitch.journalist_id = newsroom.journalists[index % newsroom.journalists.len()] - .staff - .id - .clone(); - } - } -} - fn selected_record_ids<'a>(record_ids: &'a [String], label: &str) -> Result> { if record_ids.is_empty() { return Err(anyhow!("{label} selected no public record")); @@ -1430,8 +1267,6 @@ struct PreparedNewspaper { source_receipt_ids: Vec, publication_task_binding: String, binding: String, - legacy_publication_task_binding: String, - legacy_binding: String, } fn prepare_newspaper( @@ -1477,21 +1312,6 @@ fn prepare_newspaper( max_articles, &records, )?; - let legacy_publication_task_binding = publication_task_binding_for_contract( - LEGACY_RECONCILIATION_CONTRACT_VERSION, - campaign, - &title, - &editorial_voice, - max_articles, - )?; - let legacy_binding = editorial_binding_for_contract( - LEGACY_RECONCILIATION_CONTRACT_VERSION, - campaign, - &title, - &editorial_voice, - max_articles, - &records, - )?; Ok(PreparedNewspaper { title, editorial_voice, @@ -1500,8 +1320,6 @@ fn prepare_newspaper( source_receipt_ids, publication_task_binding, binding, - legacy_publication_task_binding, - legacy_binding, }) } @@ -1649,15 +1467,6 @@ async fn run_assigned_journalists( Ok((EditorialPageDraft { articles }, receipts)) } -fn legacy_copy_desk_report( - verdict: WorldNewspaperGroundingVerdict, -) -> WorldNewspaperCopyDeskReport { - WorldNewspaperCopyDeskReport { - assessment: verdict.assessment, - queries: verdict.findings, - } -} - fn close_checkpoint_identity( publication_task_binding: &str, editorial_binding: &str, @@ -1750,110 +1559,6 @@ fn persist_close_checkpoint( Ok(()) } -fn checkpoint_identity( - publication_task_binding: &str, - binding: &str, - generation: u32, - previous_checkpoint_id: Option<&str>, - origin: &WorldNewspaperCheckpointOrigin, - source_witness_digest: Option<&str>, - editorial_agenda: Option<&WorldNewspaperEditorialAgenda>, - draft: &EditorialPageDraft, - rejection: &WorldNewspaperDeskRejection, - model_receipt_ids: &[String], - receipt_chain_digest: &str, -) -> Result { - Ok(format!( - "newspaper-reconciliation:sha256:{:x}", - Sha256::digest(rmp_serde::to_vec_named(&( - publication_task_binding, - binding, - generation, - previous_checkpoint_id, - origin, - source_witness_digest, - editorial_agenda, - draft, - rejection, - model_receipt_ids, - receipt_chain_digest, - ))?) - )) -} - -#[allow(clippy::too_many_arguments)] -fn new_reconciliation_checkpoint( - publication_task_binding: &str, - binding: &str, - generation: u32, - previous_checkpoint_id: Option, - origin: WorldNewspaperCheckpointOrigin, - source_witness_digest: Option, - editorial_agenda: Option, - draft: EditorialPageDraft, - rejection: WorldNewspaperDeskRejection, - receipts: &[ModelStageReceipt], -) -> Result { - let model_receipt_ids = receipts - .iter() - .map(|receipt| receipt.storage_key().to_owned()) - .collect::>(); - let receipt_chain_digest = checkpoint_receipt_chain_digest(&model_receipt_ids)?; - let id = checkpoint_identity( - publication_task_binding, - binding, - generation, - previous_checkpoint_id.as_deref(), - &origin, - source_witness_digest.as_deref(), - editorial_agenda.as_ref(), - &draft, - &rejection, - &model_receipt_ids, - &receipt_chain_digest, - )?; - Ok(WorldNewspaperReconciliationCheckpoint { - schema: "ghostlight.world_newspaper_reconciliation_checkpoint.v3".into(), - id, - publication_task_binding: publication_task_binding.into(), - editorial_binding: binding.into(), - generation, - previous_checkpoint_id, - origin, - source_witness_digest, - editorial_agenda, - draft, - rejection, - model_receipt_ids, - receipt_chain_digest, - }) -} - -fn persist_reconciliation_checkpoint( - store: &CampaignStore, - checkpoint: &WorldNewspaperReconciliationCheckpoint, -) -> Result<()> { - let kind = "world_newspaper_reconciliation_checkpoint.v3"; - if let Some((_, existing)) = - store.load::(kind, checkpoint.id())? - { - if existing != *checkpoint { - return Err(anyhow!( - "immutable newspaper reconciliation checkpoint conflict: {}", - checkpoint.id() - )); - } - return Ok(()); - } - store.insert( - kind, - "ghostlight.world_newspaper_reconciliation_checkpoint.v3", - checkpoint.id(), - checkpoint, - )?; - Ok(()) -} - fn persist_newspaper_completion( store: &CampaignStore, publication_task_binding: &str, @@ -1888,219 +1593,6 @@ fn persist_newspaper_completion( Ok(()) } -fn load_checkpoint_receipts( - store: &CampaignStore, - checkpoint: &WorldNewspaperReconciliationCheckpoint, -) -> Result> { - checkpoint - .model_receipt_ids - .iter() - .map(|receipt_id| { - store - .load::("persona_stage_receipt.v1", receipt_id)? - .map(|(_, receipt)| receipt) - .ok_or_else(|| anyhow!("newspaper checkpoint lost model receipt {receipt_id}")) - }) - .collect() -} - -fn validate_reconciliation_checkpoint( - checkpoint: &WorldNewspaperReconciliationCheckpoint, - prepared: &PreparedNewspaper, - max_articles: usize, - receipts: &[ModelStageReceipt], - expected_publication_task_binding: &str, - expected_editorial_binding: &str, -) -> Result<()> { - if checkpoint.schema != "ghostlight.world_newspaper_reconciliation_checkpoint.v3" - || checkpoint.publication_task_binding != expected_publication_task_binding - || checkpoint.editorial_binding != expected_editorial_binding - || checkpoint.editorial_agenda.is_none() - || receipts.len() != checkpoint.model_receipt_ids.len() - { - return Err(anyhow!("newspaper reconciliation checkpoint is invalid")); - } - if checkpoint.generation == 0 && checkpoint.previous_checkpoint_id.is_some() - || checkpoint.generation > 0 && checkpoint.previous_checkpoint_id.is_none() - { - return Err(anyhow!( - "newspaper reconciliation checkpoint lineage is invalid" - )); - } - let receipt_ids = receipts - .iter() - .map(|receipt| receipt.storage_key().to_owned()) - .collect::>(); - if receipt_ids != checkpoint.model_receipt_ids - || receipt_ids.iter().collect::>().len() != receipt_ids.len() - || checkpoint_receipt_chain_digest(&receipt_ids)? != checkpoint.receipt_chain_digest - || checkpoint_identity( - &checkpoint.publication_task_binding, - &checkpoint.editorial_binding, - checkpoint.generation, - checkpoint.previous_checkpoint_id.as_deref(), - &checkpoint.origin, - checkpoint.source_witness_digest.as_deref(), - checkpoint.editorial_agenda.as_ref(), - &checkpoint.draft, - &checkpoint.rejection, - &checkpoint.model_receipt_ids, - &checkpoint.receipt_chain_digest, - )? != checkpoint.id - { - return Err(anyhow!( - "newspaper reconciliation checkpoint receipt binding is invalid" - )); - } - let ancestry_valid = match checkpoint.origin { - WorldNewspaperCheckpointOrigin::LegacyTerminalImport => { - if checkpoint.editorial_agenda.is_some() { - let editor_index = receipts - .iter() - .position(|receipt| receipt.stage == "newspaper_editor"); - editor_index.is_some_and(|index| { - index > 0 - && receipts[..index].iter().all(|receipt| { - receipt.stage == "newspaper_narrative_selection_agent_action" - }) - && receipts - .get(index + 1) - .map(|receipt| receipt.stage.as_str()) - == Some("newspaper_copy_desk") - }) - } else { - receipts.first().map(|receipt| receipt.stage.as_str()) == Some("newspaper_editor") - && receipts.get(1).map(|receipt| receipt.stage.as_str()) - == Some("newspaper_copy_desk") - } - } - WorldNewspaperCheckpointOrigin::InitialReview - | WorldNewspaperCheckpointOrigin::Reconciliation => { - let editor_index = receipts - .iter() - .position(|receipt| receipt.stage == "newspaper_journalist_agent_action"); - checkpoint.editorial_agenda.is_some() - && editor_index.is_some_and(|index| { - index > 0 - && receipts[..index].iter().all(|receipt| { - receipt.stage == "newspaper_narrative_selection_agent_action" - }) - && receipts[index..receipts.len().saturating_sub(1)] - .iter() - .all(|receipt| receipt.stage == "newspaper_journalist_agent_action") - && receipts.last().map(|receipt| receipt.stage.as_str()) - == Some("newspaper_copy_desk") - }) - } - }; - if !ancestry_valid - || receipts - .first() - .map(|receipt| receipt.snapshot_binding.as_str()) - != Some(expected_editorial_binding) - { - return Err(anyhow!( - "newspaper reconciliation checkpoint lost its narrative, editor, or copy-desk ancestry" - )); - } - validate_editorial_draft(&prepared.records, &checkpoint.draft, max_articles)?; - if let Some(agenda) = &checkpoint.editorial_agenda { - validate_editorial_agenda(&prepared.records, &prepared.newsroom, agenda, max_articles)?; - validate_editorial_alignment(&checkpoint.draft, &prepared.newsroom, agenda)?; - } - validate_desk_rejection(&checkpoint.draft, &checkpoint.rejection)?; - let latest_review_stage = receipts - .iter() - .rev() - .find(|receipt| { - matches!( - receipt.stage.as_str(), - "newspaper_copy_desk" | "newspaper_night_editor" - ) - }) - .map(|receipt| receipt.stage.as_str()); - let expected_review_stage = match &checkpoint.rejection { - WorldNewspaperDeskRejection::CopyDesk { .. } => "newspaper_copy_desk", - WorldNewspaperDeskRejection::NightEditor { .. } => "newspaper_night_editor", - }; - if latest_review_stage != Some(expected_review_stage) { - return Err(anyhow!( - "newspaper reconciliation checkpoint lost its rejecting desk receipt" - )); - } - Ok(()) -} - -fn load_legacy_reconciliation_tip( - store: &CampaignStore, - prepared: &PreparedNewspaper, - max_articles: usize, -) -> Result< - Option<( - WorldNewspaperReconciliationCheckpoint, - Vec, - )>, -> { - let checkpoints = store - .load_all::( - "world_newspaper_reconciliation_checkpoint.v3", - )? - .into_iter() - .filter(|checkpoint| { - checkpoint.publication_task_binding == prepared.legacy_publication_task_binding - }) - .collect::>(); - if checkpoints.is_empty() { - return Ok(None); - } - let by_id = checkpoints - .iter() - .map(|checkpoint| (checkpoint.id.as_str(), checkpoint)) - .collect::>(); - let referenced = checkpoints - .iter() - .filter_map(|checkpoint| checkpoint.previous_checkpoint_id.as_deref()) - .collect::>(); - let tips = checkpoints - .iter() - .filter(|checkpoint| !referenced.contains(checkpoint.id.as_str())) - .collect::>(); - if tips.len() != 1 { - return Err(anyhow!( - "newspaper reconciliation checkpoint chain has multiple or missing tips" - )); - } - for checkpoint in &checkpoints { - let receipts = load_checkpoint_receipts(store, checkpoint)?; - validate_reconciliation_checkpoint( - checkpoint, - prepared, - max_articles, - &receipts, - &prepared.legacy_publication_task_binding, - &prepared.legacy_binding, - )?; - if let Some(previous_id) = checkpoint.previous_checkpoint_id.as_deref() { - let previous = by_id - .get(previous_id) - .ok_or_else(|| anyhow!("newspaper reconciliation checkpoint parent is missing"))?; - if checkpoint.generation != previous.generation + 1 - || !checkpoint - .model_receipt_ids - .starts_with(&previous.model_receipt_ids) - || checkpoint.model_receipt_ids.len() <= previous.model_receipt_ids.len() - { - return Err(anyhow!( - "newspaper reconciliation checkpoint chain is not an exact receipt-prefix extension" - )); - } - } - } - let tip = (*tips[0]).clone(); - let receipts = load_checkpoint_receipts(store, &tip)?; - Ok(Some((tip, receipts))) -} - fn validate_copy_desk_report( draft: &EditorialPageDraft, report: &WorldNewspaperCopyDeskReport, @@ -2120,7 +1612,6 @@ fn validate_copy_desk_report( } fn validate_close_checkpoint( - store: &CampaignStore, checkpoint: &WorldNewspaperCloseCheckpoint, prepared: &PreparedNewspaper, max_articles: usize, @@ -2133,42 +1624,12 @@ fn validate_close_checkpoint( { return Err(anyhow!("newspaper close checkpoint is invalid")); } - match checkpoint.origin { - WorldNewspaperCloseOrigin::InitialCopyDesk => { - if checkpoint.source_checkpoint_id.is_some() { - return Err(anyhow!( - "initial newspaper close checkpoint names a legacy source" - )); - } - } - WorldNewspaperCloseOrigin::LegacyV7Checkpoint => { - let source_checkpoint_id = checkpoint - .source_checkpoint_id - .as_deref() - .ok_or_else(|| anyhow!("legacy newspaper close lost its source checkpoint"))?; - let source = store - .load::( - "world_newspaper_reconciliation_checkpoint.v3", - source_checkpoint_id, - )? - .map(|(_, checkpoint)| checkpoint) - .ok_or_else(|| anyhow!("legacy newspaper close source checkpoint is missing"))?; - if source.id != source_checkpoint_id - || source.publication_task_binding != prepared.legacy_publication_task_binding - || source.editorial_binding != prepared.legacy_binding - || source.draft != checkpoint.draft - || source.editorial_agenda.as_ref() != Some(&checkpoint.editorial_agenda) - || !matches!( - source.rejection, - WorldNewspaperDeskRejection::CopyDesk { ref verdict } - if legacy_copy_desk_report(verdict.clone()) == checkpoint.copy_desk - ) - { - return Err(anyhow!( - "legacy newspaper close source checkpoint does not bind its imported page" - )); - } - } + if checkpoint.origin != WorldNewspaperCloseOrigin::InitialCopyDesk + || checkpoint.source_checkpoint_id.is_some() + { + return Err(anyhow!( + "newspaper close checkpoint does not belong to the current newsroom" + )); } let receipt_ids = receipts .iter() @@ -2251,37 +1712,7 @@ fn load_close_checkpoint( .ok_or_else(|| anyhow!("newspaper close lost model receipt {receipt_id}")) }) .collect::>>()?; - validate_close_checkpoint(store, &checkpoint, prepared, max_articles, &receipts)?; - Ok(Some((checkpoint, receipts))) -} - -fn import_legacy_close_checkpoint( - store: &CampaignStore, - prepared: &PreparedNewspaper, - max_articles: usize, -) -> Result)>> { - let Some((legacy, receipts)) = load_legacy_reconciliation_tip(store, prepared, max_articles)? - else { - return Ok(None); - }; - let WorldNewspaperDeskRejection::CopyDesk { verdict } = legacy.rejection.clone() else { - return Err(anyhow!( - "legacy v7 Night Editor rejection cannot become a factual close report without replay" - )); - }; - let checkpoint = new_close_checkpoint( - prepared, - WorldNewspaperCloseOrigin::LegacyV7Checkpoint, - Some(legacy.id), - legacy - .editorial_agenda - .ok_or_else(|| anyhow!("legacy newspaper checkpoint lost its agenda"))?, - legacy.draft, - legacy_copy_desk_report(verdict), - &receipts, - )?; - validate_close_checkpoint(store, &checkpoint, prepared, max_articles, &receipts)?; - persist_close_checkpoint(store, &checkpoint)?; + validate_close_checkpoint(&checkpoint, prepared, max_articles, &receipts)?; Ok(Some((checkpoint, receipts))) } @@ -2381,13 +1812,7 @@ fn validate_persisted_composition( .ok_or_else(|| anyhow!("persisted newspaper lost model receipt {receipt_id}")) }) .collect::>>()?; - validate_close_checkpoint( - store, - &checkpoint, - prepared, - max_articles, - &checkpoint_receipts, - )?; + validate_close_checkpoint(&checkpoint, prepared, max_articles, &checkpoint_receipts)?; if composition.model_receipts.len() != checkpoint_receipts.len() + 1 || !composition.model_receipts.starts_with(&checkpoint_receipts) || composition.copy_desk != checkpoint.copy_desk @@ -2489,8 +1914,8 @@ async fn close_world_newspaper( store: &CampaignStore, checkpoint: WorldNewspaperCloseCheckpoint, mut receipts: Vec, -) -> Result { - validate_close_checkpoint(store, &checkpoint, prepared, max_articles, &receipts)?; +) -> Result { + validate_close_checkpoint(&checkpoint, prepared, max_articles, &receipts)?; let source_json = source_json_for_copy_queries( &prepared.records, &checkpoint.editorial_agenda, @@ -2586,7 +2011,7 @@ async fn close_world_newspaper( Some(checkpoint.id), &composition, )?; - Ok(WorldNewspaperAdvance::Accepted { composition }) + Ok(composition) } pub async fn advance_world_newspaper( @@ -2597,16 +2022,14 @@ pub async fn advance_world_newspaper( newsroom: &WorldNewspaperNewsroom, max_articles: usize, store: &CampaignStore, -) -> Result { +) -> Result { let prepared = prepare_newspaper(campaign, title, editorial_voice, newsroom, max_articles)?; if let Some((_, persisted)) = store.load::( "world_newspaper_composition.v3", &prepared.publication_task_binding, )? { validate_persisted_composition(store, campaign, &prepared, max_articles, &persisted)?; - return Ok(WorldNewspaperAdvance::Accepted { - composition: persisted.composition, - }); + return Ok(persisted.composition); } if let Some((checkpoint, receipts)) = load_close_checkpoint(store, &prepared, max_articles)? { return close_world_newspaper( @@ -2620,20 +2043,6 @@ pub async fn advance_world_newspaper( ) .await; } - if let Some((checkpoint, receipts)) = - import_legacy_close_checkpoint(store, &prepared, max_articles)? - { - return close_world_newspaper( - model, - campaign, - &prepared, - max_articles, - store, - checkpoint, - receipts, - ) - .await; - } if prepared.records.is_empty() { let issue = WorldNewspaperIssue { schema: "ghostlight.world_newspaper_issue.v3".into(), @@ -2675,7 +2084,7 @@ pub async fn advance_world_newspaper( None, &composition, )?; - return Ok(WorldNewspaperAdvance::Accepted { composition }); + return Ok(composition); } let agenda_run = select_editorial_agenda(model, &prepared, max_articles) .await @@ -2729,7 +2138,7 @@ pub async fn advance_world_newspaper( copy_desk, &receipts, )?; - validate_close_checkpoint(store, &checkpoint, &prepared, max_articles, &receipts)?; + validate_close_checkpoint(&checkpoint, &prepared, max_articles, &receipts)?; persist_close_checkpoint(store, &checkpoint)?; close_world_newspaper( model, @@ -2753,7 +2162,7 @@ async fn compose_world_newspaper( ) -> Result { let directory = tempfile::tempdir()?; let store = CampaignStore::open(directory.path().join("campaign.cc"))?; - match advance_world_newspaper( + advance_world_newspaper( model, campaign, title, @@ -2762,78 +2171,7 @@ async fn compose_world_newspaper( max_articles, &store, ) - .await? - { - WorldNewspaperAdvance::Accepted { composition } => Ok(composition), - WorldNewspaperAdvance::Pending { model_receipts, .. } => Err(composition_failure( - "legacy newspaper reconciliation unexpectedly remained pending", - model_receipts, - )), - } -} - -pub fn admit_world_newspaper_reconciliation_import( - campaign: &Campaign, - title: impl Into, - editorial_voice: impl Into, - newsroom: &WorldNewspaperNewsroom, - max_articles: usize, - store: &CampaignStore, - import: WorldNewspaperReconciliationImport, -) -> Result { - let prepared = prepare_newspaper(campaign, title, editorial_voice, newsroom, max_articles)?; - let mut import = import; - if let Some(agenda) = &mut import.editorial_agenda { - normalize_legacy_agenda(agenda, newsroom); - } - let existing_tip = load_legacy_reconciliation_tip(store, &prepared, max_articles)?; - if import.schema != "ghostlight.world_newspaper_reconciliation_import.v1" - || !import.source_witness_digest.starts_with("sha256:") - || import.source_witness_digest.len() != 71 - || store - .load::( - "world_newspaper_composition.v3", - &prepared.publication_task_binding, - )? - .is_some() - { - return Err(anyhow!( - "world newspaper reconciliation import is not admissible" - )); - } - let checkpoint = new_reconciliation_checkpoint( - &prepared.legacy_publication_task_binding, - &prepared.legacy_binding, - 0, - None, - WorldNewspaperCheckpointOrigin::LegacyTerminalImport, - Some(import.source_witness_digest), - import.editorial_agenda, - import.draft, - WorldNewspaperDeskRejection::CopyDesk { - verdict: import.verdict, - }, - &import.model_receipts, - )?; - validate_reconciliation_checkpoint( - &checkpoint, - &prepared, - max_articles, - &import.model_receipts, - &prepared.legacy_publication_task_binding, - &prepared.legacy_binding, - )?; - if let Some((existing, receipts)) = existing_tip { - if existing == checkpoint && receipts == import.model_receipts { - return Ok(existing); - } - return Err(anyhow!( - "world newspaper reconciliation import would fork an existing checkpoint chain" - )); - } - store.persist_model_stage_receipts(&import.model_receipts)?; - persist_reconciliation_checkpoint(store, &checkpoint)?; - Ok(checkpoint) + .await } struct NightEditorCloseWorkbench<'a> { @@ -3697,81 +3035,6 @@ fn article_passage_occurrence_count( ) } -fn validate_legacy_grounding_verdict( - draft: &EditorialPageDraft, - verdict: &WorldNewspaperGroundingVerdict, -) -> Result<()> { - validate_single_line(&verdict.assessment, 500, "copy-desk assessment")?; - if verdict.accepted != verdict.findings.is_empty() { - return Err(anyhow!( - "copy-desk acceptance must be true exactly when findings are empty" - )); - } - let mut exact_claims = BTreeSet::new(); - for finding in &verdict.findings { - let article_index = usize::from(finding.article_index); - validate_single_line(&finding.claim_or_phrase, 500, "copy-desk claim")?; - validate_single_line(&finding.reason, 500, "copy-desk reason")?; - if !exact_claims.insert((article_index, finding.claim_or_phrase.as_str())) { - return Err(anyhow!("copy desk repeated one exact finding phrase")); - } - validate_grounding_finding_target(draft, finding)?; - } - Ok(()) -} - -fn validate_editorial_verdict( - draft: &EditorialPageDraft, - verdict: &WorldNewspaperEditorialVerdict, -) -> Result<()> { - validate_single_line(&verdict.assessment, 500, "night-editor assessment")?; - if verdict.accepted != verdict.findings.is_empty() { - return Err(anyhow!( - "night-editor acceptance must be true exactly when findings are empty" - )); - } - let mut exact_passages = BTreeSet::new(); - for finding in &verdict.findings { - validate_single_line(&finding.passage, 500, "night-editor passage")?; - validate_single_line(&finding.reason, 500, "night-editor reason")?; - validate_single_line(&finding.rewrite_goal, 500, "night-editor rewrite goal")?; - let article_index = usize::from(finding.article_index); - if !exact_passages.insert((article_index, finding.passage.as_str())) { - return Err(anyhow!("night editor repeated one exact finding passage")); - } - let occurrence_count = - article_passage_occurrence_count(draft, finding.article_index, &finding.passage) - .ok_or_else(|| anyhow!("night editor returned an invalid article index"))?; - if occurrence_count != 1 { - return Err(anyhow!( - "night-editor passage must occur exactly once in the named article" - )); - } - } - Ok(()) -} - -fn validate_desk_rejection( - draft: &EditorialPageDraft, - rejection: &WorldNewspaperDeskRejection, -) -> Result<()> { - match rejection { - WorldNewspaperDeskRejection::CopyDesk { verdict } => { - validate_legacy_grounding_verdict(draft, verdict)?; - if verdict.accepted { - return Err(anyhow!("copy-desk rejection cannot contain acceptance")); - } - } - WorldNewspaperDeskRejection::NightEditor { verdict } => { - validate_editorial_verdict(draft, verdict)?; - if verdict.accepted { - return Err(anyhow!("night-editor rejection cannot contain acceptance")); - } - } - } - Ok(()) -} - fn lower_editorial_page( campaign: &Campaign, title: String, @@ -3900,29 +3163,6 @@ fn publication_task_binding( )) } -fn publication_task_binding_for_contract( - contract_version: &str, - campaign: &Campaign, - title: &str, - voice: &str, - max_articles: usize, -) -> Result { - let bytes = rmp_serde::to_vec_named(&( - contract_version, - campaign.id, - campaign.revision, - title, - voice, - max_articles, - ))?; - Ok(format!( - "campaign:{}:revision:{}:newspaper-task:sha256:{:x}", - campaign.id, - campaign.revision, - Sha256::digest(bytes) - )) -} - fn editorial_binding( campaign: &Campaign, title: &str, @@ -3950,32 +3190,6 @@ fn editorial_binding( )) } -fn editorial_binding_for_contract( - contract_version: &str, - campaign: &Campaign, - title: &str, - voice: &str, - max_articles: usize, - records: &[PublicRecordProjection], -) -> Result { - let bytes = rmp_serde::to_vec_named(&( - contract_version, - campaign.id, - campaign.revision, - campaign.world_time, - title, - voice, - max_articles, - records, - ))?; - Ok(format!( - "campaign:{}:revision:{}:newspaper:sha256:{:x}", - campaign.id, - campaign.revision, - Sha256::digest(bytes) - )) -} - fn empty_issue_id( campaign: &Campaign, title: &str, @@ -5227,8 +4441,6 @@ mod tests { &store, ) .await - .unwrap() - .into_accepted() .unwrap(); let requests = resume_model.requests(); assert_eq!(requests.len(), 1); @@ -5249,8 +4461,6 @@ mod tests { &store, ) .await - .unwrap() - .into_accepted() .unwrap(); assert_eq!(repeated, accepted); assert!(idempotent_model.requests().is_empty()); @@ -5279,136 +4489,6 @@ mod tests { ); } - #[tokio::test] - async fn legacy_v7_copy_tip_becomes_one_v9_night_close_without_replay() { - const LEGACY_REJECTION: &str = r#"{"accepted":false,"assessment":"The deck overstates where the admitted debt reached.","findings":[{"article_index":0,"category":"unsupported_fact","claim_or_phrase":"A gambling debt reaches the throne room and leaves one official carrying the blame.","reason":"The cited source records the pawned seal and dismissal but does not locate the debt in the throne room."}]}"#; - const CURRENT_COPY_REPORT: &str = r#"{"assessment":"The deck overstates where the admitted debt reached.","queries":[{"article_index":0,"category":"unsupported_fact","claim_or_phrase":"A gambling debt reaches the throne room and leaves one official carrying the blame.","reason":"The cited source records the pawned seal and dismissal but does not locate the debt in the throne room."}]}"#; - let campaign = campaign_with_news(); - let prepared = prepare_newspaper( - &campaign, - "The Underdeep Clarion", - "A skeptical court broadsheet with dry restraint.", - &canopy_ledger_newsroom(), - 4, - ) - .unwrap(); - let source_directory = tempfile::tempdir().unwrap(); - let source_store = - CampaignStore::open(source_directory.path().join("campaign.cc")).unwrap(); - let source_model = ScriptedNewspaperModel::new([ - QUERY_ALL_RECORDS, - ONE_STORY_AGENDA, - ACCEPTED_STORY_ACTION, - CURRENT_COPY_REPORT, - ]); - let error = advance_world_newspaper( - &source_model, - &campaign, - "The Underdeep Clarion", - "A skeptical court broadsheet with dry restraint.", - &canopy_ledger_newsroom(), - 4, - &source_store, - ) - .await - .unwrap_err(); - let mut legacy_receipts = error - .downcast_ref::() - .unwrap() - .model_receipts - .clone(); - let mut receipt_keys = BTreeMap::::new(); - for receipt in &mut legacy_receipts { - let old_key = receipt.storage_key().to_owned(); - if receipt.stage == "newspaper_journalist_agent_action" { - receipt.stage = "newspaper_editor".into(); - } - for source_id in &mut receipt.source_receipt_ids { - if let Some(rebound) = receipt_keys.get(source_id) { - *source_id = rebound.clone(); - } - } - let legacy_snapshot = receipt - .snapshot_binding - .replace(&prepared.binding, &prepared.legacy_binding); - receipt.rebind_snapshot(legacy_snapshot); - receipt_keys.insert(old_key, receipt.storage_key().to_owned()); - } - - let directory = tempfile::tempdir().unwrap(); - let store = CampaignStore::open(directory.path().join("campaign.cc")).unwrap(); - let legacy_checkpoint = admit_world_newspaper_reconciliation_import( - &campaign, - "The Underdeep Clarion", - "A skeptical court broadsheet with dry restraint.", - &canopy_ledger_newsroom(), - 4, - &store, - WorldNewspaperReconciliationImport { - schema: "ghostlight.world_newspaper_reconciliation_import.v1".into(), - source_witness_digest: format!("sha256:{}", "a".repeat(64)), - editorial_agenda: Some( - serde_json::from_str::(ONE_STORY_AGENDA) - .map(|action| match action.command { - NarrativeSelectionCommand::ProposeAgenda { - dominant_throughline, - reader_stake, - story_pitches, - } => WorldNewspaperEditorialAgenda { - dominant_throughline, - reader_stake, - story_pitches, - }, - _ => unreachable!(), - }) - .unwrap(), - ), - draft: serde_json::from_str(ACCEPTED_PAGE).unwrap(), - verdict: serde_json::from_str(LEGACY_REJECTION).unwrap(), - model_receipts: legacy_receipts, - }, - ) - .unwrap(); - - let close_model = ScriptedNewspaperModel::new([QUERY_DECK_CLOSE]); - let composition = advance_world_newspaper( - &close_model, - &campaign, - "The Underdeep Clarion", - "A skeptical court broadsheet with dry restraint.", - &canopy_ledger_newsroom(), - 4, - &store, - ) - .await - .unwrap() - .into_accepted() - .unwrap(); - - let requests = close_model.requests(); - assert_eq!(requests.len(), 1); - assert_eq!( - requests[0].stage, - "newspaper_night_editor_close_agent_action" - ); - let close_checkpoints = store - .load_all::("world_newspaper_close_checkpoint.v1") - .unwrap(); - assert_eq!(close_checkpoints.len(), 1); - assert_eq!( - close_checkpoints[0].source_checkpoint_id.as_deref(), - Some(legacy_checkpoint.id()) - ); - assert!( - requests[0] - .source_receipt_ids - .contains(&close_checkpoints[0].id) - ); - assert_eq!(composition.press_close.source_page_digest.len(), 71); - assert_eq!(composition.copy_desk.queries.len(), 1); - assert_eq!(composition.model_receipts.len(), 6); - } - #[tokio::test] async fn reader_projection_escapes_model_and_consumer_markdown() { const MARKDOWN_PAGE: &str = r#"{"tool":"file_story","headline":"[Court](https://headline.invalid) Faces ","deck":"The *royal* debt now reaches every keeper of the seal.","dateline":"Room","paragraphs":["- The Thorn Court admitted the royal seal was pawned to cover a dragon's gambling debt; cannot make the confession ~~prettier~~.","1. The dismissed treasurer leaves readers with a [public record](https://copy.invalid) and the court with a seal that remains pawned."]}"#; diff --git a/crates/ghostlight-dungeon/src/registry.rs b/crates/ghostlight-dungeon/src/registry.rs index d16d7ee..c4b9aa3 100644 --- a/crates/ghostlight-dungeon/src/registry.rs +++ b/crates/ghostlight-dungeon/src/registry.rs @@ -380,7 +380,7 @@ impl CampaignRegistry { &self, request: WorldNewspaperRequest, model: &dyn ModelPort, - ) -> Result { + ) -> Result { if request.schema != "ghostlight.world_newspaper_request.v3" || request.max_articles == 0 || request.max_articles > 6 @@ -1453,8 +1453,6 @@ mod tests { .compose_world_newspaper(newspaper_request.clone(), &RegistryNewspaperModel) .await .unwrap() - .into_accepted() - .unwrap() .issue; assert_eq!(newspaper.source_world_revision, 0); assert!(newspaper.articles.is_empty()); @@ -1673,8 +1671,6 @@ mod tests { .compose_world_newspaper(newspaper_request.clone(), &RegistryNewspaperModel) .await .unwrap() - .into_accepted() - .unwrap() .issue; assert_eq!(newspaper.source_world_revision, 3); assert_eq!(newspaper.articles.len(), 1); @@ -1700,8 +1696,6 @@ mod tests { .compose_world_newspaper(newspaper_request.clone(), &RegistryNewspaperModel) .await .unwrap() - .into_accepted() - .unwrap() .issue; assert_eq!(repeated_newspaper.id, newspaper.id); assert_eq!( @@ -1719,9 +1713,7 @@ mod tests { let closed = registry .compose_world_newspaper(rejected_request, &RejectingRegistryNewspaperModel) .await - .unwrap() - .into_accepted() - .expect("the Night Editor must close the marked page once"); + .unwrap(); assert_eq!(closed.model_receipts.len(), 6); assert_eq!(closed.copy_desk.queries.len(), 1); assert!(closed.press_close.night_editor_action_applied); diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 624ef5e..45fe206 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -1377,9 +1377,16 @@ assignments and cited facts, and the affected reporter profiles; she gets one chance to resolve exactly those objections while preserving the story. The one-shot whole-page writer and generic byline registry are deleted. Deterministic press admission still owns structure, provenance, query -disposition, and closure. Focused gates pass 475/475 library tests, 16/16 -strategic-smoke tests, and all-target checks. No provider or simulation run has -been launched for v9. +disposition, and closure. + +Run 60 attempts 1 and 2 stopped before any v9 provider call. Attempt 1 exposed +the missing `wave_count + 1` final-only sentinel. Attempt 2 reached final +composition without simulation or per-wave replay, then exposed automatic v7 +close import as obsolete live authority. The current uncommitted cut deletes +legacy reconciliation/import from live newsroom ownership, makes advance return +one composition, and leaves the exact-current close checkpoint as the sole +resume owner. Gates pass 474/474 library tests, 16/16 strategic-smoke tests, and +all-target checks. Provider and reverse-Turing acceptance remain unproven. Run 54 exposed one separate upstream publicity defect: a public strategic activity propagates its channel to private `MemberMemory` outcomes, which is @@ -1390,9 +1397,10 @@ materializing a `NewsIssue`. 1. Preserve immutable Run 59 and its later-publication correction debt; do not rewrite or resimulate it. -2. Run the authorized newsroom-only provider acceptance for exact pushed - `901cfcd58794acc35b24be3d1e149fd2df0f6c3d`. Measure actual cached tokens and - freeze the resulting reader-facing v9 front page without world replay. +2. Commit and push the verified resume-ownership cut. Resume the same Run 60 + root at final composition without simulation or per-wave replay; do not + launch a fresh campaign or import a v7 close. Measure actual cached tokens + and freeze the resulting reader-facing v9 front page. 3. Dispatch an independent blind reviewer with only that fantasy newspaper and a request for its journalistic opinion. Do not expose generation mechanics, prompts, receipts, provenance, or the acceptance hypothesis. Pass only if diff --git a/notes/ghostlight-current-system-map.md b/notes/ghostlight-current-system-map.md index f9755b0..09eb84d 100644 --- a/notes/ghostlight-current-system-map.md +++ b/notes/ghostlight-current-system-map.md @@ -1433,23 +1433,29 @@ The deliberative newsroom boundary is live under this authority map: all delegate to `advance_world_newspaper`. Resume begins from the immutable close checkpoint and does not rerun assignment, journalists, or copy desk. A persisted composition is revalidated against its close checkpoint and exact - stored receipts, then returned without model work. + stored receipts, then returned without model work. Every caller receives one + completed `WorldNewspaperComposition` or an error; there is no pending + reconciliation result for a caller to interpret or advance. - **Persistence and compatibility:** the live internal contract is `character-newsroom.v9`. Public request schema v3 carries the publication roster and binds it into the task and editorial identities. Close checkpoint - v1 binds the assigned agenda, assembled page, single copy report, task and exact editorial bindings, source - checkpoint when present, and immutable receipt chain. Composition v3 stores + v1 binds the assigned agenda, assembled page, single copy report, task and + exact editorial bindings, and immutable receipt chain. Its only valid origin + is `InitialCopyDesk`, and its compatibility-shaped `source_checkpoint_id` + must be absent; validation rejects any other origin or imported ancestry. + Composition v3 stores the printed issue, copy report, press-close witness, and receipts. Public issue schema v3 remains unchanged. Candidate agendas and front-page proofs remain - ephemeral. V7 reconciliation checkpoint v3 is read-only resume provenance: - an explicit external import may materialize the exact immutable Run 58 copy - tip once, but v9 never appends to or advances that chain. Only a v7 tip whose - rejecting owner is the copy desk can become one v9 close checkpoint; its - draft, agenda, query report, bindings, and receipt ancestry must validate - exactly. A v7 Night Editor rejection fails closed rather than being recast as - copy evidence. Run 58's v7 copy tip is imported into one v9 Night Editor - close without replaying ledger queries, agenda selection, journalists, copy desk, - world mechanics, strategic waves, or accepted per-wave publications. + ephemeral. Historical v7 reconciliation rows may remain in archived or + resumed stores as inert provenance, but the live newspaper module defines no + reconciliation loader, import envelope, or conversion path and does not + deserialize or rebind them. Historical strategic recomposition v1/v2 receipts + are a separate acceptance-driver verification surface; their typed validators + can prove original artifacts but cannot write newsroom state. Resume authority + belongs only to an exact-current close checkpoint bound to the same v9 task, + newsroom, facts, page, copy report, and receipt prefix. Run 58's v7 tip and Run + 59's printed edition remain immutable evidence in their original artifacts, + not v9 close inputs. - **Cut line:** the generic byline list and one-shot whole-page writer are deleted. Assignment-owned section, journalist, byline, citations, and order are no longer regenerated in prose output. Tool schema text and prior-action @@ -1459,6 +1465,10 @@ The deliberative newsroom boundary is live under this authority map: repairs, repeated copy calls, Night rereview, post-close model judgment, and edits to unqueried articles remain absent. Whole-article prose is the sole Night Editor close unit, and exactly the query-bearing articles are mandatory. + The legacy reconciliation schemas and advance loop, automatic store lookup, + explicit import API and environment path, v7-to-v9 close conversion, and + pending consumer branches are deleted from live authority. Existing bytes are + not migrated or erased; nothing in the live path can make them decide a close. The roster, query projection, front-page proof, and article assembly reuse the generic agent harness and existing public-record, agenda, accepted issue, and audit shapes. No dependency, crate, binary, daemon, persistent schema, @@ -1474,10 +1484,11 @@ The deliberative newsroom boundary is live under this authority map: stage, deterministic source/printed digests, exact receipt ancestry, close-checkpoint resume without reporter or copy replay, idempotent persisted composition validation, printing the checkpointed page after a structurally - rejected close, and conversion of one legacy v7 copy tip into one v9 close - without replay. Strategic recomposition v3 binds the copy - report and press witness by digest while v1/v2 remain historical validation - inputs. Independent blind editorial and grounding reviews evaluate the + rejected close. The close validator admits only `InitialCopyDesk` checkpoints + with no source ancestry, while exact-current resume and idempotence regressions + exercise the only surviving resume path. Strategic recomposition v3 binds the + copy report and press witness by digest while v1/v2 remain historical + validation inputs. Independent blind editorial and grounding reviews evaluate the resulting artifact for the acceptance experiment only; they are deliberately outside runtime publication authority. Verification remains bounded to the existing newspaper library and strategic-smoke targets plus formatting, @@ -1506,64 +1517,6 @@ or communicated public scope before `NewsIssue` materialization. Revision 27 remains immutable evidence and is not rewritten for this newsroom-only acceptance. -The v8 close persistence and legacy-resume boundary is live: - -- **Owner:** `newspaper::advance_world_newspaper` owns fresh composition, - close-checkpoint resume, legacy-tip conversion, press witnessing, and - accepted-composition idempotence. `CampaignStore` owns immutable storage. - Registry and strategic smoke provide policy inputs and consume results; they - do not reconstruct or judge the newsroom. -- **Inputs:** one exact campaign snapshot, title, voice, article budget, model - port, and store; the current v8 publication and editorial bindings; and, - during the bounded transition, an optional exact v7 reconciliation tip under - its legacy bindings. The close receives one agenda, one writer page, one copy - report, and their complete ordered receipts. -- **Outputs:** a nonempty edition yields composition v3 with one issue, one copy - report, one `WorldNewspaperPressClose`, and the receipt chain. A close action - that passes structural admission records every query and changed article. A - structurally rejected one-action close prints the checkpointed page and marks - `night_editor_action_applied=false`; it does not become pending editorial work. - Provider, schema, persistence, or checkpoint-integrity failures remain errors - before an immutable composition exists. -- **Persistent and derived state:** close checkpoint v1 persists once before the - Night Editor acts. The press witness derives exact source and printed page - digests, copy and Night Editor receipt IDs, action-applied status, addressed - query indices, and changed article indices. Composition validation reloads - every receipt, validates checkpoint lineage, reconstructs the printed draft, - re-derives the typed issue, and rejects any drift. These witnesses observe the - edition; they do not make its claims true. -- **Resume and shared paths:** every caller checks accepted composition v3, - resumes one current close checkpoint if present, then optionally converts the - unique validated v7 copy tip into one v8 close checkpoint. That conversion - retains the v7 tip's exact draft, agenda, copy findings, source identity, and - receipts while rebinding only the current task. It never invokes assignment, - writer, copy desk, mechanics, or waves. Fresh work reaches the same close - owner after assignment, writer, and copy. Live per-wave, recovery, and final - combined publication all use the thin common delegate. -- **Forbidden writers:** v8 cannot append a v7 reconciliation generation or - resume its rewrite loop. An explicit import may admit one external immutable - v7 checkpoint as provenance, but cannot infer it from error prose, fork an - existing chain, or mutate it after admission. Strategic smoke, renderers, - reviewers, and world organs cannot alter a close checkpoint, press witness, - accepted issue, or source campaign. A later correction must be new - publication state; overwriting the closed edition is forbidden. -- **Cut line:** the rewrite-desk cadence, repeated desk verdicts, pending - reconciliation advances, and copy/editor rereplay paths are absent from live - v8 authority. `WorldNewspaperAdvance::Pending` and its consumer branches - remain compatibility-shaped surface only; the v8 newspaper owner has no - pending producer. Legacy v7 schemas remain solely to validate and convert the - Run 58 copy tip. -- **Verification layer:** focused tests prove one-action close admission, - structural rejection printing the bound source page, exact press-witness - re-derivation, tamper rejection, close resume without writer or copy replay, - accepted-composition idempotence, and exact Run-58-shaped v7 copy-tip - conversion into one v8 Night Editor close. Negative gates reject incomplete - query disposition, missing mandatory or duplicate article rewrites, mutation - of frozen assignment fields, v7 Night Editor rejection masquerading as copy - evidence, multiple close checkpoints, source drift, receipt drift, and witness - drift. No event, - simulation, selector, agenda, or provenance authority moved. - The typed issue retains exact event IDs, channels, reliability, source revision, and model receipts as audit data. Its internal issue time is derived from the latest cited publication record without exposing that clock to @@ -1592,7 +1545,9 @@ write world state. snapshot, the preceding wave checkpoint's committed news-ledger boundary, the configured title and voice, the explicit resume recovery boundary (`GHOSTLIGHT_STRATEGIC_NEWSPAPER_RECOVERY_START_WAVE`, defaulting to wave one - and validated within `1..=wave_count`), and the same campaign receipt store + and validated within `1..=wave_count + 1`; selecting `wave_count + 1` is the + final-only sentinel: it recovers no per-wave editions but leaves final combined + composition enabled), and the same campaign receipt store used by ordinary newspaper composition. When an accepted recomposition already exists, its immutable receipt and exact named reader/audit files are additional inputs. @@ -1619,7 +1574,9 @@ write world state. configured recovery boundary whose issue is absent. Earlier missing reports remain historical gaps, while any already successful issue inside the selected range is left untouched. The configured recovery boundary owns selection of - the missing reports in the current invocation. A completed recomposition's + the missing reports in the current invocation; the final-only sentinel derives + an empty per-wave selection because no report can reach `wave_count + 1`. A + completed recomposition's recorded recovery boundary is immutable provenance for the invocation that produced it; it does not become a second selector or invalidate the accepted issue when a later invocation selects a narrower range. @@ -1631,7 +1588,8 @@ write world state. combined composition all call the thin `compose_persisted_newspaper` delegate into `advance_world_newspaper`, including the same assignment, writer, single copy report, one deadline close, checkpoint-resume, and receipt-persistence - owner. A later invocation resumes a close checkpoint without rerunning the + owner. The final combined composition still runs when the recovery selector + returns no per-wave reports. A later invocation resumes a close checkpoint without rerunning the writer or copy desk. Once printed, it consumes the successful recomposition artifact instead of paying for the newspaper again. The driver recomputes its wave, source-campaign, and @@ -1669,7 +1627,9 @@ write world state. `Vec` before hashing and rejects an invalid receipt shape, so creation and verification share one representation. A scope-selection regression proves that earlier missing issues and successful issues are - skipped while later missing issues are selected. Dedicated regressions prove + skipped while later missing issues are selected, and that the + `wave_count + 1` boundary selects no per-wave reports for a final-only resume. + Dedicated regressions prove that historical v2 preserves its recorded original recovery boundary and accepts exact historical bytes without current rendering while rejecting byte drift, that current issue v3 still requires typed deserialization and exact diff --git a/state/map.yaml b/state/map.yaml index b81bd4d..713de10 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -325,7 +325,7 @@ data_flow: current_status: summary: Ghostlight's generic consumer boundary admits sparse WorldSeeds, runs the hundreds-class parallel cell machinery used by Dungeon, and projects grounded newspapers from committed public events. Local acceptance proves 1,000 subjects across 200 cells and two-wave durable named-person emergence. Exact-source provider run 38 at `596e50679244b5d42b4cfdf70d723f815751b8ff` resumed the preserved world at revision 8 without replaying compilation or four committed locality elaborations, recovered 51 of 52 strategic-wave cells on pulse three and the final cell on pulse four, then committed all six waves through revision 14. Pushed commit `3a69306f3004e4af2cd57bba612a6ad8d1b8132f` implements the mapped clock-consequence owner and is verified locally: world clocks own declared consequence text and exact observable scope; a bounded agent may propose legacy scope binding, while `WorldKernel` alone admits it and materializes stable threshold-crossing events and public news through the common mutation path. Binding, emitted-event ancestry, mutation receipts, and the next newspaper boundary persist atomically. Exact-source run 39 resumed run 38 without replaying its six committed waves, bound clock consequences, committed six new waves through revision 21, and emitted Issues 7 through 12 plus a final digest. This completes the mechanical provider acceptance path; all inherited wave 1-6 checkpoint hashes match run 38 exactly and live services were unchanged. Independent blind editorial review returns FAIL: only Issue 7 approaches spicy, while Issues 8-12 are dominated by plans, filings, inspections, warnings, and abstract relationship deltas without betrayal, scandal, public reaction, sustained reversal, or material lived cost. That run-39 diagnosis is superseded for the current phase: run 51 closes the world and reconciliation mechanics, and the active defect belongs to newsroom narrative selection and self-contained semantic provenance over immutable facts. Independent grounding review also returns FAIL: Issue 9 inflates a planned expedited agenda into completion, while Issue 8 assigns unsupported feminine pronouns to Sela Venn and Eryn Tal. Other grounding mechanics pass: 208/208 citations resolve, no future events appear, no replay occurred, the player remained unchanged, all 116/116 manifest entries verify, the archive is exact, and root independently confirmed the live service PIDs and invocation IDs remained unchanged. Editorial and grounding acceptance are terminally failed for run 39. Eight titled elaborators use the generic agent harness under deterministic weighted scheduling, bounded parallelism, and Luna/Terra/Sol routing. Workers remain proposal-only; deterministic admission binds exact frozen schedules, dispatch, receipts, conflicts, and semantic verification, and `WorldKernel` alone owns atomic CultCache persistence. Destination reconciliation is a private transactional workbench, provider schema compatibility is lowered once at the shared model-connector boundary, and durable elaboration and strategic-pulse checkpoints bind to their canonical mutation proofs. The generic ModelAgent harness remains consumer-neutral transport. Provider run 23 remains stopped; earlier failed runs and checkpoint archives remain immutable provenance. The live daemons remain outside the acceptance body. Dungeon play remains regression pressure, the Delvehold game-side adapter remains unimplemented, and unpublished Mars and Kalsa destination previews remain unapproved. - provider_acceptance_checkpoint: Exact-source Run 59 at pushed commit `161ff2f19021249563e192cce9e6f76f00429d48` is printed from immutable Run 58 at world revision 27 with no selection, writer, copy-desk, world, or wave replay. Its acceptance witness binds one copy report with five queries, one Night Editor close, `night_editor_action_applied=true`, three changed articles, and unchanged live-service PID/invocation identities. Newspaper SHA-256 is `ee2ce08ceb627d433ac2d4ff7e1ebecb1437bf28b1399d23bfb3d5b066c8ab27`; audit is `b210d50d4ef1a350315d528f6d37e1e7b618926346713f737c4169c49df26b49`; immutable local archive `F:\GameCult\GhostlightDungeon\acceptance\elven-realms-autonomous-161ff2f-59.tar` is `ecb4db41e9a40ee4db8b0f83dce42d74e40f940e5930b1d3c1ced227dfec76b3`. A finalizer verifier incorrectly expected a schema field on the assessment-plus-queries report; finalization resumed without another model call. The edition satisfies the press and delivery boundary. - implementation_checkpoint: Exact pushed commit `901cfcd58794acc35b24be3d1e149fd2df0f6c3d` implements and locally verifies `character-newsroom.v9`. The assignment editor sees one embodied publication roster and owns story, section, evidence, and journalist assignment. One bounded journalist agent writes each assigned story from only its own profile, pitch, and exact cited records; assignment-owned byline, section, citations, and order are assembled deterministically. Dalen Marr alone emits one complete factual checklist. Meret Sorn receives only queried articles, affected assignments, their cited facts, and affected reporter profiles, then gets one checklist-bound close action over exactly those articles. The generic model harness now preserves typed append-only user/assistant request prefixes, keeps schemas out of prompt text, and routes stable stage/model/schema cache keys; selector query/proposal/commit and Night close each keep one stable action schema while their private deterministic workbenches enforce current legality. The one-shot whole-page writer and generic byline registry are deleted. No dependency, crate, binary, daemon, persistent schema, event authority, or simulation pass was added. Gates pass 475/475 library tests, 16/16 strategic-smoke tests, and all-target checks. Provider and reverse-Turing acceptance remain unproven. - next_action: Preserve Run 59 as immutable evidence and run the authorized newsroom-only provider acceptance for exact pushed `901cfcd58794acc35b24be3d1e149fd2df0f6c3d`; do not replay world simulation. Capture real cached-token behavior and the immutable v9 front page, then dispatch an independent blind reviewer with only that reader-facing fantasy newspaper and a request for its journalistic opinion. Pass only if the reviewer is not immediately tipped that the page is procedurally generated; do not expose mechanics, prompts, receipts, provenance, or the acceptance hypothesis, and do not redefine a merely less-bad result as passage. Preserve Run 59 findings as later publication debt and defer multi-publication expansion, TeX layout, and engraved cuts. + provider_acceptance_checkpoint: Run 60 remains the active `character-newsroom.v9` acceptance root and has made no v9 provider call. Attempt 1 stopped on the missing `wave_count + 1` final-only sentinel. Attempt 2 reached final composition without simulation or per-wave replay, then stopped because automatic v7 close import still claimed live resume authority. Run 59 remains the prior immutable printed evidence with its exact press proof and later-publication debt; neither Run 60 attempt changes it or supplies reverse-Turing evidence. + implementation_checkpoint: Exact pushed commit `901cfcd58794acc35b24be3d1e149fd2df0f6c3d` implements `character-newsroom.v9`. The assignment editor sees one embodied publication roster and owns story, section, evidence, and journalist assignment. One bounded journalist agent writes each assigned story from only its own profile, pitch, and exact cited records; assignment-owned byline, section, citations, and order are assembled deterministically. Dalen Marr alone emits one complete factual checklist. Meret Sorn receives only queried articles, affected assignments, their cited facts, and affected reporter profiles, then gets one checklist-bound close action over exactly those articles. The generic model harness preserves typed append-only user/assistant request prefixes, keeps schemas out of prompt text, and routes stable stage/model/schema cache keys. The current uncommitted resume-ownership cut removes legacy reconciliation/import from live newsroom authority, makes advance return exactly one composition, and leaves the exact-current close checkpoint as the sole resume owner. Gates pass 474/474 library tests, 16/16 strategic-smoke tests, and all-target checks. Provider and reverse-Turing acceptance remain unproven. + next_action: Commit and push the verified resume-ownership cut, then resume the same Run 60 root at final composition without replaying simulation or per-wave publications. Capture real cached-token behavior and the immutable v9 front page, then dispatch the independent blind journalistic review under the exact reverse-Turing criterion. Preserve Run 59 and both failed Run 60 attempts; do not launch a fresh campaign, import a v7 close, or redefine a merely less-bad review as passage. Defer multi-publication expansion, TeX layout, and engraved cuts. gestalt_population: Gestalt Personas own shared population baselines; persistent member deltas own named individuality; WorldKernel materialization and approval-gated fission preserve lineage and identity. Materialisation/folding changes only representation and the exact member delta, advances resolution epoch, and retires the old cover; it has no Gestalt-wide effect authority. Selected actionable member exceptions are mandatory Projector perspective owners, so aggregation cannot silently erase their decision. Presence projection is limited to nearby active leaves and exact-location dormant members, includes reciprocal player relationship evidence, and may propose an earned cast without committing it. Simulation cells are derived covers only.