diff --git a/acceptance/full-world-delvehold-regions-v2.txt b/acceptance/full-world-delvehold-regions-v2.txt new file mode 100644 index 0000000..0a78e28 --- /dev/null +++ b/acceptance/full-world-delvehold-regions-v2.txt @@ -0,0 +1,8 @@ +Beyond the Confluence, approach and admit a politically inhabited elven forest realm whose watershed law, living-landscape institutions, rival offices, settlements, and public routes respond independently to the deep crisis. +Beyond the Confluence, approach and admit a politically inhabited human rail-capital realm with a government, opposition, university, mage interests, labor constituencies, dependent infrastructure, surrounding settlements, and public routes. +Beyond the Confluence, approach and admit a politically inhabited orcish oathland realm with several councils and communities, guardian-species obligations, contested enforcement, trade interests, dissent, and public routes. +Beyond the Confluence, approach and admit a politically inhabited halfling grain-and-river realm with farms, market towns, tenants, cooperatives, water authorities, insurers, faiths, political movements, and public routes. +Beyond the Confluence, approach and admit a politically inhabited goblin deep-route realm with distinct settlements, route guilds, kin networks, custodians, brokers, radicals, discounted expertise, and public routes. +Beyond the Confluence, approach and admit a second politically inhabited human polity: a tidal mercantile republic with elected harbor offices, hereditary creditors, dock unions, displaced fisher wards, rival courts, naval contracts, and public routes independent of the rail capital. +Beyond the Confluence, approach and admit a mixed border federation whose member towns, mobile communities, species, faiths, and trade leagues retain local authority while sharing delegates, mutual-aid stores, contested asylum law, watershed obligations, and public routes. +Beyond the Confluence, approach and admit an autonomous deep-ecology polity of dungeon organs, custodial communities, symbiotic species, and negotiated human-scale envoys, with legible law, dissent, material means, territorial claims, redress, and public routes; do not make it a single mystical will. diff --git a/acceptance/full-world-delvehold-v1.md b/acceptance/full-world-delvehold-v1.md index 291f2c2..a506670 100644 --- a/acceptance/full-world-delvehold-v1.md +++ b/acceptance/full-world-delvehold-v1.md @@ -91,10 +91,16 @@ ontology or turn the Greathold into an autonomous character. insurers suspend cover, armies secure infrastructure, displaced families cross borders, and an unknown party circulates proof that several foreign governments secretly financed the extraction they now condemn.` -- World locality elaborations: `5` (one per admitted realm jurisdiction) +- Ordered outer-realm requests: + `acceptance/full-world-delvehold-regions-v2.txt` (eight requests, one per + line; their order is part of the acceptance input) +- World locality elaborations: `8` (one per ordered outer-realm request) - World elaboration parallelism: `8` - Active cell budget: `240` -- Strategic waves: `12` +- Target active-cover basis points: `2000` (exactly `1200` qualified subjects) +- World complexity parallelism: `8` +- Maximum world complexity rounds: `128` +- Strategic waves: `1` - Maximum rejected pulses per wave: `4` - Provider routing: Luna fast, Terra balanced, Sol capable through the existing Codex connector diff --git a/crates/ghostlight-dungeon/src/bin/ghostlight-strategic-smoke.rs b/crates/ghostlight-dungeon/src/bin/ghostlight-strategic-smoke.rs index 1b21876..7fa1384 100644 --- a/crates/ghostlight-dungeon/src/bin/ghostlight-strategic-smoke.rs +++ b/crates/ghostlight-dungeon/src/bin/ghostlight-strategic-smoke.rs @@ -330,6 +330,20 @@ struct FoundationCheckpoint { model_receipts: Vec, } +#[derive(serde::Serialize, serde::Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +struct FoundationCommitCheckpoint { + schema: String, + location_id: String, + location_name: String, + request: String, + world_revision_before: u64, + world_revision_after: u64, + model_receipt_hashes: Vec, + commit_receipt: ghostlight_dungeon::domain::WorldCommitReceipt, + mutation_proof: TitledMutationProof, +} + #[derive(serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] struct WorldRegionPlanCheckpoint { @@ -356,6 +370,7 @@ struct WorldRegionCommitCheckpoint { jurisdiction_location_id: String, commit_receipt: ghostlight_dungeon::domain::WorldCommitReceipt, model_receipt_hashes: Vec, + mutation_proof: TitledMutationProof, } #[derive(serde::Serialize, serde::Deserialize)] @@ -401,26 +416,58 @@ struct TitledFailureCheckpoint { schedule: Option, completed_invocations: Vec, invocation_failures: Vec, + #[serde(default)] + semantic_retry_diagnostic: Option, } #[derive(serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] -struct TitledPreviewCheckpoint { +struct TitledReadyCheckpoint { schema: String, location_id: String, location_name: String, request: String, - wave: ghostlight_dungeon::elaboration::ElaborationWaveBinding, - schedule: ghostlight_dungeon::elaboration::ElaborationScheduleReceipt, - accepted_operations: Vec, - rejections: Vec, - candidate: Option, - candidate_diagnostic: Option, - model_receipt_hashes: Vec, + finalized: ghostlight_dungeon::elaboration::FinalizedWorldElaboration, #[serde(default)] resumed_from: Option, #[serde(default)] retried_dispatch_ordinals: Vec, + #[serde(default)] + semantic_retry_diagnostic: Option, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct TitledSemanticFailureCheckpoint { + schema: String, + attempt: u8, + location_id: String, + location_name: String, + base_request: String, + verification_request: String, + repair_request: Option, + diagnostic: String, + admission: ghostlight_dungeon::elaboration::WorldElaborationAdmission, + wave: ghostlight_dungeon::elaboration::ElaborationWaveBinding, + schedule: ghostlight_dungeon::elaboration::ElaborationScheduleReceipt, + model_receipt_hashes: Vec, + verifier_receipt_hashes: Vec, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct TitledVerifierExecutionFailureCheckpoint { + schema: String, + semantic_attempt: u8, + generation: u32, + location_id: String, + location_name: String, + verification_request: String, + diagnostic: String, + attempts: u8, + admission: ghostlight_dungeon::elaboration::WorldElaborationAdmission, + model_receipt_hashes: Vec, + verifier_receipt_hashes: Vec, } #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] @@ -435,7 +482,7 @@ struct TitledMutationProof { intended_effect_digest: String, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] struct TitledCommitCheckpoint { schema: String, @@ -495,7 +542,7 @@ struct ComplexityRoundFailureCheckpoint { invocation_failures: Vec, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(deny_unknown_fields)] struct ComplexityMutationCheckpoint { schema: String, @@ -505,9 +552,26 @@ struct ComplexityMutationCheckpoint { mutation_kind: String, affected_subject_ids: Vec, model_receipt_hashes: Vec, + #[serde(default)] + semantic_summary: String, commit_receipt: ghostlight_dungeon::domain::WorldCommitReceipt, } +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +struct ComplexityPreparedMutationCheckpoint { + schema: String, + round: u32, + dispatch: ghostlight_dungeon::elaboration::ElaborationDispatch, + expected_revision: u64, + proposal: ghostlight_dungeon::elaboration::WorldComplexityProposal, + parent_gestalt_id: String, + mutation_kind: String, + affected_subject_ids: Vec, + model_receipt_hashes: Vec, + semantic_summary: String, +} + #[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] struct ComplexitySupersededInvocationCheckpoint { @@ -515,10 +579,29 @@ struct ComplexitySupersededInvocationCheckpoint { round: u32, dispatch: ghostlight_dungeon::elaboration::ElaborationDispatch, retained_dispatch_ordinal: u64, - canonical_member_id: String, + canonical_subject_ids: Vec, + public_identity_keys: Vec, diagnostic: String, } +#[derive(serde::Serialize, serde::Deserialize, PartialEq)] +#[serde(deny_unknown_fields)] +struct ComplexitySemanticRejectionCheckpoint { + schema: String, + round: u32, + dispatch: ghostlight_dungeon::elaboration::ElaborationDispatch, + parent_gestalt_id: String, + proposal: ghostlight_dungeon::elaboration::WorldComplexityProposal, + verifier_receipt_hash: String, + diagnostic: String, +} + +fn complexity_semantic_rejection_diagnostic( + verdict: &ghostlight_dungeon::elaboration::WorldComplexitySemanticVerification, +) -> anyhow::Result { + Ok(serde_json::to_string(verdict)?) +} + #[derive(serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] struct ComplexityRoundCheckpoint { @@ -536,8 +619,307 @@ struct ComplexityRoundCheckpoint { >, } -fn complexity_session_journal_summary(mutation_kind: &str, parent_gestalt_id: &str) -> String { - format!("Applied {mutation_kind} to {parent_gestalt_id}.") +fn complexity_affected_subject_ids( + proposal: &ghostlight_dungeon::elaboration::WorldComplexityProposal, +) -> Vec { + match proposal { + ghostlight_dungeon::elaboration::WorldComplexityProposal::Fission { preview, .. } => { + std::iter::once(preview.parent_gestalt_id.clone()) + .chain(preview.children.iter().map(|child| child.id.clone())) + .collect() + } + ghostlight_dungeon::elaboration::WorldComplexityProposal::Individuate { + individuation, + .. + } => vec![ + individuation.gestalt_id.clone(), + ghostlight_dungeon::domain::gestalt_member_subject_id(&individuation.member.id), + ], + } +} + +fn complexity_session_journal_summary( + proposal: &ghostlight_dungeon::elaboration::WorldComplexityProposal, +) -> anyhow::Result { + let semantic_delta = match proposal { + ghostlight_dungeon::elaboration::WorldComplexityProposal::Fission { preview, .. } => { + let parent = preview + .children + .iter() + .find(|child| child.id == preview.residual_child_id) + .ok_or_else(|| anyhow::anyhow!("complexity fission summary lost its residual"))?; + serde_json::json!({ + "specific_children":preview.children.iter() + .filter(|child|child.id != preview.residual_child_id) + .map(|child|serde_json::json!({ + "name":child.name, + "capabilities":child.shared_capabilities.difference(&parent.shared_capabilities).collect::>(), + "knowledge":child.shared_knowledge.difference(&parent.shared_knowledge).collect::>(), + "goals":child.goals.iter().filter(|goal|!parent.goals.contains(goal)).collect::>(), + "pressures":child.pressures.iter().filter(|pressure|!parent.pressures.contains(pressure)).collect::>(), + })).collect::>() + }) + } + ghostlight_dungeon::elaboration::WorldComplexityProposal::Individuate { + individuation, + .. + } => serde_json::json!({ + "person":individuation.member.name, + "capabilities":individuation.member.capability_additions, + "knowledge":individuation.member.knowledge_additions, + "obligations":individuation.member.obligations, + "relationships":individuation.member.relationships, + "goals":individuation.member.goals, + "memories":individuation.member.memories, + }), + }; + Ok(bounded_prompt_excerpt( + &format!( + "Applied {} to {}. Semantic delta: {}", + proposal.mutation_kind(), + proposal.parent_gestalt_id(), + semantic_delta + ), + 900, + )) +} + +fn validate_complexity_round_session_checkpoints( + campaign: &ghostlight_dungeon::domain::Campaign, + previous: &std::collections::BTreeMap< + String, + ghostlight_dungeon::elaboration::ElaboratorSessionCheckpoint, + >, + current: &std::collections::BTreeMap< + String, + ghostlight_dungeon::elaboration::ElaboratorSessionCheckpoint, + >, + session_routes: &std::collections::BTreeMap< + String, + (ghostlight_dungeon::elaboration::ElaboratorTitle, String), + >, + journals: &std::collections::BTreeMap< + String, + Vec, + >, + rejection_findings: &std::collections::BTreeMap>, + through_world_revision: u64, +) -> anyhow::Result<()> { + let touched = journals + .keys() + .chain(rejection_findings.keys()) + .cloned() + .collect::>(); + let expected_ids = previous + .keys() + .chain(touched.iter()) + .cloned() + .collect::>(); + if current + .keys() + .cloned() + .collect::>() + != expected_ids + { + anyhow::bail!( + "completed complexity round session checkpoint set does not match prior memory plus exact invocation journals" + ) + } + for session_id in expected_ids { + let checkpoint = current + .get(&session_id) + .expect("the exact session set was checked above"); + if !touched.contains(&session_id) { + if previous.get(&session_id) != Some(checkpoint) { + anyhow::bail!("completed complexity round changed an untouched session checkpoint") + } + continue; + } + let (title, location_id) = session_routes + .get(&session_id) + .ok_or_else(|| anyhow::anyhow!("complexity session journal has no exact route"))?; + checkpoint.validate_for(campaign, location_id, *title)?; + let expected_receipt_ids = journals + .get(&session_id) + .into_iter() + .flatten() + .map(|entry| entry.commit_receipt_id.clone()) + .collect::>(); + let expected_rejections = rejection_findings + .get(&session_id) + .cloned() + .unwrap_or_default(); + let expected_prior_digest = previous + .get(&session_id) + .map(|checkpoint| checkpoint.digest.clone()); + let expected_generation = previous + .get(&session_id) + .map_or(1, |checkpoint| checkpoint.generation.saturating_add(1)); + if checkpoint.through_world_revision != through_world_revision + || checkpoint.generation != expected_generation + || checkpoint.recent_commit_receipt_ids != expected_receipt_ids + || checkpoint.recent_rejection_findings != expected_rejections + || checkpoint.prior_checkpoint_digest != expected_prior_digest + { + anyhow::bail!( + "completed complexity round session checkpoint is not bound to its exact journal, rejection findings, and prior memory" + ) + } + } + Ok(()) +} + +fn complexity_proposal_is_committed( + campaign: &ghostlight_dungeon::domain::Campaign, + proposal: &ghostlight_dungeon::elaboration::WorldComplexityProposal, +) -> bool { + match proposal { + ghostlight_dungeon::elaboration::WorldComplexityProposal::Fission { preview, .. } => { + let child_ids = preview + .children + .iter() + .map(|child| child.id.as_str()) + .collect::>(); + let actionable = + ghostlight_dungeon::elaboration::canonical_actionable_subject_ids(campaign); + campaign + .gestalt_lineages + .get(&preview.parent_gestalt_id) + .is_some_and(|lineage| { + lineage.child_gestalt_ids + == preview + .children + .iter() + .map(|child| child.id.clone()) + .collect::>() + && lineage.partition_axis == preview.partition_axis + && lineage.partition_values == preview.child_partition_values + && lineage.residual_child_id == preview.residual_child_id + && lineage.source_revision == preview.expected_world_revision + && preview.children.iter().all(|expected| { + campaign.gestalts.get(&expected.id) == Some(expected) + && campaign.agency_profiles.get(&expected.id).is_some_and( + |profile| { + profile.parent_subject_id.as_deref() + == Some(preview.parent_gestalt_id.as_str()) + && profile.active_leaf + && profile.location_ids + == std::collections::BTreeSet::from([expected + .home_location_id + .clone()]) + && profile.simulation_eligible + == (expected.id != preview.residual_child_id) + }, + ) + && actionable.contains(&expected.id) + == (expected.id != preview.residual_child_id) + }) + && campaign + .agency_profiles + .get(&preview.parent_gestalt_id) + .is_some_and(|profile| { + !profile.active_leaf && !profile.simulation_eligible + }) + && !actionable.contains(&preview.parent_gestalt_id) + && preview + .member_child_assignments + .iter() + .all(|(member_id, child_id)| { + campaign + .gestalt_members + .get(member_id) + .is_some_and(|member| member.gestalt_id == *child_id) + }) + && campaign.gestalt_members.values().all(|member| { + member.gestalt_id != preview.parent_gestalt_id + && (!child_ids.contains(member.gestalt_id.as_str()) + || preview.member_child_assignments.contains_key(&member.id) + || member.gestalt_id == preview.residual_child_id) + }) + && campaign.civic_systems.values().all(|system| { + !system + .resident_population_ids + .contains(&preview.parent_gestalt_id) + }) + && campaign.agency_relations.values().all(|relation| { + !relation.active + || (relation.from_subject_id != preview.parent_gestalt_id + && relation.to_subject_id != preview.parent_gestalt_id) + }) + }) + } + ghostlight_dungeon::elaboration::WorldComplexityProposal::Individuate { + individuation, + .. + } => { + let member_id = ghostlight_dungeon::domain::canonical_gestalt_member_local_id( + &individuation.member.id, + ); + let actor_id = ghostlight_dungeon::domain::gestalt_member_subject_id(&member_id); + let mut expected_member = individuation.member.clone(); + expected_member.id = member_id.clone(); + expected_member.version = 1; + expected_member.last_location_id = Some(individuation.location_id.clone()); + expected_member.materialized_actor_id = Some(actor_id.clone()); + expected_member.last_relevant_revision = proposal.expected_world_revision(); + expected_member.relevance_lease_until_revision = + proposal.expected_world_revision().saturating_add(2); + let Some(gestalt) = campaign.gestalts.get(&individuation.gestalt_id) else { + return false; + }; + let overlay = + |base: &std::collections::BTreeSet, + additions: &std::collections::BTreeSet, + removals: &std::collections::BTreeSet| { + base.difference(removals) + .cloned() + .chain(additions.iter().cloned()) + .collect::>() + }; + let expected_actor = ghostlight_dungeon::domain::ActorState { + id: actor_id.clone(), + name: expected_member.name.clone(), + location_id: individuation.location_id.clone(), + capabilities: overlay( + &gestalt.shared_capabilities, + &expected_member.capability_additions, + &expected_member.capability_removals, + ), + knowledge: overlay( + &gestalt.shared_knowledge, + &expected_member.knowledge_additions, + &expected_member.knowledge_removals, + ), + equipment: expected_member.equipment.clone(), + conditions: expected_member.conditions.clone(), + obligations: expected_member.obligations.clone(), + relationships: expected_member.relationships.clone(), + goals: if expected_member.goals.is_empty() { + gestalt.goals.clone() + } else { + expected_member.goals.clone() + }, + memories: expected_member.memories.clone(), + }; + campaign.gestalt_members.get(&member_id) == Some(&expected_member) + && campaign.actors.get(&actor_id) == Some(&expected_actor) + && gestalt.version == individuation.expected_gestalt_version + && campaign + .agency_profiles + .get(&actor_id) + .is_some_and(|profile| { + profile.subject_kind == ghostlight_dungeon::domain::AgencySubjectKind::Actor + && profile.active_leaf + && profile.simulation_eligible + && profile.location_ids + == std::collections::BTreeSet::from([individuation + .location_id + .clone()]) + }) + && ghostlight_dungeon::elaboration::canonical_actionable_subject_ids(campaign) + .contains(&actor_id) + } + } } fn retain_unique_complexity_invocations( @@ -547,44 +929,87 @@ fn retain_unique_complexity_invocations( Vec<&ComplexityPreviewInvocation>, Vec, ) { - let mut retained_ordinals = BTreeMap::::new(); - for invocation in invocations { - if let ghostlight_dungeon::elaboration::WorldComplexityProposal::Individuate { - individuation, - } = &invocation.proposal - { - let canonical_id = ghostlight_dungeon::domain::canonical_gestalt_member_local_id( - &individuation.member.id, - ); - retained_ordinals - .entry(canonical_id) - .and_modify(|ordinal| *ordinal = (*ordinal).min(invocation.dispatch.ordinal)) - .or_insert(invocation.dispatch.ordinal); - } - } let mut retained = Vec::new(); let mut superseded = Vec::new(); - for invocation in invocations { - let ghostlight_dungeon::elaboration::WorldComplexityProposal::Individuate { individuation } = - &invocation.proposal - else { - retained.push(invocation); - continue; - }; - let canonical_id = - ghostlight_dungeon::domain::canonical_gestalt_member_local_id(&individuation.member.id); - let retained_ordinal = retained_ordinals[&canonical_id]; - if invocation.dispatch.ordinal == retained_ordinal { + let mut retained_subject_ids = BTreeMap::::new(); + let mut retained_person_names = BTreeMap::::new(); + let mut retained_population_names = BTreeMap::::new(); + let mut ordered = invocations.iter().collect::>(); + ordered.sort_by_key(|invocation| invocation.dispatch.ordinal); + for invocation in ordered { + let (canonical_subject_ids, public_identity_keys, population_identity_keys) = + match &invocation.proposal { + ghostlight_dungeon::elaboration::WorldComplexityProposal::Individuate { + individuation, + .. + } => ( + vec![ghostlight_dungeon::domain::gestalt_member_subject_id( + &individuation.member.id, + )], + vec![ghostlight_dungeon::resolution::public_identity_key( + &individuation.member.name, + )], + Vec::new(), + ), + ghostlight_dungeon::elaboration::WorldComplexityProposal::Fission { + preview, + .. + } => ( + preview + .children + .iter() + .map(|child| child.id.clone()) + .collect(), + Vec::new(), + preview + .children + .iter() + .filter(|child| child.id != preview.residual_child_id) + .map(|child| { + ghostlight_dungeon::resolution::public_identity_key(&child.name) + }) + .collect(), + ), + }; + let retained_ordinal = canonical_subject_ids + .iter() + .filter_map(|id| retained_subject_ids.get(id)) + .chain( + public_identity_keys + .iter() + .filter_map(|key| retained_person_names.get(key)), + ) + .chain( + population_identity_keys + .iter() + .filter_map(|key| retained_population_names.get(key)), + ) + .copied() + .min(); + if retained_ordinal.is_none() { + for id in &canonical_subject_ids { + retained_subject_ids.insert(id.clone(), invocation.dispatch.ordinal); + } + for key in &public_identity_keys { + retained_person_names.insert(key.clone(), invocation.dispatch.ordinal); + } + for key in &population_identity_keys { + retained_population_names.insert(key.clone(), invocation.dispatch.ordinal); + } retained.push(invocation); } else { superseded.push(ComplexitySupersededInvocationCheckpoint { schema: "ghostlight.complexity_superseded_invocation.v1".into(), round, dispatch: invocation.dispatch.clone(), - retained_dispatch_ordinal: retained_ordinal, - canonical_member_id: canonical_id, + retained_dispatch_ordinal: retained_ordinal.expect("duplicate has an owner"), + canonical_subject_ids, + public_identity_keys: public_identity_keys + .into_iter() + .chain(population_identity_keys) + .collect(), diagnostic: - "parallel individuation duplicated an exact local member identity; the earliest dispatch owns the proposal" + "parallel complexity proposals duplicated a canonical subject ID or normalized public identity; the earliest dispatch owns the proposal" .into(), }); } @@ -683,6 +1108,38 @@ fn titled_failure_checkpoint_paths( Ok(paths) } +fn titled_verifier_execution_failure_checkpoint_paths( + root: &std::path::Path, + index: usize, + semantic_attempt: u8, +) -> anyhow::Result> { + let prefix = format!( + "titled-elaboration-{index:02}-verifier-execution-failure-{semantic_attempt:02}-generation-" + ); + let mut generations = std::fs::read_dir(root)? + .filter_map(|entry| entry.ok()) + .filter_map(|entry| { + let name = entry.file_name(); + let name = name.to_str()?; + let generation = name + .strip_prefix(&prefix)? + .strip_suffix(".json")? + .parse::() + .ok()?; + Some((generation, entry.path())) + }) + .collect::>(); + generations.sort_by_key(|(generation, _)| *generation); + if generations + .iter() + .enumerate() + .any(|(index, (generation, _))| *generation != index as u32 + 1) + { + anyhow::bail!("titled verifier-execution failure checkpoint generations are not contiguous") + } + Ok(generations) +} + fn load_checkpoint_receipts( store: &ghostlight_dungeon::persistence::CampaignStore, hashes: &[String], @@ -701,6 +1158,649 @@ fn load_checkpoint_receipts( .collect() } +#[derive(Clone, Copy, PartialEq, Eq)] +enum ComplexityCommitValidationMode { + CurrentCampaignEffect, + HistoricalCommit, +} + +fn validate_complexity_semantic_rejection_checkpoint( + store: &ghostlight_dungeon::persistence::CampaignStore, + campaign: &ghostlight_dungeon::domain::Campaign, + round: u32, + invocation: &ComplexityPreviewInvocation, + path: &std::path::Path, +) -> anyhow::Result { + use ghostlight_dungeon::elaboration::{ + WorldComplexityProposal, WorldComplexitySemanticVerification, + validate_world_complexity_semantic_qualification_shape, + }; + + if !path.is_file() { + anyhow::bail!( + "completed complexity round references a missing semantic-rejection checkpoint: {}", + path.display() + ) + } + let rejection = read_checkpoint::(path)?; + let rejected_verdict = + serde_json::from_str::(&rejection.diagnostic)?; + if rejection.schema != "ghostlight.complexity_semantic_rejection.v2" + || rejection.round != round + || rejection.dispatch != invocation.dispatch + || rejection.parent_gestalt_id != invocation.proposal.parent_gestalt_id() + || rejection.proposal.parent_gestalt_id() != invocation.proposal.parent_gestalt_id() + || rejected_verdict.accepted() + { + anyhow::bail!("complexity semantic-rejection checkpoint is inconsistent") + } + let receipt = store + .load::( + "persona_stage_receipt.v1", + &rejection.verifier_receipt_hash, + )? + .map(|(_, receipt)| receipt) + .ok_or_else(|| { + anyhow::anyhow!("complexity semantic rejection lost its verifier receipt") + })?; + let semantic = match &rejection.proposal { + WorldComplexityProposal::Fission { qualification, .. } => &qualification.semantic, + WorldComplexityProposal::Individuate { qualification, .. } => &qualification.semantic, + }; + validate_world_complexity_semantic_qualification_shape(&rejection.proposal, semantic)?; + let expected_sources = invocation + .model_receipt_hashes + .iter() + .cloned() + .collect::>(); + let actual_sources = receipt + .source_receipt_ids + .iter() + .cloned() + .collect::>(); + let expected_error = format!( + "world-complexity semantic verifier rejected the candidate: {}", + rejection.diagnostic + ); + if receipt.storage_key() != rejection.verifier_receipt_hash + || receipt.stage != "world-complexity-semantic-verification" + || receipt.model != ghostlight_dungeon::model::MODEL_BALANCED + || receipt.validation_result == "valid" + || receipt.local_validation_error.as_deref() != Some(expected_error.as_str()) + || receipt.snapshot_binding != semantic.semantic_verification_binding + || semantic.frozen_campaign_id != campaign.id + || semantic.frozen_world_revision > campaign.revision + || rejection.proposal.expected_world_revision() != semantic.frozen_world_revision + || expected_sources.is_empty() + || expected_sources.len() != invocation.model_receipt_hashes.len() + || actual_sources.len() != receipt.source_receipt_ids.len() + || actual_sources != expected_sources + { + anyhow::bail!( + "complexity semantic rejection is not backed by its exact invalid verifier receipt" + ) + } + Ok(rejection) +} + +fn committed_complexity_fission_mutation_proof( + store: &ghostlight_dungeon::persistence::CampaignStore, + world_receipt: &ghostlight_dungeon::domain::WorldCommitReceipt, + preview: &ghostlight_dungeon::domain::GestaltFissionPreview, +) -> anyhow::Result { + let intended_effect_digest = + ghostlight_dungeon::legacy_transition::digest_serializable(preview)?; + let mut exact_batches = store + .load_all::("world_mutation_batch.v1")? + .into_iter() + .filter(|batch| { + batch.campaign_id == world_receipt.campaign_id + && batch.expected_world_revision == world_receipt.previous_revision + && batch.intended_effect_digest.as_deref() == Some(&intended_effect_digest) + }) + .collect::>(); + if exact_batches.len() != 1 { + anyhow::bail!( + "complexity fission world commit has {} exact candidate mutation batches", + exact_batches.len() + ) + } + let expected_resolution_epoch = exact_batches + .pop() + .expect("one exact fission batch was required") + .expected_resolution_epoch; + committed_compiler_mutation_proof( + store, + world_receipt, + intended_effect_digest, + "elaborate_gestalt_fission", + "fission-preview", + expected_resolution_epoch, + ) +} + +fn validate_complexity_mutation_checkpoint( + store: &ghostlight_dungeon::persistence::CampaignStore, + campaign: &ghostlight_dungeon::domain::Campaign, + round: u32, + invocation: &ComplexityPreviewInvocation, + prepared: &ComplexityPreparedMutationCheckpoint, + checkpoint: &ComplexityMutationCheckpoint, + mode: ComplexityCommitValidationMode, +) -> anyhow::Result<()> { + use ghostlight_dungeon::elaboration::WorldComplexityProposal; + + let derived_affected_subject_ids = complexity_affected_subject_ids(&prepared.proposal); + let derived_semantic_summary = complexity_session_journal_summary(&prepared.proposal)?; + if checkpoint.schema != "ghostlight.complexity_mutation_checkpoint.v1" + || checkpoint.round != round + || checkpoint.dispatch != invocation.dispatch + || prepared.schema != "ghostlight.complexity_prepared_mutation.v1" + || prepared.round != round + || prepared.dispatch != invocation.dispatch + || prepared.parent_gestalt_id != invocation.proposal.parent_gestalt_id() + || prepared.mutation_kind != invocation.proposal.mutation_kind() + || checkpoint.parent_gestalt_id != prepared.parent_gestalt_id + || checkpoint.mutation_kind != prepared.mutation_kind + || checkpoint.affected_subject_ids != derived_affected_subject_ids + || prepared.affected_subject_ids != derived_affected_subject_ids + || checkpoint.semantic_summary != derived_semantic_summary + || prepared.semantic_summary != derived_semantic_summary + || checkpoint.model_receipt_hashes != prepared.model_receipt_hashes + || !prepared + .model_receipt_hashes + .starts_with(&invocation.model_receipt_hashes) + || prepared.model_receipt_hashes.len() + != invocation.model_receipt_hashes.len().saturating_add(1) + || checkpoint.commit_receipt.schema != "ghostlight.world_commit_receipt.v1" + || checkpoint.commit_receipt.campaign_id != campaign.id + || checkpoint.commit_receipt.previous_revision != prepared.expected_revision + || checkpoint.commit_receipt.revision != prepared.expected_revision.saturating_add(1) + || checkpoint.commit_receipt.revision > campaign.revision + || checkpoint.commit_receipt.command_kind != checkpoint.mutation_kind + { + anyhow::bail!("complexity mutation checkpoint is inconsistent") + } + if mode == ComplexityCommitValidationMode::CurrentCampaignEffect + && !complexity_proposal_is_committed(campaign, &prepared.proposal) + { + anyhow::bail!( + "complexity mutation checkpoint is present but its committed proposal effect is absent from the campaign" + ) + } + + let model_receipts = load_checkpoint_receipts(store, &prepared.model_receipt_hashes)?; + let semantic = match &prepared.proposal { + WorldComplexityProposal::Fission { qualification, .. } => &qualification.semantic, + WorldComplexityProposal::Individuate { qualification, .. } => &qualification.semantic, + }; + ghostlight_dungeon::elaboration::validate_world_complexity_semantic_receipt_provenance( + &prepared.proposal, + semantic, + &model_receipts, + )?; + if semantic.frozen_campaign_id != campaign.id + || semantic.frozen_world_revision != prepared.expected_revision + || prepared.proposal.expected_world_revision() != prepared.expected_revision + { + anyhow::bail!("prepared complexity qualification is bound to another campaign revision") + } + + let canonical_receipt = store + .load::( + "world_commit_receipt.v1", + &format!( + "{}-{}", + checkpoint.commit_receipt.campaign_id, checkpoint.commit_receipt.revision + ), + )? + .map(|(_, receipt)| receipt) + .ok_or_else(|| anyhow::anyhow!("complexity mutation lacks its canonical world receipt"))?; + if canonical_receipt != checkpoint.commit_receipt { + anyhow::bail!("complexity checkpoint disagrees with its canonical world receipt") + } + + match &prepared.proposal { + WorldComplexityProposal::Fission { preview, .. } => { + committed_complexity_fission_mutation_proof(store, &canonical_receipt, preview)?; + } + WorldComplexityProposal::Individuate { individuation, .. } => { + let presence = store + .load::( + "gestalt_materialization_receipt.v1", + &format!( + "{}-{}", + canonical_receipt.campaign_id, canonical_receipt.revision + ), + )? + .map(|(_, receipt)| receipt) + .ok_or_else(|| { + anyhow::anyhow!("complexity individuation lacks its canonical presence receipt") + })?; + let member_id = ghostlight_dungeon::domain::canonical_gestalt_member_local_id( + &individuation.member.id, + ); + let actor_id = ghostlight_dungeon::domain::gestalt_member_subject_id(&member_id); + if presence.schema != "ghostlight.gestalt_materialization_receipt.v1" + || presence.campaign_id != canonical_receipt.campaign_id + || presence.previous_revision != prepared.expected_revision + || presence.revision != canonical_receipt.revision + || presence.previous_resolution_epoch.saturating_add(1) != presence.resolution_epoch + || presence.reason != "model-qualified world-complexity individuation" + || presence.committed_at != canonical_receipt.committed_at + || presence.changes.len() != 1 + || presence.changes[0].operation != "materialized" + || presence.changes[0].gestalt_id != individuation.gestalt_id + || presence.changes[0].member_id != member_id + || presence.changes[0].actor_id != actor_id + || presence.changes[0].gestalt_version != individuation.expected_gestalt_version + || presence.changes[0].member_version != 1 + { + anyhow::bail!( + "complexity individuation found a different canonical presence change" + ) + } + } + } + Ok(()) +} + +fn load_and_validate_complexity_mutation_checkpoint( + store: &ghostlight_dungeon::persistence::CampaignStore, + campaign: &ghostlight_dungeon::domain::Campaign, + round: u32, + invocation: &ComplexityPreviewInvocation, + prepared_path: &std::path::Path, + commit_path: &std::path::Path, + mode: ComplexityCommitValidationMode, +) -> anyhow::Result<( + ComplexityPreparedMutationCheckpoint, + ComplexityMutationCheckpoint, +)> { + if !prepared_path.is_file() { + anyhow::bail!( + "complexity commit references a missing prepared-mutation checkpoint: {}", + prepared_path.display() + ) + } + if !commit_path.is_file() { + anyhow::bail!( + "completed complexity round references a missing mutation checkpoint: {}", + commit_path.display() + ) + } + let prepared = read_checkpoint::(prepared_path)?; + let checkpoint = read_checkpoint::(commit_path)?; + validate_complexity_mutation_checkpoint( + store, + campaign, + round, + invocation, + &prepared, + &checkpoint, + mode, + )?; + Ok((prepared, checkpoint)) +} + +fn validate_completed_complexity_round_checkpoint( + root: &std::path::Path, + store: &ghostlight_dungeon::persistence::CampaignStore, + campaign: &ghostlight_dungeon::domain::Campaign, + checkpoint: &ComplexityRoundCheckpoint, + round: u32, +) -> anyhow::Result<()> { + if checkpoint.schema != "ghostlight.complexity_round_checkpoint.v1" + || checkpoint.round != round + || checkpoint.actionable_subjects_after + > ghostlight_dungeon::elaboration::canonical_actionable_subject_count(campaign) + { + anyhow::bail!("complexity round checkpoint is stale or malformed") + } + for path in &checkpoint.mutation_checkpoints { + if !path.is_file() { + anyhow::bail!( + "completed complexity round references a missing mutation checkpoint: {}", + path.display() + ) + } + } + for path in &checkpoint.superseded_invocation_checkpoints { + if !path.is_file() { + anyhow::bail!( + "completed complexity round references a missing supersession or rejection checkpoint: {}", + path.display() + ) + } + } + + let preview_path = root.join(format!("complexity-round-{round:03}-preview.json")); + if !preview_path.is_file() { + anyhow::bail!( + "completed complexity round has no immutable preview checkpoint: {}", + preview_path.display() + ) + } + let preview = read_checkpoint::(&preview_path)?; + if preview.schema != "ghostlight.complexity_round_preview.v1" + || preview.round != round + || preview.invocations.is_empty() + || preview.frozen_world_revision > campaign.revision + || preview.demand != checkpoint.demand_before + || preview.schedule != checkpoint.schedule + { + anyhow::bail!("completed complexity round preview is stale or malformed") + } + let previous_session_checkpoints = if round == 1 { + std::collections::BTreeMap::new() + } else { + let previous_path = root.join(format!( + "complexity-round-{:03}-checkpoint.json", + round.saturating_sub(1) + )); + let previous = read_checkpoint::(&previous_path)?; + if previous.schema != "ghostlight.complexity_round_checkpoint.v1" + || previous.round != round.saturating_sub(1) + { + anyhow::bail!("completed complexity round has no exact prior session checkpoint") + } + previous.session_checkpoints + }; + + let (retained_invocations, superseded_invocations) = + retain_unique_complexity_invocations(round, &preview.invocations); + let mut expected_superseded_paths = Vec::new(); + for superseded in superseded_invocations { + let path = root.join(format!( + "complexity-round-{round:03}-superseded-{:04}.json", + superseded.dispatch.ordinal + )); + let persisted = read_checkpoint::(&path)?; + if persisted != superseded { + anyhow::bail!("completed complexity supersession checkpoint is inconsistent") + } + expected_superseded_paths.push(path); + } + + let mut expected_mutation_paths = Vec::new(); + let mut expected_revision = preview.frozen_world_revision; + let mut expected_actionable_subjects = checkpoint.demand_before.current_actionable_subjects; + let mut session_routes = std::collections::BTreeMap::new(); + let mut session_journals = std::collections::BTreeMap::< + String, + Vec, + >::new(); + let mut session_rejection_findings = std::collections::BTreeMap::>::new(); + for invocation in retained_invocations { + let location_id = match &invocation.proposal { + ghostlight_dungeon::elaboration::WorldComplexityProposal::Fission { + qualification, + .. + } => qualification.jurisdiction_location_id.clone(), + ghostlight_dungeon::elaboration::WorldComplexityProposal::Individuate { + qualification, + .. + } => qualification.jurisdiction_location_id.clone(), + }; + let session_id = ghostlight_dungeon::elaboration::elaborator_session_id( + invocation.dispatch.title, + &location_id, + ); + session_routes.insert(session_id.clone(), (invocation.dispatch.title, location_id)); + let commit_path = root.join(format!( + "complexity-round-{round:03}-commit-{:04}.json", + invocation.dispatch.ordinal + )); + let rejection_path = root.join(format!( + "complexity-round-{round:03}-semantic-rejection-{:04}.json", + invocation.dispatch.ordinal + )); + match (commit_path.is_file(), rejection_path.is_file()) { + (true, false) => { + let prepared_path = root.join(format!( + "complexity-round-{round:03}-prepared-{:04}.json", + invocation.dispatch.ordinal + )); + let (prepared, mutation_checkpoint) = + load_and_validate_complexity_mutation_checkpoint( + store, + campaign, + round, + invocation, + &prepared_path, + &commit_path, + ComplexityCommitValidationMode::HistoricalCommit, + )?; + if prepared.expected_revision != expected_revision { + anyhow::bail!( + "completed complexity commits do not form the exact historical revision chain" + ) + } + expected_revision = expected_revision.saturating_add(1); + expected_actionable_subjects = + expected_actionable_subjects.saturating_add(match &prepared.proposal { + ghostlight_dungeon::elaboration::WorldComplexityProposal::Fission { + qualification, + .. + } => u32::from(qualification.target_actionable_gain), + ghostlight_dungeon::elaboration::WorldComplexityProposal::Individuate { + .. + } => 1, + }); + session_journals + .entry(session_id.clone()) + .or_default() + .push( + ghostlight_dungeon::elaboration::ElaboratorSessionJournalEntry { + world_revision: mutation_checkpoint.commit_receipt.revision, + commit_receipt_id: format!( + "{}-{}", + mutation_checkpoint.commit_receipt.campaign_id, + mutation_checkpoint.commit_receipt.revision + ), + mutation_kind: mutation_checkpoint.mutation_kind, + affected_subject_ids: mutation_checkpoint.affected_subject_ids, + summary: mutation_checkpoint.semantic_summary, + }, + ); + expected_mutation_paths.push(commit_path); + } + (false, true) => { + let rejection = validate_complexity_semantic_rejection_checkpoint( + store, + campaign, + round, + invocation, + &rejection_path, + )?; + session_rejection_findings + .entry(session_id) + .or_default() + .push(bounded_prompt_excerpt(&rejection.diagnostic, 800)); + expected_superseded_paths.push(rejection_path); + } + _ => anyhow::bail!( + "completed complexity invocation must have exactly one commit or semantic rejection checkpoint" + ), + } + } + if checkpoint.mutation_checkpoints != expected_mutation_paths + || checkpoint.superseded_invocation_checkpoints != expected_superseded_paths + || checkpoint.actionable_subjects_after != expected_actionable_subjects + { + anyhow::bail!( + "completed complexity round does not exactly cover its mutations, supersessions, rejections, and admitted complexity gain" + ) + } + validate_complexity_round_session_checkpoints( + campaign, + &previous_session_checkpoints, + &checkpoint.session_checkpoints, + &session_routes, + &session_journals, + &session_rejection_findings, + expected_revision, + )?; + Ok(()) +} + +fn validate_titled_semantic_failure_checkpoint( + store: &ghostlight_dungeon::persistence::CampaignStore, + campaign: &ghostlight_dungeon::domain::Campaign, + checkpoint: &TitledSemanticFailureCheckpoint, + expected_attempt: u8, + location_id: &str, + location_name: &str, + base_request: &str, + expected_verification_request: &str, + wave: &ghostlight_dungeon::elaboration::ElaborationWaveBinding, +) -> anyhow::Result<()> { + wave.validate()?; + if checkpoint.schema != "ghostlight.titled_elaboration_semantic_failure.v2" + || checkpoint.attempt != expected_attempt + || checkpoint.location_id != location_id + || checkpoint.location_name != location_name + || checkpoint.base_request != base_request + || checkpoint.verification_request != expected_verification_request + || &checkpoint.wave != wave + || checkpoint.schedule.schema != "ghostlight.elaboration_schedule_receipt.v1" + || checkpoint.schedule.dispatches.is_empty() + || checkpoint.model_receipt_hashes.is_empty() + || checkpoint.verifier_receipt_hashes.len() != 1 + || (expected_attempt == 1 + && checkpoint + .repair_request + .as_deref() + .is_none_or(str::is_empty)) + || (expected_attempt == 2 && checkpoint.repair_request.is_some()) + { + anyhow::bail!("titled semantic-failure checkpoint is malformed or stale") + } + let candidate = checkpoint.admission.valid_candidate(campaign)?; + let expected_model_receipt_hashes = checkpoint + .admission + .model_stage_receipts() + .iter() + .map(|receipt| receipt.storage_key().to_owned()) + .collect::>(); + if checkpoint.admission.target_location_id() != location_id + || checkpoint.admission.wave() != wave + || checkpoint.admission.schedule() != &checkpoint.schedule + || candidate.target_location_id != location_id + || checkpoint.model_receipt_hashes != expected_model_receipt_hashes + { + anyhow::bail!( + "titled semantic-failure checkpoint admission is not its exact derived candidate" + ) + } + let loaded_model_receipts = load_checkpoint_receipts(store, &checkpoint.model_receipt_hashes)?; + if loaded_model_receipts != checkpoint.admission.model_stage_receipts() { + anyhow::bail!("titled semantic-failure checkpoint model ancestry was substituted") + } + let verifier_receipts = load_checkpoint_receipts(store, &checkpoint.verifier_receipt_hashes)?; + let expected_verifier_binding = + ghostlight_dungeon::compiler::titled_civic_verifier_binding(campaign, &candidate)?; + let expected_sources = checkpoint + .model_receipt_hashes + .iter() + .cloned() + .collect::>(); + if verifier_receipts.iter().any(|receipt| { + let actual_sources = receipt + .source_receipt_ids + .iter() + .cloned() + .collect::>(); + receipt.stage != "destination_civic_verification" + || receipt.model != ghostlight_dungeon::model::MODEL_CAPABLE + || receipt.validation_result == "valid" + || receipt.local_validation_error.is_none() + || receipt.snapshot_binding != expected_verifier_binding + || actual_sources.len() != receipt.source_receipt_ids.len() + || actual_sources != expected_sources + }) { + anyhow::bail!( + "titled semantic-failure checkpoint lacks its exact invalid civic verifier ancestry" + ) + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn validate_titled_verifier_execution_failure_checkpoint( + store: &ghostlight_dungeon::persistence::CampaignStore, + campaign: &ghostlight_dungeon::domain::Campaign, + checkpoint: &TitledVerifierExecutionFailureCheckpoint, + expected_semantic_attempt: u8, + expected_generation: u32, + location_id: &str, + location_name: &str, + expected_verification_request: &str, +) -> anyhow::Result<()> { + if checkpoint.schema != "ghostlight.titled_verifier_execution_failure.v2" + || !matches!(expected_semantic_attempt, 1 | 2) + || checkpoint.semantic_attempt != expected_semantic_attempt + || checkpoint.generation != expected_generation + || checkpoint.location_id != location_id + || checkpoint.location_name != location_name + || checkpoint.verification_request != expected_verification_request + || checkpoint.diagnostic.trim().is_empty() + || checkpoint.attempts == 0 + || checkpoint.attempts > 3 + { + anyhow::bail!("titled verifier-execution failure checkpoint is malformed or stale") + } + let candidate = checkpoint.admission.valid_candidate(campaign)?; + let expected_model_receipt_hashes = checkpoint + .admission + .model_stage_receipts() + .iter() + .map(|receipt| receipt.storage_key().to_owned()) + .collect::>(); + if checkpoint.admission.target_location_id() != location_id + || checkpoint.model_receipt_hashes != expected_model_receipt_hashes + { + anyhow::bail!( + "titled verifier-execution failure admission is not its exact derived candidate" + ) + } + let loaded_model_receipts = load_checkpoint_receipts(store, &checkpoint.model_receipt_hashes)?; + if loaded_model_receipts != checkpoint.admission.model_stage_receipts() { + anyhow::bail!("titled verifier-execution failure model ancestry was substituted") + } + let verifier_receipts = load_checkpoint_receipts(store, &checkpoint.verifier_receipt_hashes)?; + let expected_binding = + ghostlight_dungeon::compiler::titled_civic_verifier_binding(campaign, &candidate)?; + let expected_sources = checkpoint + .model_receipt_hashes + .iter() + .cloned() + .collect::>(); + let unique_verifier_hashes = checkpoint + .verifier_receipt_hashes + .iter() + .collect::>(); + if unique_verifier_hashes.len() != checkpoint.verifier_receipt_hashes.len() + || verifier_receipts.iter().any(|receipt| { + let actual_sources = receipt + .source_receipt_ids + .iter() + .cloned() + .collect::>(); + receipt.stage != "destination_civic_verification" + || receipt.model != ghostlight_dungeon::model::MODEL_CAPABLE + || receipt.validation_result == "valid" + || receipt.local_validation_error.is_none() + || receipt.snapshot_binding != expected_binding + || actual_sources.len() != receipt.source_receipt_ids.len() + || actual_sources != expected_sources + }) + { + anyhow::bail!("titled verifier-execution failure lacks its exact failed verifier ancestry") + } + Ok(()) +} + fn latest_clock_binding_receipt( store: &ghostlight_dungeon::persistence::CampaignStore, campaign_id: uuid::Uuid, @@ -1057,14 +2157,81 @@ fn committed_elaboration_mutation_proof( store: &ghostlight_dungeon::persistence::CampaignStore, world_receipt: &ghostlight_dungeon::domain::WorldCommitReceipt, expansion: &ghostlight_dungeon::domain::RegionExpansion, +) -> anyhow::Result { + committed_compiler_mutation_proof( + store, + world_receipt, + ghostlight_dungeon::legacy_transition::digest_serializable(expansion)?, + "elaborate_locality", + "region-expansion", + None, + ) +} + +fn committed_region_mutation_proof( + store: &ghostlight_dungeon::persistence::CampaignStore, + world_receipt: &ghostlight_dungeon::domain::WorldCommitReceipt, + expansion: &ghostlight_dungeon::domain::RegionExpansion, +) -> anyhow::Result { + committed_compiler_mutation_proof( + store, + world_receipt, + ghostlight_dungeon::legacy_transition::digest_serializable(expansion)?, + "expand_region", + "region-expansion", + None, + ) +} + +fn expected_foundation_commit_checkpoint( + store: &ghostlight_dungeon::persistence::CampaignStore, + location_id: &str, + location_name: &str, + request: &str, + preview: &ghostlight_dungeon::domain::LocalityElaborationPreview, + model_receipts: &[ghostlight_dungeon::model::ModelStageReceipt], + commit_receipt: ghostlight_dungeon::domain::WorldCommitReceipt, +) -> anyhow::Result { + if commit_receipt.command_kind != "elaborate_locality" + || commit_receipt.previous_revision != preview.expected_revision + || commit_receipt.revision != preview.expected_revision.saturating_add(1) + || preview.elaboration.target_location_id != location_id + { + anyhow::bail!("foundation world receipt does not bind the prepared locality") + } + Ok(FoundationCommitCheckpoint { + schema: "ghostlight.foundation_commit_checkpoint.v1".into(), + location_id: location_id.into(), + location_name: location_name.into(), + request: request.into(), + world_revision_before: preview.expected_revision, + world_revision_after: preview.expected_revision.saturating_add(1), + model_receipt_hashes: model_receipts + .iter() + .map(|receipt| receipt.storage_key().to_owned()) + .collect(), + mutation_proof: committed_elaboration_mutation_proof( + store, + &commit_receipt, + &preview.elaboration.expansion, + )?, + commit_receipt, + }) +} + +fn committed_compiler_mutation_proof( + store: &ghostlight_dungeon::persistence::CampaignStore, + world_receipt: &ghostlight_dungeon::domain::WorldCommitReceipt, + intended_effect_digest: String, + expected_command_kind: &str, + source_receipt_kind: &str, + expected_resolution_epoch: Option, ) -> anyhow::Result { use ghostlight_dungeon::transition::{ MutationAuthorityEnvelope, WorldMutationBatch, WorldMutationReceipt, mutation_digest, validate_batch_structure, }; - let intended_effect_digest = - ghostlight_dungeon::legacy_transition::digest_serializable(expansion)?; let mut batches = store .load_all::("world_mutation_batch.v1")? .into_iter() @@ -1076,7 +2243,7 @@ fn committed_elaboration_mutation_proof( .collect::>(); if batches.len() != 1 { anyhow::bail!( - "titled world commit has {} exact candidate mutation batches", + "world commit has {} exact candidate mutation batches", batches.len() ) } @@ -1092,7 +2259,7 @@ fn committed_elaboration_mutation_proof( .collect::>(); if authorities.len() != 1 { anyhow::bail!( - "titled world commit has {} exact mutation authorities", + "world commit has {} exact mutation authorities", authorities.len() ) } @@ -1114,7 +2281,7 @@ fn committed_elaboration_mutation_proof( .collect::>(); if mutation_receipts.len() != 1 { anyhow::bail!( - "titled world commit has {} exact mutation receipts", + "world commit has {} exact mutation receipts", mutation_receipts.len() ) } @@ -1127,18 +2294,18 @@ fn committed_elaboration_mutation_proof( .map(mutation_digest) .collect::>>()?; let expected_source_receipt_id = format!( - "region-expansion:{}", + "{source_receipt_kind}:{}", intended_effect_digest.trim_start_matches("sha256:") ); if world_receipt.schema != "ghostlight.world_commit_receipt.v1" - || world_receipt.command_kind != "elaborate_locality" + || world_receipt.command_kind != expected_command_kind || world_receipt.previous_revision.saturating_add(1) != world_receipt.revision || batch.schema != "ghostlight.world_mutation_batch.v1" || batch.source_receipt_id != expected_source_receipt_id - || batch.expected_resolution_epoch.is_some() + || batch.expected_resolution_epoch != expected_resolution_epoch || batch.mutations.is_empty() || authority.schema != "ghostlight.mutation_authority_envelope.v1" - || authority.resolution_epoch.is_some() + || authority.resolution_epoch != expected_resolution_epoch || authority.source_subject.is_some() || authority.procedure != ghostlight_dungeon::transition::MutationProcedure::CompilerAdmission @@ -1149,7 +2316,7 @@ fn committed_elaboration_mutation_proof( || mutation_receipt.committed_at != world_receipt.committed_at || mutation_receipt.mutation_digests != expected_mutation_digests { - anyhow::bail!("titled mutation proof does not bind one compiler admission commit") + anyhow::bail!("mutation proof does not bind one compiler admission commit") } Ok(TitledMutationProof { @@ -1175,7 +2342,8 @@ async fn main() -> anyhow::Result<()> { WorldComplexityProposal, WorldScaleIntent, admit_world_elaboration_wave, canonical_actionable_subject_count, compact_elaborator_session, derive_world_elaboration_demand, dispatch_elaboration_wave, elaborator_session_id, - finalize_world_elaboration, rebase_world_complexity_proposal, resume_elaboration_wave, + finalize_world_elaboration, qualify_world_complexity_proposal_semantics, + rebase_world_complexity_proposal, resume_elaboration_wave, world_complexity_parent_binding, world_elaboration_wave_binding, }, kernel::{CommandResult, WorldKernel}, @@ -1458,26 +2626,43 @@ async fn main() -> anyhow::Result<()> { } let mut expanded_jurisdictions = Vec::new(); for (index, request) in region_requests.iter().enumerate() { + let compile_request = strategic_region_request(request); let preview_path = root.join(format!("world-region-{:02}-preview.json", index + 1)); let commit_path = root.join(format!("world-region-{:02}-checkpoint.json", index + 1)); if resume && commit_path.is_file() { let checkpoint: WorldRegionCommitCheckpoint = read_checkpoint(&commit_path)?; - if checkpoint.schema != "ghostlight.world_region_expansion_checkpoint.v1" + let prepared: WorldRegionPreviewCheckpoint = read_checkpoint(&preview_path)?; + if checkpoint.schema != "ghostlight.world_region_expansion_checkpoint.v2" || checkpoint.request != *request || checkpoint.origin_location_id != origin_location_id || checkpoint.commit_receipt.command_kind != "expand_region" + || prepared.schema != "ghostlight.world_region_expansion_preview.v1" + || prepared.request != *request + || prepared.preview.expansion.origin_location_id != origin_location_id { anyhow::bail!("world-region checkpoint differs at index {}", index + 1) } - if !campaign - .locations - .contains_key(&checkpoint.jurisdiction_location_id) - { - anyhow::bail!( - "world-region checkpoint is not committed: {}", - checkpoint.jurisdiction_location_id - ) - } + validate_strategic_region_expansion_shape(&prepared.preview.expansion)?; + let expected_jurisdiction_id = prepared + .preview + .expansion + .civic_system + .as_ref() + .map(|civic| civic.jurisdiction_location_id.clone()) + .or_else(|| { + prepared + .preview + .expansion + .locations + .first() + .map(|location| location.id.clone()) + }) + .ok_or_else(|| anyhow::anyhow!("world-region expansion created no locality"))?; + let expected_model_receipt_hashes = prepared + .model_receipts + .iter() + .map(|receipt| receipt.storage_key().to_owned()) + .collect::>(); let canonical_receipt = store .load::( "world_commit_receipt.v1", @@ -1485,8 +2670,23 @@ async fn main() -> anyhow::Result<()> { )? .map(|(_, receipt)| receipt) .ok_or_else(|| anyhow::anyhow!("world-region commit receipt is missing"))?; - if canonical_receipt != checkpoint.commit_receipt { - anyhow::bail!("world-region checkpoint differs from canonical receipt") + let mutation_proof = committed_region_mutation_proof( + &store, + &canonical_receipt, + &prepared.preview.expansion, + )?; + if canonical_receipt != checkpoint.commit_receipt + || checkpoint.commit_receipt.previous_revision + != prepared.preview.expected_revision + || checkpoint.jurisdiction_location_id != expected_jurisdiction_id + || checkpoint.model_receipt_hashes != expected_model_receipt_hashes + || checkpoint.mutation_proof != mutation_proof + || load_checkpoint_receipts(&store, &checkpoint.model_receipt_hashes)? + != prepared.model_receipts + { + anyhow::bail!( + "world-region checkpoint differs from its exact canonical expansion proof" + ) } expanded_jurisdictions.push(checkpoint.jurisdiction_location_id); continue; @@ -1516,7 +2716,7 @@ async fn main() -> anyhow::Result<()> { checkpoint } else { let (preview, receipts) = compiler - .compile_destination(&campaign, &origin_location_id, request) + .compile_destination(&campaign, &origin_location_id, &compile_request) .await?; let ghostlight_dungeon::domain::DestinationCompilationPreview::RegionExpansion( preview, @@ -1540,6 +2740,7 @@ async fn main() -> anyhow::Result<()> { model_receipts: receipts, .. } = preview_checkpoint; + validate_strategic_region_expansion_shape(&preview.expansion)?; if !preview.gaps.is_empty() { publish_immutable_checkpoint( &root.join(format!( @@ -1589,40 +2790,29 @@ async fn main() -> anyhow::Result<()> { }; campaign = expanded; receipt - } else if campaign.revision > preview.expected_revision - && preview - .expansion - .locations - .iter() - .all(|location| campaign.locations.contains_key(&location.id)) - && preview - .expansion - .institutions - .iter() - .all(|institution| campaign.institutions.contains_key(&institution.id)) - && preview - .expansion - .populations - .iter() - .all(|population| campaign.gestalts.contains_key(&population.id)) - { + } else if campaign.revision > preview.expected_revision { store .load::( "world_commit_receipt.v1", &format!("{}-{}", campaign.id, preview.expected_revision + 1), )? .map(|(_, receipt)| receipt) - .filter(|receipt| receipt.command_kind == "expand_region") + .filter(|receipt| { + receipt.command_kind == "expand_region" + && receipt.previous_revision == preview.expected_revision + }) .ok_or_else(|| { anyhow::anyhow!("committed world region lacks its canonical receipt") })? } else { anyhow::bail!("world-region preview no longer matches canonical campaign state") }; + let mutation_proof = + committed_region_mutation_proof(&store, &receipt, &preview.expansion)?; publish_immutable_checkpoint( &commit_path, &WorldRegionCommitCheckpoint { - schema: "ghostlight.world_region_expansion_checkpoint.v1".into(), + schema: "ghostlight.world_region_expansion_checkpoint.v2".into(), request: request.clone(), origin_location_id: origin_location_id.clone(), jurisdiction_location_id: jurisdiction_id.clone(), @@ -1631,6 +2821,7 @@ async fn main() -> anyhow::Result<()> { .iter() .map(|receipt| receipt.storage_key().to_owned()) .collect(), + mutation_proof, }, )?; expanded_jurisdictions.push(jurisdiction_id); @@ -1780,6 +2971,8 @@ async fn main() -> anyhow::Result<()> { let foundation_request = strategic_locality_request(&location_name, location_id, &pressure); let preview_path = root.join(format!("elaboration-{:02}-preview.json", index + 1)); + let foundation_commit_path = + root.join(format!("elaboration-{:02}-commit.json", index + 1)); let (elaborated, receipts) = if resume && preview_path.is_file() { let checkpoint: FoundationCheckpoint = read_checkpoint(&preview_path)?; let (expected_revision, expected_civic) = match &checkpoint.preview { @@ -1808,19 +3001,101 @@ async fn main() -> anyhow::Result<()> { if checkpoint.location_id != *location_id || checkpoint.location_name != location_name || checkpoint.request != foundation_request - || campaign.revision <= expected_revision - || campaign - .civic_systems - .get(location_id) - .is_none_or(|current| { - !civic_manifest_preserves(&campaign, current, &expected_civic) - }) + || campaign.revision < expected_revision { anyhow::bail!( - "foundation checkpoint for {location_id} is not committed in the resumed campaign" + "foundation checkpoint for {location_id} does not bind the resumed campaign" ) } - (campaign.clone(), checkpoint.model_receipts) + if campaign.revision == expected_revision { + validate_strategic_foundation_civic_shape(&expected_civic)?; + let command = match &checkpoint.preview { + ghostlight_dungeon::domain::DestinationCompilationPreview::LocalityElaboration( + preview, + ) => WorldCommand::ElaborateLocality { + expected_revision: preview.expected_revision, + elaboration: preview.elaboration.clone(), + evidence_receipts: preview.evidence_receipts.clone(), + canon_candidates: preview.canon_candidates.clone(), + model_stage_receipts: checkpoint.model_receipts.clone(), + }, + _ => unreachable!("foundation checkpoint target was validated above"), + }; + let CommandResult::Committed { + campaign: elaborated, + receipt, + } = kernel.command(command).await? + else { + anyhow::bail!("prepared foundation did not commit") + }; + let locality_preview = match &checkpoint.preview { + ghostlight_dungeon::domain::DestinationCompilationPreview::LocalityElaboration(preview) => preview, + _ => unreachable!("foundation checkpoint target was validated above"), + }; + let expected_commit = expected_foundation_commit_checkpoint( + &store, + location_id, + &location_name, + &foundation_request, + locality_preview, + &checkpoint.model_receipts, + receipt, + )?; + if foundation_commit_path.is_file() { + if read_checkpoint::(&foundation_commit_path)? + != expected_commit + { + anyhow::bail!( + "foundation completion checkpoint for {location_id} differs from its exact canonical proof" + ) + } + } else { + publish_immutable_checkpoint(&foundation_commit_path, &expected_commit)?; + } + (elaborated, checkpoint.model_receipts) + } else { + let locality_preview = match &checkpoint.preview { + ghostlight_dungeon::domain::DestinationCompilationPreview::LocalityElaboration(preview) => preview, + _ => unreachable!("foundation checkpoint target was validated above"), + }; + let receipt = store + .load::( + "world_commit_receipt.v1", + &format!("{}-{}", campaign.id, expected_revision.saturating_add(1)), + )? + .map(|(_, receipt)| receipt) + .ok_or_else(|| { + anyhow::anyhow!( + "foundation checkpoint for {location_id} lacks its canonical world receipt" + ) + })?; + let expected_commit = expected_foundation_commit_checkpoint( + &store, + location_id, + &location_name, + &foundation_request, + locality_preview, + &checkpoint.model_receipts, + receipt, + )?; + if foundation_commit_path.is_file() { + if read_checkpoint::(&foundation_commit_path)? + != expected_commit + { + anyhow::bail!( + "foundation completion checkpoint for {location_id} differs from its exact canonical proof" + ) + } + } else { + publish_immutable_checkpoint(&foundation_commit_path, &expected_commit)?; + } + if load_checkpoint_receipts(&store, &expected_commit.model_receipt_hashes)? + != checkpoint.model_receipts + { + anyhow::bail!("foundation checkpoint model ancestry was substituted") + } + (campaign.clone(), checkpoint.model_receipts) + } } else { let compilation = compiler .compile_destination(&campaign, location_id, &foundation_request) @@ -1870,12 +3145,24 @@ async fn main() -> anyhow::Result<()> { let command = match &preview { ghostlight_dungeon::domain::DestinationCompilationPreview::LocalityElaboration( preview, - ) => WorldCommand::ElaborateLocality { - expected_revision: preview.expected_revision, - elaboration: preview.elaboration.clone(), - evidence_receipts: preview.evidence_receipts.clone(), - canon_candidates: preview.canon_candidates.clone(), - model_stage_receipts: receipts.clone(), + ) => { + validate_strategic_foundation_civic_shape( + preview + .elaboration + .expansion + .civic_system + .as_ref() + .ok_or_else(|| anyhow::anyhow!( + "strategic locality foundation has no civic manifest" + ))?, + )?; + WorldCommand::ElaborateLocality { + expected_revision: preview.expected_revision, + elaboration: preview.elaboration.clone(), + evidence_receipts: preview.evidence_receipts.clone(), + canon_candidates: preview.canon_candidates.clone(), + model_stage_receipts: receipts.clone(), + } }, ghostlight_dungeon::domain::DestinationCompilationPreview::RegionExpansion(_) => { anyhow::bail!( @@ -1883,24 +3170,38 @@ async fn main() -> anyhow::Result<()> { ) } }; - std::fs::write( + publish_immutable_checkpoint( &preview_path, - serde_json::to_vec_pretty(&serde_json::json!({ + &serde_json::json!({ "location_id":location_id, "location_name":location_name, "request":foundation_request, "preview":&preview, "model_receipts":&receipts, - }))?, + }), )?; let committed = kernel.command(command).await?; let CommandResult::Committed { campaign: elaborated, - .. + receipt, } = committed else { anyhow::bail!("strategic locality elaboration did not commit") }; + let locality_preview = match &preview { + ghostlight_dungeon::domain::DestinationCompilationPreview::LocalityElaboration(preview) => preview, + _ => unreachable!("foundation command target was validated above"), + }; + let expected_commit = expected_foundation_commit_checkpoint( + &store, + location_id, + &location_name, + &foundation_request, + locality_preview, + &receipts, + receipt, + )?; + publish_immutable_checkpoint(&foundation_commit_path, &expected_commit)?; (elaborated, receipts) }; campaign = elaborated; @@ -1909,136 +3210,116 @@ async fn main() -> anyhow::Result<()> { let titled_commit_path = root.join(format!("titled-elaboration-{:02}-commit.json", index + 1)); if resume && titled_preview_path.is_file() { - let titled: TitledPreviewCheckpoint = read_checkpoint(&titled_preview_path)?; - let candidate = titled.candidate.as_ref().ok_or_else(|| { - anyhow::anyhow!("committed titled checkpoint has no candidate") + let titled: TitledReadyCheckpoint = read_checkpoint(&titled_preview_path)?; + let admission = titled.finalized.admission(); + let candidate = admission.candidate().ok_or_else(|| { + anyhow::anyhow!("prepared titled checkpoint has no candidate") })?; let candidate_civic = candidate.expansion.civic_system.as_ref().ok_or_else(|| { - anyhow::anyhow!("committed titled checkpoint has no civic candidate") + anyhow::anyhow!("prepared titled checkpoint has no civic candidate") })?; - if titled.schema != "ghostlight.titled_elaboration_preview.v1" + if titled.schema != "ghostlight.titled_elaboration_ready.v1" || titled.location_id != *location_id || titled.location_name != location_name || titled.request != strategic_titled_locality_request(&location_name, location_id, &pressure) || candidate.target_location_id != *location_id - || titled.candidate_diagnostic.is_some() - || !titled.rejections.is_empty() - || titled.accepted_operations.len() != titled.schedule.dispatches.len() + || admission.target_location_id() != *location_id { anyhow::bail!( - "committed titled checkpoint for {location_id} is internally inconsistent" + "prepared titled checkpoint for {location_id} is internally inconsistent" ) } - let civic = campaign.civic_systems.get(location_id).ok_or_else(|| { - anyhow::anyhow!("resumed campaign lacks civic system for {location_id}") - })?; - if !candidate_civic.semantic_verification_receipt_id.is_empty() - || !civic_manifest_preserves(&campaign, civic, candidate_civic) - { - anyhow::bail!( - "titled preview for {location_id} is not the civic system committed in CultCache" - ) - } - let existing_commit_checkpoint = titled_commit_path - .is_file() - .then(|| read_checkpoint::(&titled_commit_path)) - .transpose()?; - let verifier_receipt_hash = existing_commit_checkpoint - .as_ref() - .map(|checkpoint| checkpoint.verifier_receipt_hash.as_str()) - .unwrap_or(&civic.semantic_verification_receipt_id); - let verifier_receipt = store - .load::( - "persona_stage_receipt.v1", - verifier_receipt_hash, - )? - .map(|(_, receipt)| receipt) - .ok_or_else(|| { - anyhow::anyhow!( - "committed titled verifier receipt is missing for {location_id}" - ) - })?; - let proposal_receipts = - load_checkpoint_receipts(&store, &titled.model_receipt_hashes)?; - let expected_foundation_revision = match &read_checkpoint::( - &preview_path, - )? - .preview - { - ghostlight_dungeon::domain::DestinationCompilationPreview::LocalityElaboration( - preview, - ) => preview.expected_revision, - _ => unreachable!("foundation checkpoint was validated above"), - }; - let world_revision_before = expected_foundation_revision.saturating_add(1); - let world_revision_after = expected_foundation_revision.saturating_add(2); - let persisted_commit_receipt = store - .load::( - "world_commit_receipt.v1", - &format!("{}-{world_revision_after}", campaign.id), - )? - .map(|(_, receipt)| receipt) - .ok_or_else(|| { - anyhow::anyhow!( - "titled world commit receipt is missing for {location_id} revision {world_revision_after}" - ) - })?; + let verifier_receipt = titled.finalized.semantic_verifier_receipt().clone(); + let world_revision_before = admission.expected_revision(); + let world_revision_after = world_revision_before.saturating_add(1); let finalized_expansion = finalized_titled_expansion(candidate, verifier_receipt.storage_key())?; + let (titled_elaborated, persisted_commit_receipt) = if campaign.revision + == world_revision_before + { + let CommandResult::Committed { + campaign: advanced, + receipt, + } = kernel + .commit_elaboration(titled.finalized.clone()) + .await + .map_err(anyhow::Error::new)? + else { + anyhow::bail!("prepared titled elaboration did not commit") + }; + (advanced, receipt) + } else if campaign.revision == world_revision_after { + let civic = campaign.civic_systems.get(location_id).ok_or_else(|| { + anyhow::anyhow!("resumed campaign lacks civic system for {location_id}") + })?; + if !civic_manifest_preserves(&campaign, civic, candidate_civic) + || civic.semantic_verification_receipt_id != verifier_receipt.storage_key() + { + anyhow::bail!( + "prepared titled elaboration for {location_id} is not the canonical committed civic system" + ) + } + let receipt = store + .load::( + "world_commit_receipt.v1", + &format!("{}-{world_revision_after}", campaign.id), + )? + .map(|(_, receipt)| receipt) + .ok_or_else(|| { + anyhow::anyhow!( + "titled world commit receipt is missing for {location_id} revision {world_revision_after}" + ) + })?; + (campaign.clone(), receipt) + } else { + anyhow::bail!( + "prepared titled elaboration for {location_id} cannot recover from world revision {}", + campaign.revision + ) + }; let mutation_proof = committed_elaboration_mutation_proof( &store, &persisted_commit_receipt, &finalized_expansion, )?; - let commit_checkpoint = if let Some(checkpoint) = existing_commit_checkpoint { - checkpoint - } else { - let checkpoint = TitledCommitCheckpoint { - schema: "ghostlight.titled_elaboration_commit.v1".into(), - location_id: location_id.clone(), - location_name: location_name.clone(), - world_revision_before, - world_revision_after, - wave: titled.wave.clone(), - schedule: titled.schedule.clone(), - admission_digest: None, - verifier_receipt_hash: verifier_receipt.storage_key().into(), - model_receipt_hashes: titled.model_receipt_hashes.clone(), - commit_receipt: persisted_commit_receipt.clone(), - mutation_proof: mutation_proof.clone(), - legacy_inferred: true, - }; - publish_immutable_checkpoint(&titled_commit_path, &checkpoint)?; - checkpoint + let admitted_model_receipt_hashes = admission + .model_stage_receipts() + .iter() + .map(|receipt| receipt.storage_key().to_owned()) + .collect::>(); + let expected_commit_checkpoint = TitledCommitCheckpoint { + schema: "ghostlight.titled_elaboration_commit.v1".into(), + location_id: location_id.clone(), + location_name: location_name.clone(), + world_revision_before, + world_revision_after, + wave: admission.wave().clone(), + schedule: admission.schedule().clone(), + admission_digest: Some(admission.digest().into()), + verifier_receipt_hash: verifier_receipt.storage_key().into(), + model_receipt_hashes: admitted_model_receipt_hashes.clone(), + commit_receipt: persisted_commit_receipt.clone(), + mutation_proof: mutation_proof.clone(), + legacy_inferred: false, }; - if commit_checkpoint.schema != "ghostlight.titled_elaboration_commit.v1" - || commit_checkpoint.location_id != *location_id - || commit_checkpoint.location_name != location_name - || commit_checkpoint.world_revision_before != world_revision_before - || commit_checkpoint.world_revision_after != world_revision_after - || commit_checkpoint.wave != titled.wave - || commit_checkpoint.schedule != titled.schedule - || commit_checkpoint.verifier_receipt_hash != verifier_receipt.storage_key() - || commit_checkpoint.model_receipt_hashes != titled.model_receipt_hashes - || commit_checkpoint.commit_receipt != persisted_commit_receipt - || commit_checkpoint.mutation_proof != mutation_proof - || commit_checkpoint.commit_receipt.command_kind != "elaborate_locality" - || (!commit_checkpoint.legacy_inferred - && commit_checkpoint - .admission_digest - .as_deref() - .is_none_or(str::is_empty)) - { + let commit_checkpoint = if titled_commit_path.is_file() { + read_checkpoint::(&titled_commit_path)? + } else { + publish_immutable_checkpoint(&titled_commit_path, &expected_commit_checkpoint)?; + expected_commit_checkpoint.clone() + }; + if commit_checkpoint != expected_commit_checkpoint { anyhow::bail!( "titled completion checkpoint for {location_id} is not bound to its canonical commit" ) } titled_scheduler = ElaborationScheduler::from_state( &titled_profile, - titled.schedule.final_state.clone(), + admission.schedule().final_state.clone(), )?; + campaign = titled_elaborated; elaboration_reports.push(serde_json::json!({ "location_id":location_id, "location_name":location_name, @@ -2046,175 +3327,547 @@ async fn main() -> anyhow::Result<()> { "preview_path":preview_path, "titled_preview_path":titled_preview_path, "titled_commit_path":titled_commit_path, - "titled_model_receipts":proposal_receipts, + "titled_model_receipts":admission.model_stage_receipts(), "titled_semantic_verifier_receipt":verifier_receipt, "model_receipts":receipts, "resumed_committed_checkpoint":true, - "original_wave":titled.wave, + "original_wave":admission.wave(), "original_resume_source":titled.resumed_from, "original_retried_dispatch_ordinals":titled.retried_dispatch_ordinals, })); continue; } let titled_wave = world_elaboration_wave_binding(&campaign, location_id)?; - let titled_worker = Arc::new(ModelWorldElaborationWorker::new( - model.clone(), - Arc::new(campaign.clone()), - location_id.clone(), - strategic_titled_locality_request(&location_name, location_id, &pressure), - )?); + let base_titled_request = + strategic_titled_locality_request(&location_name, location_id, &pressure); + let semantic_failure_one_path = root.join(format!( + "titled-elaboration-{:02}-semantic-failure-01.json", + index + 1 + )); + let semantic_failure_two_path = root.join(format!( + "titled-elaboration-{:02}-semantic-failure-02.json", + index + 1 + )); + if resume && semantic_failure_two_path.is_file() { + if !semantic_failure_one_path.is_file() { + anyhow::bail!( + "terminal titled semantic failure for {location_id} lost its first-attempt authority" + ) + } + let first: TitledSemanticFailureCheckpoint = + read_checkpoint(&semantic_failure_one_path)?; + validate_titled_semantic_failure_checkpoint( + &store, + &campaign, + &first, + 1, + location_id, + &location_name, + &base_titled_request, + &base_titled_request, + &titled_wave, + )?; + let repair_request = first.repair_request.as_deref().ok_or_else(|| { + anyhow::anyhow!("first titled semantic failure has no repair request") + })?; + let terminal: TitledSemanticFailureCheckpoint = + read_checkpoint(&semantic_failure_two_path)?; + validate_titled_semantic_failure_checkpoint( + &store, + &campaign, + &terminal, + 2, + location_id, + &location_name, + &base_titled_request, + repair_request, + &titled_wave, + )?; + anyhow::bail!( + "titled semantic repair was already rejected for {location_id}: {}", + terminal.diagnostic + ) + } + let semantic_resume = if resume && semantic_failure_one_path.is_file() { + let checkpoint: TitledSemanticFailureCheckpoint = + read_checkpoint(&semantic_failure_one_path)?; + validate_titled_semantic_failure_checkpoint( + &store, + &campaign, + &checkpoint, + 1, + location_id, + &location_name, + &base_titled_request, + &base_titled_request, + &titled_wave, + )?; + Some(checkpoint) + } else { + None + }; let original_failure_path = root.join(format!( "titled-elaboration-{:02}-terminal-failure.json", index + 1 )); let failure_checkpoint_paths = titled_failure_checkpoint_paths(&root, index + 1)?; - let checkpoint_path = failure_checkpoint_paths.last().cloned(); - let next_failure_path = if checkpoint_path.is_some() { + let mut checkpoint_path = failure_checkpoint_paths.last().cloned(); + let mut checkpoint_for_resume = checkpoint_path + .as_ref() + .map(|path| read_checkpoint::(path)) + .transpose()?; + if let Some(semantic) = semantic_resume.as_ref() + && checkpoint_for_resume.as_ref().is_some_and(|checkpoint| { + checkpoint.request != semantic.repair_request.as_deref().unwrap_or_default() + }) + { + checkpoint_path = None; + checkpoint_for_resume = None; + } + if let (Some(semantic), Some(checkpoint)) = + (semantic_resume.as_ref(), checkpoint_for_resume.as_ref()) + && checkpoint.semantic_retry_diagnostic.as_deref() + != Some(semantic.diagnostic.as_str()) + { + anyhow::bail!("titled repair failure checkpoint lost its semantic retry diagnostic") + } + if checkpoint_for_resume.is_none() + && let Some(semantic) = semantic_resume.as_ref() + { + titled_scheduler = ElaborationScheduler::from_state( + &titled_profile, + semantic.schedule.final_state.clone(), + )?; + } + let active_titled_request = checkpoint_for_resume + .as_ref() + .map(|checkpoint| checkpoint.request.clone()) + .or_else(|| { + semantic_resume + .as_ref() + .and_then(|checkpoint| checkpoint.repair_request.clone()) + }) + .unwrap_or_else(|| base_titled_request.clone()); + let initial_semantic_retry_diagnostic = semantic_resume + .as_ref() + .map(|checkpoint| checkpoint.diagnostic.clone()); + let mut verification_request = active_titled_request.clone(); + let verifier_semantic_attempt = if semantic_resume.is_some() { 2 } else { 1 }; + let verifier_execution_paths = titled_verifier_execution_failure_checkpoint_paths( + &root, + index + 1, + verifier_semantic_attempt, + )?; + let verifier_execution_resume = if resume { + let mut latest = None; + let mut prior_admission_digest = None::; + let mut prior_verifier_hashes = std::collections::BTreeSet::::new(); + for (generation, path) in &verifier_execution_paths { + let checkpoint = + read_checkpoint::(path)?; + validate_titled_verifier_execution_failure_checkpoint( + &store, + &campaign, + &checkpoint, + verifier_semantic_attempt, + *generation, + location_id, + &location_name, + &verification_request, + )?; + let verifier_hashes = checkpoint + .verifier_receipt_hashes + .iter() + .cloned() + .collect::>(); + if let Some(prior_digest) = prior_admission_digest.as_deref() + && (checkpoint.admission.digest() != prior_digest + || !verifier_hashes.is_superset(&prior_verifier_hashes)) + { + anyhow::bail!( + "titled verifier-execution recovery generations changed admission or lost failed verifier ancestry" + ) + } + prior_admission_digest = Some(checkpoint.admission.digest().into()); + prior_verifier_hashes = verifier_hashes; + latest = Some((path.clone(), checkpoint)); + } + latest + } else { + None + }; + if let Some((path, _)) = verifier_execution_resume.as_ref() { + checkpoint_path = Some(path.clone()); + } + let next_failure_path = if failure_checkpoint_paths.is_empty() { + original_failure_path + } else { root.join(format!( "titled-elaboration-{:02}-resume-{:02}-terminal-failure.json", index + 1, failure_checkpoint_paths.len() )) - } else { - original_failure_path }; let mut retried_dispatch_ordinals = Vec::new(); - let titled_result = if resume { - if let Some(checkpoint_path) = checkpoint_path.as_ref() { - let checkpoint: TitledFailureCheckpoint = read_checkpoint(checkpoint_path)?; - if checkpoint.location_id != *location_id - || checkpoint.location_name != location_name - || checkpoint.request != titled_worker.task_request() - || checkpoint.wave.as_ref() != Some(&titled_wave) - { - anyhow::bail!( - "titled failure checkpoint for {location_id} does not bind the current frozen wave" - ) - } - retried_dispatch_ordinals = checkpoint - .invocation_failures - .iter() - .filter_map(|failure| { - failure.dispatch.as_ref().map(|dispatch| dispatch.ordinal) - }) - .collect(); - let scheduler_state = checkpoint - .schedule - .as_ref() - .ok_or_else(|| anyhow::anyhow!("resume checkpoint has no schedule"))? - .final_state - .clone(); - titled_scheduler = - ElaborationScheduler::from_state(&titled_profile, scheduler_state)?; - resume_elaboration_wave( - rehydrate_titled_failure(checkpoint, &store)?, - titled_parallelism, - titled_worker.clone(), + let (mut proposal_receipts, mut admission, mut verifier_execution_receipt_hashes) = + if let Some((_, checkpoint)) = verifier_execution_resume { + titled_scheduler = ElaborationScheduler::from_state( + &titled_profile, + checkpoint.admission.schedule().final_state.clone(), + )?; + let proposal_receipts = checkpoint.admission.model_stage_receipts().to_vec(); + ( + proposal_receipts, + checkpoint.admission, + checkpoint.verifier_receipt_hashes, ) - .await } else { - dispatch_elaboration_wave( - &mut titled_scheduler, - titled_wave, - &titled_eligible, - titled_invocation_budget, - titled_parallelism, - titled_worker.clone(), - ) - .await - } - } else { - dispatch_elaboration_wave( - &mut titled_scheduler, - titled_wave, - &titled_eligible, - titled_invocation_budget, - titled_parallelism, - titled_worker.clone(), - ) - .await - }; - let titled_run = match titled_result { - Ok(run) => run, - Err(failure) => { - let receipts = failure - .completed_invocations - .iter() - .flat_map(|invocation| invocation.model_stage_receipts.iter()) - .chain( - failure + let titled_worker = Arc::new(ModelWorldElaborationWorker::new( + model.clone(), + Arc::new(campaign.clone()), + location_id.clone(), + active_titled_request, + )?); + let titled_result = if resume { + if let Some(checkpoint) = checkpoint_for_resume { + if checkpoint.location_id != *location_id + || checkpoint.location_name != location_name + || checkpoint.request != titled_worker.task_request() + || checkpoint.wave.as_ref() != Some(&titled_wave) + { + anyhow::bail!( + "titled failure checkpoint for {location_id} does not bind the current frozen wave" + ) + } + retried_dispatch_ordinals = checkpoint .invocation_failures .iter() - .flat_map(|failure| failure.model_stage_receipts.iter()), + .filter_map(|failure| { + failure.dispatch.as_ref().map(|dispatch| dispatch.ordinal) + }) + .collect(); + let scheduler_state = checkpoint + .schedule + .as_ref() + .ok_or_else(|| { + anyhow::anyhow!("resume checkpoint has no schedule") + })? + .final_state + .clone(); + titled_scheduler = + ElaborationScheduler::from_state(&titled_profile, scheduler_state)?; + resume_elaboration_wave( + rehydrate_titled_failure(checkpoint, &store)?, + titled_parallelism, + titled_worker.clone(), + ) + .await + } else { + dispatch_elaboration_wave( + &mut titled_scheduler, + titled_wave.clone(), + &titled_eligible, + titled_invocation_budget, + titled_parallelism, + titled_worker.clone(), + ) + .await + } + } else { + dispatch_elaboration_wave( + &mut titled_scheduler, + titled_wave.clone(), + &titled_eligible, + titled_invocation_budget, + titled_parallelism, + titled_worker.clone(), ) + .await + }; + let titled_run = match titled_result { + Ok(run) => run, + Err(failure) => { + let receipts = failure + .completed_invocations + .iter() + .flat_map(|invocation| invocation.model_stage_receipts.iter()) + .chain( + failure + .invocation_failures + .iter() + .flat_map(|failure| failure.model_stage_receipts.iter()), + ) + .cloned() + .collect::>(); + if !receipts.is_empty() { + store.persist_model_stage_receipts(&receipts)?; + } + let failure_path = next_failure_path; + publish_immutable_checkpoint( + &failure_path, + &serde_json::json!({ + "schema":"ghostlight.titled_elaboration_failure.v1", + "location_id":location_id, + "location_name":location_name, + "request":titled_worker.task_request(), + "wave":failure.wave, + "schedule":failure.schedule, + "completed_invocations":failure.completed_invocations.iter().map(|invocation|serde_json::json!({ + "dispatch":invocation.dispatch, + "proposal":invocation.proposal, + "model_receipt_hashes":invocation.model_stage_receipts.iter().map(|receipt|receipt.storage_key()).collect::>(), + })).collect::>(), + "invocation_failures":failure.invocation_failures.iter().map(|failure|serde_json::json!({ + "dispatch":failure.dispatch, + "diagnostic":failure.diagnostic, + "model_receipt_hashes":failure.model_stage_receipts.iter().map(|receipt|receipt.storage_key()).collect::>(), + })).collect::>(), + "semantic_retry_diagnostic":initial_semantic_retry_diagnostic, + }), + )?; + anyhow::bail!( + "titled elaboration wave failed for {location_id}; exact receipt at {}", + failure_path.display() + ); + } + }; + let proposal_receipts = titled_run + .invocations() + .iter() + .flat_map(|invocation| invocation.model_stage_receipts.iter()) .cloned() .collect::>(); - if !receipts.is_empty() { - store.persist_model_stage_receipts(&receipts)?; - } - let failure_path = next_failure_path; - publish_immutable_checkpoint( - &failure_path, - &serde_json::json!({ - "schema":"ghostlight.titled_elaboration_failure.v1", - "location_id":location_id, - "location_name":location_name, - "request":titled_worker.task_request(), - "wave":failure.wave, - "schedule":failure.schedule, - "completed_invocations":failure.completed_invocations.iter().map(|invocation|serde_json::json!({ - "dispatch":invocation.dispatch, - "proposal":invocation.proposal, - "model_receipt_hashes":invocation.model_stage_receipts.iter().map(|receipt|receipt.storage_key()).collect::>(), - })).collect::>(), - "invocation_failures":failure.invocation_failures.iter().map(|failure|serde_json::json!({ - "dispatch":failure.dispatch, - "diagnostic":failure.diagnostic, - "model_receipt_hashes":failure.model_stage_receipts.iter().map(|receipt|receipt.storage_key()).collect::>(), - })).collect::>(), - }), - )?; + store.persist_model_stage_receipts(&proposal_receipts)?; + let admission = + admit_world_elaboration_wave(&campaign, location_id, titled_run)?; + (proposal_receipts, admission, Vec::new()) + }; + let mut semantic_retry_diagnostic = initial_semantic_retry_diagnostic; + let mut verifier_execution_failures = 0_u8; + let (candidate, verifier_receipt) = loop { + let candidate = admission.candidate().cloned().ok_or_else(|| { + anyhow::anyhow!( + "titled elaboration produced no candidate: {}", + admission.candidate_diagnostic().unwrap_or("no diagnostic") + ) + })?; + if let Some(diagnostic) = admission.candidate_diagnostic() { anyhow::bail!( - "titled elaboration wave failed for {location_id}; exact receipt at {}", - failure_path.display() + "titled elaboration candidate requires reconciliation: {diagnostic}" ); } + let causal_receipt_ids = admission + .model_stage_receipts() + .iter() + .map(|receipt| receipt.storage_key().to_owned()) + .collect::>(); + match compiler + .verify_titled_locality_elaboration( + &campaign, + &verification_request, + description, + &candidate, + &causal_receipt_ids, + ) + .await + { + Ok(receipt) => break (candidate, receipt), + Err(error) => { + let failure = error.downcast_ref::< + ghostlight_dungeon::compiler::CivicElaborationVerificationFailure, + >(); + if let Some(failure) = failure { + store.persist_model_stage_receipts(&failure.model_receipts)?; + verifier_execution_receipt_hashes.extend( + failure + .model_receipts + .iter() + .map(|receipt| receipt.storage_key().to_owned()), + ); + } + if failure + .and_then(|failure| failure.semantic_diagnostic.as_ref()) + .is_none() + { + verifier_execution_failures = + verifier_execution_failures.saturating_add(1); + if failure.is_some() && verifier_execution_failures < 3 { + continue; + } + verifier_execution_receipt_hashes.sort(); + verifier_execution_receipt_hashes.dedup(); + let diagnostic = failure + .map(|failure| failure.message.clone()) + .unwrap_or_else(|| error.to_string()); + let semantic_attempt = if semantic_retry_diagnostic.is_some() { + 2 + } else { + 1 + }; + let prior_failures = + titled_verifier_execution_failure_checkpoint_paths( + &root, + index + 1, + semantic_attempt, + )?; + let generation = prior_failures + .last() + .map_or(1, |(generation, _)| generation.saturating_add(1)); + let failure_path = root.join(format!( + "titled-elaboration-{:02}-verifier-execution-failure-{semantic_attempt:02}-generation-{generation:03}.json", + index + 1, + )); + publish_immutable_checkpoint( + &failure_path, + &TitledVerifierExecutionFailureCheckpoint { + schema: "ghostlight.titled_verifier_execution_failure.v2" + .into(), + semantic_attempt, + generation, + location_id: location_id.clone(), + location_name: location_name.clone(), + verification_request: verification_request.clone(), + diagnostic: diagnostic.clone(), + attempts: verifier_execution_failures, + admission: admission.clone(), + model_receipt_hashes: admission + .model_stage_receipts() + .iter() + .map(|receipt| receipt.storage_key().to_owned()) + .collect(), + verifier_receipt_hashes: verifier_execution_receipt_hashes + .clone(), + }, + )?; + anyhow::bail!( + "titled civic verifier execution failed without rejecting the candidate for {location_id}; exact retry authority at {}: {diagnostic}", + failure_path.display() + ) + } + let diagnostic = failure + .and_then(|failure| failure.semantic_diagnostic.clone()) + .expect("semantic rejection was established above"); + let repair_request = strategic_titled_repair_request( + &location_name, + location_id, + &pressure, + &diagnostic, + ); + let semantic_failure = TitledSemanticFailureCheckpoint { + schema: "ghostlight.titled_elaboration_semantic_failure.v2".into(), + attempt: if semantic_retry_diagnostic.is_some() { + 2 + } else { + 1 + }, + location_id: location_id.clone(), + location_name: location_name.clone(), + base_request: base_titled_request.clone(), + verification_request: verification_request.clone(), + repair_request: semantic_retry_diagnostic + .is_none() + .then_some(repair_request.clone()), + diagnostic: diagnostic.clone(), + admission: admission.clone(), + wave: admission.wave().clone(), + schedule: admission.schedule().clone(), + model_receipt_hashes: admission + .model_stage_receipts() + .iter() + .map(|receipt| receipt.storage_key().to_owned()) + .collect(), + verifier_receipt_hashes: failure + .into_iter() + .flat_map(|failure| failure.model_receipts.iter()) + .map(|receipt| receipt.storage_key().to_owned()) + .collect(), + }; + let semantic_failure_path = if semantic_retry_diagnostic.is_some() { + &semantic_failure_two_path + } else { + &semantic_failure_one_path + }; + publish_immutable_checkpoint(semantic_failure_path, &semantic_failure)?; + if semantic_retry_diagnostic.is_some() { + return Err(error); + } + semantic_retry_diagnostic = Some(diagnostic.clone()); + let repair_worker = Arc::new(ModelWorldElaborationWorker::new( + model.clone(), + Arc::new(campaign.clone()), + location_id.clone(), + repair_request.clone(), + )?); + let repair_result = dispatch_elaboration_wave( + &mut titled_scheduler, + titled_wave.clone(), + &titled_eligible, + titled_invocation_budget, + titled_parallelism, + repair_worker, + ) + .await; + let repair_run = match repair_result { + Ok(run) => run, + Err(failure) => { + let receipts = failure + .completed_invocations + .iter() + .flat_map(|invocation| invocation.model_stage_receipts.iter()) + .chain( + failure.invocation_failures.iter().flat_map(|failure| { + failure.model_stage_receipts.iter() + }), + ) + .cloned() + .collect::>(); + if !receipts.is_empty() { + store.persist_model_stage_receipts(&receipts)?; + } + publish_immutable_checkpoint( + &next_failure_path, + &serde_json::json!({ + "schema":"ghostlight.titled_elaboration_failure.v1", + "location_id":location_id, + "location_name":location_name, + "request":repair_request, + "wave":failure.wave, + "schedule":failure.schedule, + "completed_invocations":failure.completed_invocations.iter().map(|invocation|serde_json::json!({ + "dispatch":invocation.dispatch, + "proposal":invocation.proposal, + "model_receipt_hashes":invocation.model_stage_receipts.iter().map(|receipt|receipt.storage_key()).collect::>(), + })).collect::>(), + "invocation_failures":failure.invocation_failures.iter().map(|failure|serde_json::json!({ + "dispatch":failure.dispatch, + "diagnostic":failure.diagnostic, + "model_receipt_hashes":failure.model_stage_receipts.iter().map(|receipt|receipt.storage_key()).collect::>(), + })).collect::>(), + "semantic_retry_diagnostic":semantic_retry_diagnostic, + }), + )?; + anyhow::bail!( + "titled semantic retry wave failed for {location_id}; exact receipt at {}", + next_failure_path.display() + ) + } + }; + let repair_receipts = repair_run + .invocations() + .iter() + .flat_map(|invocation| invocation.model_stage_receipts.iter()) + .cloned() + .collect::>(); + store.persist_model_stage_receipts(&repair_receipts)?; + proposal_receipts.extend(repair_receipts); + verification_request = repair_request; + admission = + admit_world_elaboration_wave(&campaign, location_id, repair_run)?; + verifier_execution_failures = 0; + verifier_execution_receipt_hashes.clear(); + } + } }; - let proposal_receipts = titled_run - .invocations() - .iter() - .flat_map(|invocation| invocation.model_stage_receipts.iter()) - .cloned() - .collect::>(); - store.persist_model_stage_receipts(&proposal_receipts)?; - let admission = admit_world_elaboration_wave(&campaign, location_id, titled_run)?; - std::fs::write( - &titled_preview_path, - serde_json::to_vec_pretty(&serde_json::json!({ - "schema":"ghostlight.titled_elaboration_preview.v1", - "location_id":location_id, - "location_name":location_name, - "request":titled_worker.task_request(), - "wave":admission.wave(), - "schedule":admission.schedule(), - "accepted_operations":admission.accepted_operations(), - "rejections":admission.rejections(), - "candidate":admission.candidate(), - "candidate_diagnostic":admission.candidate_diagnostic(), - "model_receipt_hashes":admission.model_stage_receipts().iter().map(|receipt|receipt.storage_key()).collect::>(), - "resumed_from":checkpoint_path, - "retried_dispatch_ordinals":retried_dispatch_ordinals, - }))?, - )?; - let candidate = admission.candidate().cloned().ok_or_else(|| { - anyhow::anyhow!( - "titled elaboration produced no candidate: {}", - admission.candidate_diagnostic().unwrap_or("no diagnostic") - ) - })?; - if let Some(diagnostic) = admission.candidate_diagnostic() { - anyhow::bail!("titled elaboration candidate requires reconciliation: {diagnostic}"); - } let admission_digest = admission.digest().to_owned(); let admitted_wave = admission.wave().clone(); let admitted_schedule = admission.schedule().clone(); @@ -2223,36 +3876,24 @@ async fn main() -> anyhow::Result<()> { .iter() .map(|receipt| receipt.storage_key().to_owned()) .collect::>(); - let causal_receipt_ids = admission - .model_stage_receipts() - .iter() - .map(|receipt| receipt.storage_key().to_owned()) - .collect::>(); - let verifier_receipt = match compiler - .verify_titled_locality_elaboration( - &campaign, - titled_worker.task_request(), - description, - &candidate, - &causal_receipt_ids, - ) - .await - { - Ok(receipt) => receipt, - Err(error) => { - if let Some(failure) = error.downcast_ref::< - ghostlight_dungeon::compiler::CivicElaborationVerificationFailure, - >() { - store.persist_model_stage_receipts(&failure.model_receipts)?; - } - return Err(error); - } - }; store.persist_model_stage_receipts(std::slice::from_ref(&verifier_receipt))?; let finalized_expansion = finalized_titled_expansion(&candidate, verifier_receipt.storage_key())?; let finalized = finalize_world_elaboration(&campaign, admission, verifier_receipt.clone())?; + publish_immutable_checkpoint( + &titled_preview_path, + &TitledReadyCheckpoint { + schema: "ghostlight.titled_elaboration_ready.v1".into(), + location_id: location_id.clone(), + location_name: location_name.clone(), + request: base_titled_request.clone(), + finalized: finalized.clone(), + resumed_from: checkpoint_path, + retried_dispatch_ordinals, + semantic_retry_diagnostic, + }, + )?; let CommandResult::Committed { campaign: titled_elaborated, receipt: titled_commit_receipt, @@ -2333,6 +3974,7 @@ async fn main() -> anyhow::Result<()> { let mut complexity_scheduler = ElaborationScheduler::new(&complexity_profile)?; let mut session_checkpoints = BTreeMap::::new(); let mut completed_complexity_rounds = 0_u32; + let mut last_completed_complexity_count = None; if resume { for round in 1..=maximum_complexity_rounds as u32 { let path = root.join(format!("complexity-round-{round:03}-checkpoint.json")); @@ -2340,13 +3982,13 @@ async fn main() -> anyhow::Result<()> { break; } let checkpoint: ComplexityRoundCheckpoint = read_checkpoint(&path)?; - if checkpoint.schema != "ghostlight.complexity_round_checkpoint.v1" - || checkpoint.round != round - || checkpoint.actionable_subjects_after - > canonical_actionable_subject_count(&campaign) - { - anyhow::bail!("complexity round checkpoint is stale or malformed") - } + validate_completed_complexity_round_checkpoint( + &root, + &store, + &campaign, + &checkpoint, + round, + )?; complexity_scheduler = ElaborationScheduler::from_state( &complexity_profile, checkpoint.schedule.final_state.clone(), @@ -2362,6 +4004,7 @@ async fn main() -> anyhow::Result<()> { } session_checkpoints = checkpoint.session_checkpoints.clone(); completed_complexity_rounds = round; + last_completed_complexity_count = Some(checkpoint.actionable_subjects_after); complexity_reports.push(serde_json::to_value(checkpoint)?); } } @@ -2455,6 +4098,7 @@ async fn main() -> anyhow::Result<()> { first_dispatch_ordinal, parents.clone(), parent_jurisdiction_ids, + demand.actionable_subject_deficit, session_checkpoints.clone(), )?); let result = if let Some(path) = latest_failure.as_ref() { @@ -2617,103 +4261,339 @@ async fn main() -> anyhow::Result<()> { let mut session_routes = BTreeMap::::new(); let mut invocation_sessions = BTreeMap::::new(); for invocation in &retained_invocations { - let profile = campaign - .agency_profiles - .get(invocation.proposal.parent_gestalt_id()) - .ok_or_else(|| anyhow::anyhow!("complexity parent has no agency profile"))?; - let location_id = complexity_realm_for_profile(&campaign, profile, &demand) - .ok_or_else(|| { - anyhow::anyhow!("complexity parent has no demanded realm jurisdiction") - })?; + let location_id = match &invocation.proposal { + WorldComplexityProposal::Fission { qualification, .. } => { + qualification.jurisdiction_location_id.clone() + } + WorldComplexityProposal::Individuate { qualification, .. } => { + qualification.jurisdiction_location_id.clone() + } + }; let session_id = elaborator_session_id(invocation.dispatch.title, &location_id); invocation_sessions.insert(invocation.dispatch.ordinal, session_id.clone()); session_routes.insert(session_id, (invocation.dispatch.title, location_id)); } let mut journals = BTreeMap::>::new(); + let mut recent_rejection_findings = BTreeMap::>::new(); + let mut accepted_round_semantic_deltas = Vec::::new(); for invocation in &retained_invocations { let commit_path = root.join(format!( "complexity-round-{round:03}-commit-{:04}.json", invocation.dispatch.ordinal )); + let semantic_rejection_path = root.join(format!( + "complexity-round-{round:03}-semantic-rejection-{:04}.json", + invocation.dispatch.ordinal + )); + let prepared_path = root.join(format!( + "complexity-round-{round:03}-prepared-{:04}.json", + invocation.dispatch.ordinal + )); + if semantic_rejection_path.is_file() { + let rejection = validate_complexity_semantic_rejection_checkpoint( + &store, + &campaign, + round, + invocation, + &semantic_rejection_path, + )?; + let session_id = invocation_sessions + .get(&invocation.dispatch.ordinal) + .ok_or_else(|| { + anyhow::anyhow!("complexity rejection has no session route") + })?; + recent_rejection_findings + .entry(session_id.clone()) + .or_default() + .push(bounded_prompt_excerpt(&rejection.diagnostic, 800)); + superseded_paths.push(semantic_rejection_path); + continue; + } let checkpoint = if commit_path.is_file() { read_checkpoint::(&commit_path)? } else { - let proposal = rebase_world_complexity_proposal( - &invocation.parent_binding, - &campaign, - invocation.proposal.clone(), - )?; - let parent_gestalt_id = proposal.parent_gestalt_id().to_owned(); - let mutation_kind = proposal.mutation_kind().to_owned(); - let affected_subject_ids = match &proposal { - WorldComplexityProposal::Fission { preview } => { - std::iter::once(preview.parent_gestalt_id.clone()) - .chain(preview.children.iter().map(|child| child.id.clone())) - .collect::>() + let prepared = if prepared_path.is_file() { + let prepared = read_checkpoint::( + &prepared_path, + )?; + let derived_affected_subject_ids = + complexity_affected_subject_ids(&prepared.proposal); + let derived_semantic_summary = + complexity_session_journal_summary(&prepared.proposal)?; + if prepared.schema != "ghostlight.complexity_prepared_mutation.v1" + || prepared.round != round + || prepared.dispatch != invocation.dispatch + || prepared.parent_gestalt_id != invocation.proposal.parent_gestalt_id() + || prepared.mutation_kind != invocation.proposal.mutation_kind() + || !prepared + .model_receipt_hashes + .starts_with(&invocation.model_receipt_hashes) + || prepared.model_receipt_hashes.len() + != invocation.model_receipt_hashes.len().saturating_add(1) + || prepared.affected_subject_ids != derived_affected_subject_ids + || prepared.semantic_summary != derived_semantic_summary + { + anyhow::bail!("complexity prepared-mutation checkpoint is inconsistent") } - WorldComplexityProposal::Individuate { individuation } => vec![ - individuation.gestalt_id.clone(), - ghostlight_dungeon::domain::gestalt_member_subject_id( - &individuation.member.id, - ), - ], + prepared + } else { + let proposal = rebase_world_complexity_proposal( + &invocation.parent_binding, + &campaign, + invocation.proposal.clone(), + )?; + let semantic_summary = complexity_session_journal_summary(&proposal)?; + let parent_gestalt_id = proposal.parent_gestalt_id().to_owned(); + let mutation_kind = proposal.mutation_kind().to_owned(); + let source_model_receipts = + load_checkpoint_receipts(&store, &invocation.model_receipt_hashes)?; + let source_receipt_ids = source_model_receipts + .iter() + .map(|receipt| receipt.storage_key().to_owned()) + .collect::>(); + let prior_world_sessions = + session_checkpoints.values().cloned().collect::>(); + let semantic_context = serde_json::json!({ + "assigned_title":invocation.dispatch.title, + "all_prior_round_title_jurisdiction_sessions":&prior_world_sessions, + "accepted_earlier_this_round":&accepted_round_semantic_deltas, + }); + let (proposal, semantic_verdict, semantic_receipt) = + qualify_world_complexity_proposal_semantics( + model.as_ref(), + &campaign, + &semantic_context, + proposal, + source_receipt_ids, + ) + .await?; + store.persist_model_stage_receipts(std::slice::from_ref( + &semantic_receipt, + ))?; + if !semantic_verdict.accepted() { + let rejection = ComplexitySemanticRejectionCheckpoint { + schema: "ghostlight.complexity_semantic_rejection.v2".into(), + round, + dispatch: invocation.dispatch.clone(), + parent_gestalt_id, + proposal, + verifier_receipt_hash: semantic_receipt.storage_key().into(), + diagnostic: complexity_semantic_rejection_diagnostic( + &semantic_verdict, + )?, + }; + publish_immutable_checkpoint(&semantic_rejection_path, &rejection)?; + let session_id = invocation_sessions + .get(&invocation.dispatch.ordinal) + .ok_or_else(|| { + anyhow::anyhow!("complexity rejection has no session route") + })?; + recent_rejection_findings + .entry(session_id.clone()) + .or_default() + .push(bounded_prompt_excerpt(&rejection.diagnostic, 800)); + superseded_paths.push(semantic_rejection_path); + continue; + } + let affected_subject_ids = complexity_affected_subject_ids(&proposal); + let mut model_receipts = source_model_receipts; + model_receipts.push(semantic_receipt); + let committed_model_receipt_hashes = model_receipts + .iter() + .map(|receipt| receipt.storage_key().to_owned()) + .collect::>(); + let prepared = ComplexityPreparedMutationCheckpoint { + schema: "ghostlight.complexity_prepared_mutation.v1".into(), + round, + dispatch: invocation.dispatch.clone(), + expected_revision: campaign.revision, + proposal, + parent_gestalt_id, + mutation_kind, + affected_subject_ids, + model_receipt_hashes: committed_model_receipt_hashes, + semantic_summary, + }; + publish_immutable_checkpoint(&prepared_path, &prepared)?; + prepared }; let model_receipts = - load_checkpoint_receipts(&store, &invocation.model_receipt_hashes)?; - let committed = match proposal { - WorldComplexityProposal::Fission { preview } => { - kernel - .command(WorldCommand::FissionGestalt { - expected_revision: campaign.revision, - preview, - evidence_receipts: Vec::new(), - model_stage_receipts: model_receipts, - }) - .await? + load_checkpoint_receipts(&store, &prepared.model_receipt_hashes)?; + let semantic = match &prepared.proposal { + WorldComplexityProposal::Fission { qualification, .. } => { + &qualification.semantic } - WorldComplexityProposal::Individuate { individuation } => { - if model_receipts.is_empty() { - anyhow::bail!("complexity individuation has no model receipt") - } - kernel - .command(WorldCommand::IndividuateGestaltMember { - expected_revision: campaign.revision, - individuation, - }) - .await? + WorldComplexityProposal::Individuate { qualification, .. } => { + &qualification.semantic } }; - let CommandResult::Committed { - campaign: advanced, - receipt, - } = committed - else { - anyhow::bail!("complexity mutation did not commit") + ghostlight_dungeon::elaboration::validate_world_complexity_semantic_receipt_provenance( + &prepared.proposal, + semantic, + &model_receipts, + )?; + if semantic.frozen_campaign_id != campaign.id + || semantic.frozen_world_revision != prepared.expected_revision + || prepared.proposal.expected_world_revision() != prepared.expected_revision + { + anyhow::bail!( + "prepared complexity qualification is bound to another campaign revision" + ) + } + let (advanced, receipt) = if campaign.revision == prepared.expected_revision { + let committed = match prepared.proposal.clone() { + WorldComplexityProposal::Fission { + preview, + qualification, + } => { + kernel + .command(WorldCommand::ElaborateGestaltFission { + expected_revision: campaign.revision, + preview, + qualification, + model_stage_receipts: model_receipts, + }) + .await? + } + WorldComplexityProposal::Individuate { + individuation, + qualification, + } => { + kernel + .command(WorldCommand::ElaborateGestaltIndividuation { + expected_revision: campaign.revision, + individuation, + qualification, + model_stage_receipts: model_receipts, + }) + .await? + } + }; + let CommandResult::Committed { + campaign: advanced, + receipt, + } = committed + else { + anyhow::bail!("complexity mutation did not commit") + }; + (advanced, receipt) + } else if campaign.revision == prepared.expected_revision.saturating_add(1) + && complexity_proposal_is_committed(&campaign, &prepared.proposal) + { + let receipt = store + .load::( + "world_commit_receipt.v1", + &format!("{}-{}", campaign.id, campaign.revision), + )? + .map(|(_, receipt)| receipt) + .ok_or_else(|| { + anyhow::anyhow!( + "prepared complexity mutation lacks its canonical world receipt" + ) + })?; + if receipt.schema != "ghostlight.world_commit_receipt.v1" + || receipt.campaign_id != campaign.id + || receipt.previous_revision != prepared.expected_revision + || receipt.revision != campaign.revision + || receipt.command_kind != prepared.mutation_kind + { + anyhow::bail!( + "prepared complexity mutation found a different canonical world receipt" + ) + } + match &prepared.proposal { + WorldComplexityProposal::Fission { preview, .. } => { + committed_compiler_mutation_proof( + &store, + &receipt, + ghostlight_dungeon::legacy_transition::digest_serializable( + preview, + )?, + "elaborate_gestalt_fission", + "fission-preview", + Some( + campaign + .resolution_policy + .resolution_epoch + .saturating_sub(1), + ), + )?; + } + WorldComplexityProposal::Individuate { individuation, .. } => { + let presence = store + .load::( + "gestalt_materialization_receipt.v1", + &format!("{}-{}", campaign.id, campaign.revision), + )? + .map(|(_, receipt)| receipt) + .ok_or_else(|| { + anyhow::anyhow!( + "prepared individuation lacks its canonical presence receipt" + ) + })?; + let member_id = + ghostlight_dungeon::domain::canonical_gestalt_member_local_id( + &individuation.member.id, + ); + let actor_id = + ghostlight_dungeon::domain::gestalt_member_subject_id( + &member_id, + ); + if presence.schema + != "ghostlight.gestalt_materialization_receipt.v1" + || presence.campaign_id != campaign.id + || presence.previous_revision != prepared.expected_revision + || presence.revision != campaign.revision + || presence.reason + != "model-qualified world-complexity individuation" + || presence.changes.len() != 1 + || presence.changes[0].operation != "materialized" + || presence.changes[0].gestalt_id != individuation.gestalt_id + || presence.changes[0].member_id != member_id + || presence.changes[0].actor_id != actor_id + || presence.changes[0].gestalt_version + != individuation.expected_gestalt_version + || presence.changes[0].member_version != 1 + { + anyhow::bail!( + "prepared individuation found a different canonical presence change" + ) + } + } + } + (campaign.clone(), receipt) + } else { + anyhow::bail!( + "prepared complexity mutation cannot recover from world revision {}", + campaign.revision + ) }; let checkpoint = ComplexityMutationCheckpoint { schema: "ghostlight.complexity_mutation_checkpoint.v1".into(), round, dispatch: invocation.dispatch.clone(), - parent_gestalt_id, - mutation_kind, - affected_subject_ids, - model_receipt_hashes: invocation.model_receipt_hashes.clone(), + parent_gestalt_id: prepared.parent_gestalt_id, + mutation_kind: prepared.mutation_kind, + affected_subject_ids: prepared.affected_subject_ids, + model_receipt_hashes: prepared.model_receipt_hashes, + semantic_summary: prepared.semantic_summary, commit_receipt: receipt, }; publish_immutable_checkpoint(&commit_path, &checkpoint)?; campaign = advanced; checkpoint }; - if checkpoint.schema != "ghostlight.complexity_mutation_checkpoint.v1" - || checkpoint.round != round - || checkpoint.dispatch != invocation.dispatch - || checkpoint.parent_gestalt_id != invocation.proposal.parent_gestalt_id() - || checkpoint.model_receipt_hashes != invocation.model_receipt_hashes - || checkpoint.mutation_kind != invocation.proposal.mutation_kind() - || checkpoint.commit_receipt.command_kind != checkpoint.mutation_kind - { - anyhow::bail!("complexity mutation checkpoint is inconsistent") - } + let prepared_authority = + read_checkpoint::(&prepared_path)?; + validate_complexity_mutation_checkpoint( + &store, + &campaign, + round, + invocation, + &prepared_authority, + &checkpoint, + ComplexityCommitValidationMode::CurrentCampaignEffect, + )?; let session_id = invocation_sessions .get(&checkpoint.dispatch.ordinal) .ok_or_else(|| anyhow::anyhow!("complexity invocation has no session route"))?; @@ -2727,17 +4607,24 @@ async fn main() -> anyhow::Result<()> { ), mutation_kind: checkpoint.mutation_kind.clone(), affected_subject_ids: checkpoint.affected_subject_ids.clone(), - summary: complexity_session_journal_summary( - &checkpoint.mutation_kind, - &checkpoint.parent_gestalt_id, - ), + summary: checkpoint.semantic_summary.clone(), }, ); + accepted_round_semantic_deltas.push(checkpoint.semantic_summary.clone()); mutation_paths.push(commit_path); } let compaction_limit = Arc::new(tokio::sync::Semaphore::new(complexity_parallelism)); let mut compactions = tokio::task::JoinSet::new(); - for (session_id, journal) in journals { + let sessions_to_compact = journals + .keys() + .chain(recent_rejection_findings.keys()) + .cloned() + .collect::>(); + for session_id in sessions_to_compact { + let journal = journals.remove(&session_id).unwrap_or_default(); + let rejection_findings = recent_rejection_findings + .remove(&session_id) + .unwrap_or_default(); let (title, location_id) = session_routes .get(&session_id) .ok_or_else(|| anyhow::anyhow!("complexity journal has no session route"))?; @@ -2752,9 +4639,26 @@ async fn main() -> anyhow::Result<()> { "complexity-round-{round:03}-session-{}-{session_suffix}.json", title.display_name().to_ascii_lowercase(), )); + let previous = session_checkpoints.get(&session_id).cloned(); if path.is_file() { let checkpoint = read_checkpoint::(&path)?; checkpoint.validate_for(&campaign, location_id, *title)?; + let expected_commit_receipt_ids = journal + .iter() + .map(|entry| entry.commit_receipt_id.clone()) + .collect::>(); + if checkpoint.through_world_revision != campaign.revision + || checkpoint.recent_commit_receipt_ids != expected_commit_receipt_ids + || checkpoint.recent_rejection_findings != rejection_findings + || checkpoint.prior_checkpoint_digest + != previous + .as_ref() + .map(|checkpoint| checkpoint.digest.clone()) + { + anyhow::bail!( + "complexity session checkpoint is not bound to its exact admitted journal and rejection findings" + ) + } session_checkpoints.insert(session_id, checkpoint); continue; } @@ -2762,7 +4666,6 @@ async fn main() -> anyhow::Result<()> { let campaign = Arc::new(campaign.clone()); let title = *title; let location_id = location_id.clone(); - let previous = session_checkpoints.get(&session_id).cloned(); let compaction_limit = compaction_limit.clone(); compactions.spawn(async move { let _permit = compaction_limit @@ -2777,7 +4680,7 @@ async fn main() -> anyhow::Result<()> { &session_id, previous.as_ref(), &journal, - Vec::new(), + rejection_findings, ) .await; Ok::<_, anyhow::Error>((session_id, title, location_id, path, campaign, result)) @@ -2828,21 +4731,23 @@ async fn main() -> anyhow::Result<()> { }; let path = root.join(format!("complexity-round-{round:03}-checkpoint.json")); publish_immutable_checkpoint(&path, &checkpoint)?; + last_completed_complexity_count = Some(actionable_after); complexity_reports.push(serde_json::to_value(&checkpoint)?); } - let final_count = canonical_actionable_subject_count(&campaign); + let final_count = last_completed_complexity_count + .unwrap_or_else(|| canonical_actionable_subject_count(&campaign)); let final_demand = derive_world_elaboration_demand( u16::from(campaign.resolution_policy.active_cell_budget), final_count, &scale_intent, realm_weights, )?; - if final_demand.actionable_subject_deficit != 0 { - anyhow::bail!( - "world complexity stopped at {final_count} of {} admitted subjects after {maximum_complexity_rounds} rounds", - final_demand.target_actionable_subjects - ) - } + validate_exact_world_scale_count( + final_count, + final_demand.target_actionable_subjects, + maximum_complexity_rounds, + )?; + validate_qualified_horizontal_spread(&campaign, &initial_location_ids)?; if let Some(metadata) = world_compile .as_mut() .and_then(serde_json::Value::as_object_mut) @@ -3562,12 +5467,153 @@ fn strategic_world_compiler( ) } +fn strategic_region_request(request: &str) -> String { + bounded_strategic_request( + "Create one jurisdiction: exactly 4 resident populations, 6 governing institutions, 3+ connected internal places, homes in 3+ places, and one mixed/cross-border population. Give opposed interests executable means. Preserve every Region constraint. Region: ".into(), + request, + ) +} + +fn expansion_location_is_within( + expansion: &ghostlight_dungeon::domain::RegionExpansion, + location_id: &str, + jurisdiction_id: &str, +) -> bool { + let locations = expansion + .locations + .iter() + .map(|location| (location.id.as_str(), location)) + .collect::>(); + let mut current = Some(location_id); + for _ in 0..=locations.len() { + let Some(id) = current else { + return false; + }; + if id == jurisdiction_id { + return true; + } + current = locations + .get(id) + .and_then(|location| location.container_id.as_deref()); + } + false +} + +fn validate_strategic_region_expansion_shape( + expansion: &ghostlight_dungeon::domain::RegionExpansion, +) -> anyhow::Result<()> { + let civic = expansion + .civic_system + .as_ref() + .ok_or_else(|| anyhow::anyhow!("strategic region has no civic apparatus"))?; + if civic.resident_population_ids.len() != 4 + || civic.governing_institution_ids.len() != 6 + || expansion.populations.len() != 4 + || expansion.institutions.len() != 6 + { + anyhow::bail!( + "strategic region requires exactly four resident populations and six governing institutions before foundation; candidate supplied {}/{} civic IDs and {}/{} records", + civic.resident_population_ids.len(), + civic.governing_institution_ids.len(), + expansion.populations.len(), + expansion.institutions.len() + ) + } + let admitted_locations = expansion + .locations + .iter() + .filter(|location| { + expansion_location_is_within(expansion, &location.id, &civic.jurisdiction_location_id) + }) + .map(|location| location.id.as_str()) + .collect::>(); + let resident_homes = expansion + .populations + .iter() + .filter(|population| civic.resident_population_ids.contains(&population.id)) + .map(|population| population.home_location_id.as_str()) + .collect::>(); + if admitted_locations.len() < 3 + || resident_homes.len() < 3 + || !resident_homes.is_subset(&admitted_locations) + { + anyhow::bail!( + "strategic region needs at least three admitted locations and resident homes inside its jurisdiction; candidate supplied {} locations and {} resident homes", + admitted_locations.len(), + resident_homes.len() + ) + } + Ok(()) +} + +fn validate_qualified_horizontal_spread( + campaign: &ghostlight_dungeon::domain::Campaign, + jurisdiction_ids: &[String], +) -> anyhow::Result<()> { + let actionable = ghostlight_dungeon::elaboration::canonical_actionable_subject_ids(campaign); + for jurisdiction_id in jurisdiction_ids { + let admitted = + ghostlight_dungeon::elaboration::locations_in_jurisdiction(campaign, jurisdiction_id) + .into_iter() + .collect::>(); + let mut counts = std::collections::BTreeMap::::new(); + for subject_id in &actionable { + let Some(profile) = campaign.agency_profiles.get(subject_id) else { + continue; + }; + let homes = profile + .location_ids + .intersection(&admitted) + .cloned() + .collect::>(); + if homes.len() == 1 { + *counts.entry(homes[0].clone()).or_default() += 1; + } + } + let total = counts.values().sum::(); + let largest = counts.values().copied().max().unwrap_or_default(); + if counts.len() < 3 || total == 0 || largest.saturating_mul(5) > total.saturating_mul(4) { + anyhow::bail!( + "qualified complexity in jurisdiction {jurisdiction_id} is not horizontally distributed: {total} subjects across {} occupied places, largest place {largest}", + counts.len() + ) + } + } + Ok(()) +} + +fn validate_exact_world_scale_count( + qualified_subject_count: u32, + target_qualified_subject_count: u32, + maximum_complexity_rounds: usize, +) -> anyhow::Result<()> { + if qualified_subject_count != target_qualified_subject_count { + anyhow::bail!( + "world complexity stopped at {qualified_subject_count} of exact target {target_qualified_subject_count} admitted subjects after {maximum_complexity_rounds} rounds" + ) + } + Ok(()) +} + fn strategic_locality_request(location_name: &str, location_id: &str, pressure: &str) -> String { - let pressure = pressure.chars().take(60).collect::(); - let request = format!( - "Elaborate canonical locality {location_name:?} (ID {location_id}) as a politically inhabited jurisdiction. Crisis: {pressure}. Add exactly four non-overlapping resident population leaves and exactly six distinct institutions, never more. Invent authority, succession, revenue, redress, leverage, and opposed interests. Give every new subject a concrete public notice or report channel." + let location_name = bounded_prompt_excerpt(location_name, 48); + let prefix = format!( + "Elaborate {location_name:?} [{location_id}]. Exactly 4 resident populations and 6 institutions. Include one mixed/diasporic/cross-border body; ancestry is not a civic partition. Give opposed interests executable means. State authority, succession, revenue, redress, and a public notice channel. Pressure: " ); - request.chars().take(500).collect() + bounded_strategic_request(prefix, pressure) +} + +fn validate_strategic_foundation_civic_shape( + civic: &ghostlight_dungeon::domain::CivicSystemManifest, +) -> anyhow::Result<()> { + if civic.resident_population_ids.len() != 4 || civic.governing_institution_ids.len() != 6 { + anyhow::bail!( + "strategic locality foundation requires exactly four resident populations and six governing institutions; candidate supplied {} and {}", + civic.resident_population_ids.len(), + civic.governing_institution_ids.len() + ) + } + Ok(()) } fn strategic_titled_locality_request( @@ -3575,11 +5621,71 @@ fn strategic_titled_locality_request( location_id: &str, pressure: &str, ) -> String { - let pressure = pressure.chars().take(60).collect::(); - let request = format!( - "Deepen canonical locality {location_name:?} (ID {location_id}) after its civic foundation has been admitted. Current pressure: {pressure}. Add independently authored texture, material pressure, ordinary life, political leverage, secrets, active instability, and numinous meaning without rewriting the admitted apparatus." + let location_name = bounded_prompt_excerpt(location_name, 48); + let prefix = format!( + "Deepen canonical locality {location_name:?} [{location_id}] after civic admission. Add intact ordinary life, material pressure, political leverage, secrets, instability, and numinous meaning. Prefer executable means, cross-border ties, and non-civic ecology over another filing procedure. Pressure: " ); - request.chars().take(500).collect() + bounded_strategic_request(prefix, pressure) +} + +fn strategic_titled_repair_request( + location_name: &str, + location_id: &str, + pressure: &str, + diagnostic: &str, +) -> String { + const COMPILER_REQUEST_LIMIT: usize = 500; + const PRESSURE_RESERVE: usize = 48; + let location_name = bounded_prompt_excerpt(location_name, 48); + let prefix = format!( + "Redo {location_name:?} [{location_id}] on the same frozen world: fresh complete deepening, civic authority preserved, with material/ecological/political means. Verifier finding: " + ); + let pressure_label = " Pressure: "; + let diagnostic_budget = COMPILER_REQUEST_LIMIT + .saturating_sub(prefix.chars().count()) + .saturating_sub(pressure_label.chars().count()) + .saturating_sub(PRESSURE_RESERVE); + let diagnostic = bounded_prompt_excerpt(diagnostic, diagnostic_budget); + bounded_strategic_request(format!("{prefix}{diagnostic}{pressure_label}"), pressure) +} + +fn bounded_strategic_request(prefix: String, pressure: &str) -> String { + const COMPILER_REQUEST_LIMIT: usize = 500; + let prefix_chars = prefix.chars().count(); + assert!( + prefix_chars < COMPILER_REQUEST_LIMIT, + "strategic request identity exceeds compiler request budget" + ); + let pressure = bounded_prompt_excerpt(pressure, COMPILER_REQUEST_LIMIT - prefix_chars); + let request = format!("{prefix}{pressure}"); + debug_assert!(request.chars().count() <= COMPILER_REQUEST_LIMIT); + request +} + +fn bounded_prompt_excerpt(value: &str, max_chars: usize) -> String { + let trimmed = value.trim(); + if max_chars == 0 { + return String::new(); + } + if trimmed.chars().count() <= max_chars { + return trimmed.into(); + } + let mut excerpt = String::new(); + for word in trimmed.split_whitespace() { + let separator = usize::from(!excerpt.is_empty()); + if excerpt.chars().count() + separator + word.chars().count() + 1 > max_chars { + break; + } + if !excerpt.is_empty() { + excerpt.push(' '); + } + excerpt.push_str(word); + } + if excerpt.is_empty() { + excerpt.extend(trimmed.chars().take(max_chars.saturating_sub(1))); + } + excerpt.push('…'); + excerpt } fn strategic_world_elaboration_profile() -> ghostlight_dungeon::elaboration::WorldElaborationProfile @@ -3596,10 +5702,10 @@ fn strategic_world_elaboration_profile() -> ghostlight_dungeon::elaboration::Wor title, // Patina's bounded child place requires an outward and return // route. Every other title receives two operations per pass. - weight: if title == ElaboratorTitle::Patina { - 3 - } else { - 2 + weight: match title { + ElaboratorTitle::Patina => 9, + ElaboratorTitle::Tangle => 4, + _ => 2, }, }) .collect(), @@ -3611,6 +5717,7 @@ fn complexity_parent_candidates( demand: &ghostlight_dungeon::elaboration::WorldElaborationDemand, limit: usize, ) -> Vec { + let qualified_ids = ghostlight_dungeon::elaboration::canonical_actionable_subject_ids(campaign); let mut current_by_realm = demand .realm_subject_targets .keys() @@ -3620,7 +5727,7 @@ fn complexity_parent_candidates( for profile in campaign .agency_profiles .values() - .filter(|profile| profile.active_leaf && profile.simulation_eligible) + .filter(|profile| qualified_ids.contains(&profile.subject_id)) { if let Some(realm) = complexity_realm_for_profile(campaign, profile, demand) { *current_by_realm.entry(realm).or_default() += 1; @@ -3639,7 +5746,7 @@ fn complexity_parent_candidates( let mut by_realm = BTreeMap::, String)>>::new(); for (id, profile) in campaign.gestalts.keys().filter_map(|id| { let profile = campaign.agency_profiles.get(id)?; - (profile.active_leaf && profile.simulation_eligible).then_some((id, profile)) + qualified_ids.contains(id).then_some((id, profile)) }) { if let Some(realm) = complexity_realm_for_profile(campaign, profile, demand) && realm_pressure.get(&realm).copied().unwrap_or(0) > 0 @@ -3964,33 +6071,28 @@ fn strategic_campaign() -> ghostlight_dungeon::domain::Campaign { #[cfg(test)] mod tests { use super::{ - ComplexityPreviewInvocation, HistoricalWorldNewspaperArticleV2, + ComplexityCommitValidationMode, ComplexityMutationCheckpoint, + ComplexityPreparedMutationCheckpoint, ComplexityPreviewInvocation, + ComplexityRoundCheckpoint, HistoricalWorldNewspaperArticleV2, HistoricalWorldNewspaperEditorialVerdict, HistoricalWorldNewspaperGroundingVerdict, - HistoricalWorldNewspaperIssueV2, admitted_public_channel, + HistoricalWorldNewspaperIssueV2, admitted_public_channel, bounded_prompt_excerpt, civic_manifest_is_committed_candidate, civic_manifest_preserves, committed_elaboration_mutation_proof, completed_wave_issue_campaign, - complexity_realm_for_profile, complexity_session_journal_summary, final_wave_field, - fission_population_binding_is_present, fission_relation_binding_is_present, - latest_partial_wave_checkpoint, missing_newspaper_report_indices, + complexity_affected_subject_ids, complexity_realm_for_profile, + complexity_semantic_rejection_diagnostic, complexity_session_journal_summary, + final_wave_field, fission_population_binding_is_present, + fission_relation_binding_is_present, latest_partial_wave_checkpoint, + load_and_validate_complexity_mutation_checkpoint, missing_newspaper_report_indices, publish_immutable_checkpoint, recomposed_model_receipt_set_digest, recover_committed_clock_binding, retain_unique_complexity_invocations, strategic_campaign, - strategic_locality_request, strategic_smoke_bytes_digest, strategic_smoke_digest, - strategic_titled_locality_request, titled_failure_checkpoint_paths, + strategic_locality_request, strategic_region_request, strategic_smoke_bytes_digest, + strategic_smoke_digest, strategic_titled_locality_request, strategic_titled_repair_request, + titled_failure_checkpoint_paths, validate_completed_complexity_round_checkpoint, validate_completed_newspaper_recomposition_receipt, + validate_complexity_round_session_checkpoints, validate_exact_world_scale_count, + validate_strategic_foundation_civic_shape, }; - #[test] - fn complexity_journal_summary_does_not_duplicate_typed_subject_ids() { - let summary = - complexity_session_journal_summary("fission_gestalt", "the-assigned-parent-population"); - - assert_eq!( - summary, - "Applied fission_gestalt to the-assigned-parent-population." - ); - assert!(summary.chars().count() < 1_000); - } - #[test] fn complexity_realm_is_derived_from_canonical_location_containment() { use ghostlight_dungeon::domain::{Location, Route}; @@ -4049,11 +6151,271 @@ mod tests { assert_eq!(profile.location_ids, BTreeSet::from(["ward".into()])); } + #[test] + fn completed_complexity_round_rejects_missing_referenced_mutation_despite_sufficient_count() { + use ghostlight_dungeon::elaboration::{ + ElaborationDispatchState, ElaborationScheduleReceipt, WorldElaborationDemand, + canonical_actionable_subject_count, + }; + use std::collections::{BTreeMap, BTreeSet}; + + let directory = tempfile::tempdir().unwrap(); + let store = ghostlight_dungeon::persistence::CampaignStore::open( + directory.path().join("campaign.cc"), + ) + .unwrap(); + let campaign = strategic_campaign(); + let missing = directory + .path() + .join("complexity-round-001-commit-0001.json"); + let checkpoint = ComplexityRoundCheckpoint { + schema: "ghostlight.complexity_round_checkpoint.v1".into(), + round: 1, + demand_before: WorldElaborationDemand { + schema: "ghostlight.world_elaboration_demand.v1".into(), + active_cell_budget: 240, + target_active_cover_basis_points: 2_000, + target_actionable_subjects: 48, + current_actionable_subjects: 1, + actionable_subject_deficit: 47, + round_mutation_budget: 1, + realm_complexity_weights: BTreeMap::new(), + realm_subject_targets: BTreeMap::new(), + }, + actionable_subjects_after: canonical_actionable_subject_count(&campaign), + schedule: ElaborationScheduleReceipt { + schema: "ghostlight.elaboration_schedule_receipt.v1".into(), + requested_invocations: 0, + unused_invocations: 0, + eligible_titles: BTreeSet::new(), + dispatch_counts: BTreeMap::new(), + unused_counts: BTreeMap::new(), + dispatches: Vec::new(), + final_state: ElaborationDispatchState::default(), + }, + mutation_checkpoints: vec![missing], + superseded_invocation_checkpoints: Vec::new(), + session_checkpoints: BTreeMap::new(), + }; + + let error = validate_completed_complexity_round_checkpoint( + directory.path(), + &store, + &campaign, + &checkpoint, + 1, + ) + .unwrap_err(); + assert!( + error + .to_string() + .contains("references a missing mutation checkpoint") + ); + } + + #[test] + fn completed_complexity_round_cannot_drop_a_touched_session_checkpoint() { + use ghostlight_dungeon::elaboration::{ + ElaboratorSessionCheckpoint, ElaboratorSessionCompactionDraft, + ElaboratorSessionJournalEntry, ElaboratorTitle, elaborator_session_id, + }; + use std::collections::BTreeMap; + + let campaign = strategic_campaign(); + let title = ElaboratorTitle::Charter; + let location_id = "yard".to_owned(); + let session_id = elaborator_session_id(title, &location_id); + let journal = ElaboratorSessionJournalEntry { + world_revision: campaign.revision, + commit_receipt_id: format!("{}-{}", campaign.id, campaign.revision), + mutation_kind: "fission".into(), + affected_subject_ids: vec!["workers".into()], + summary: "Applied one distinct charter split.".into(), + }; + let checkpoint = ElaboratorSessionCheckpoint::bind_compaction( + &session_id, + title, + 1, + campaign.id, + campaign.revision, + &location_id, + ElaboratorSessionCompactionDraft { + schema: "ghostlight.elaborator_session_compaction_draft.v1".into(), + frontier_summary: "One charter distinction remains live.".into(), + unresolved_leads: Vec::new(), + }, + vec![journal.commit_receipt_id.clone()], + Vec::new(), + None, + ) + .unwrap(); + let routes = BTreeMap::from([(session_id.clone(), (title, location_id))]); + let journals = BTreeMap::from([(session_id.clone(), vec![journal])]); + let mut current = BTreeMap::from([(session_id.clone(), checkpoint)]); + validate_complexity_round_session_checkpoints( + &campaign, + &BTreeMap::new(), + ¤t, + &routes, + &journals, + &BTreeMap::new(), + campaign.revision, + ) + .unwrap(); + + current.remove(&session_id); + let diagnostic = validate_complexity_round_session_checkpoints( + &campaign, + &BTreeMap::new(), + ¤t, + &routes, + &journals, + &BTreeMap::new(), + campaign.revision, + ) + .unwrap_err() + .to_string(); + assert!(diagnostic.contains("session checkpoint set")); + } + + #[test] + fn existing_complexity_commit_checkpoint_rejects_campaign_without_committed_effect() { + use ghostlight_dungeon::domain::{GestaltIndividuation, GestaltMemberDelta}; + use ghostlight_dungeon::elaboration::{ + ElaborationDispatch, ElaboratorTitle, WorldComplexityIndividuationQualification, + WorldComplexityProposal, WorldComplexitySemanticQualification, + }; + use std::collections::{BTreeMap, BTreeSet}; + + let directory = tempfile::tempdir().unwrap(); + let store = ghostlight_dungeon::persistence::CampaignStore::open( + directory.path().join("campaign.cc"), + ) + .unwrap(); + let mut campaign = strategic_campaign(); + let expected_revision = campaign.revision; + let mut semantic = WorldComplexitySemanticQualification::default(); + semantic.frozen_campaign_id = campaign.id; + semantic.frozen_world_revision = expected_revision; + let proposal = WorldComplexityProposal::Individuate { + individuation: GestaltIndividuation { + gestalt_id: "workers".into(), + expected_gestalt_version: campaign.gestalts["workers"].version, + location_id: "yard".into(), + member: GestaltMemberDelta { + schema: "ghostlight.gestalt_member_delta.v1".into(), + id: "resume-ghost".into(), + gestalt_id: "workers".into(), + version: 0, + name: "Mara Quill".into(), + capability_additions: BTreeSet::from(["read pressure gauges".into()]), + capability_removals: BTreeSet::new(), + knowledge_additions: BTreeSet::from(["night sluice timings".into()]), + knowledge_removals: BTreeSet::new(), + equipment: BTreeSet::new(), + conditions: BTreeSet::new(), + obligations: BTreeSet::new(), + relationships: BTreeMap::new(), + goals: vec!["keep the lower channel open".into()], + memories: vec!["the winter gauge fracture".into()], + last_location_id: Some("yard".into()), + materialized_actor_id: None, + last_relevant_revision: expected_revision, + relevance_lease_until_revision: expected_revision, + }, + }, + qualification: WorldComplexityIndividuationQualification { + schema: "ghostlight.world_complexity_individuation_qualification.v1".into(), + title: ElaboratorTitle::Veil, + jurisdiction_location_id: "yard".into(), + semantic, + }, + }; + let dispatch = ElaborationDispatch { + schema: "ghostlight.elaboration_dispatch.v1".into(), + budget_ordinal: 1, + ordinal: 1, + title: ElaboratorTitle::Veil, + title_weight: 1, + total_enabled_weight: 1, + requested_share_millionths: 1_000_000, + title_dispatch_count: 1, + }; + let invocation = ComplexityPreviewInvocation { + dispatch: dispatch.clone(), + parent_binding: "plausible-frozen-parent-binding".into(), + proposal: proposal.clone(), + model_receipt_hashes: vec!["sha256:plausible-generation".into()], + }; + let semantic_summary = complexity_session_journal_summary(&proposal).unwrap(); + let affected_subject_ids = complexity_affected_subject_ids(&proposal); + let prepared = ComplexityPreparedMutationCheckpoint { + schema: "ghostlight.complexity_prepared_mutation.v1".into(), + round: 1, + dispatch: dispatch.clone(), + expected_revision, + proposal, + parent_gestalt_id: "workers".into(), + mutation_kind: "elaborate_gestalt_individuation".into(), + affected_subject_ids: affected_subject_ids.clone(), + model_receipt_hashes: vec![ + "sha256:plausible-generation".into(), + "sha256:plausible-verifier".into(), + ], + semantic_summary: semantic_summary.clone(), + }; + campaign.revision = expected_revision.saturating_add(1); + let checkpoint = ComplexityMutationCheckpoint { + schema: "ghostlight.complexity_mutation_checkpoint.v1".into(), + round: 1, + dispatch, + parent_gestalt_id: "workers".into(), + mutation_kind: "elaborate_gestalt_individuation".into(), + affected_subject_ids, + model_receipt_hashes: prepared.model_receipt_hashes.clone(), + semantic_summary, + commit_receipt: ghostlight_dungeon::domain::WorldCommitReceipt { + schema: "ghostlight.world_commit_receipt.v1".into(), + campaign_id: campaign.id, + previous_revision: expected_revision, + revision: campaign.revision, + command_kind: "elaborate_gestalt_individuation".into(), + committed_at: chrono::Utc::now(), + roll: None, + }, + }; + let prepared_path = directory + .path() + .join("complexity-round-001-prepared-0001.json"); + let commit_path = directory + .path() + .join("complexity-round-001-commit-0001.json"); + publish_immutable_checkpoint(&prepared_path, &prepared).unwrap(); + publish_immutable_checkpoint(&commit_path, &checkpoint).unwrap(); + + let error = load_and_validate_complexity_mutation_checkpoint( + &store, + &campaign, + 1, + &invocation, + &prepared_path, + &commit_path, + ComplexityCommitValidationMode::CurrentCampaignEffect, + ) + .unwrap_err(); + assert!( + error + .to_string() + .contains("committed proposal effect is absent") + ); + } + #[test] fn parallel_individuation_retains_one_exact_identity_and_records_supersession() { use ghostlight_dungeon::domain::{GestaltIndividuation, GestaltMemberDelta}; use ghostlight_dungeon::elaboration::{ - ElaborationDispatch, ElaboratorTitle, WorldComplexityProposal, + ElaborationDispatch, ElaboratorTitle, WorldComplexityIndividuationQualification, + WorldComplexityProposal, WorldComplexitySemanticQualification, }; use std::collections::{BTreeMap, BTreeSet}; @@ -4096,6 +6458,12 @@ mod tests { relevance_lease_until_revision: 0, }, }, + qualification: WorldComplexityIndividuationQualification { + schema: "ghostlight.world_complexity_individuation_qualification.v1".into(), + title: ElaboratorTitle::Veil, + jurisdiction_location_id: "yard".into(), + semantic: WorldComplexitySemanticQualification::default(), + }, }, model_receipt_hashes: vec![format!("receipt:{ordinal}")], }; @@ -4111,12 +6479,147 @@ mod tests { .iter() .map(|invocation| invocation.dispatch.ordinal) .collect::>(), - [9, 14] + [9] + ); + assert_eq!( + superseded + .iter() + .map(|checkpoint| checkpoint.dispatch.ordinal) + .collect::>(), + [12, 14] + ); + assert!( + superseded + .iter() + .all(|checkpoint| checkpoint.retained_dispatch_ordinal == 9) + ); + assert_eq!(superseded[0].canonical_subject_ids, ["member:tarin-vel"]); + assert_eq!(superseded[1].canonical_subject_ids, ["member:oren-pell"]); + assert!( + superseded + .iter() + .all(|checkpoint| checkpoint.public_identity_keys == ["tarinvel"]) + ); + } + + #[test] + fn parallel_fissions_reserve_every_child_id_and_public_specific_name() { + use ghostlight_dungeon::domain::{AgencyAxis, GestaltFissionPreview, GestaltPersonaState}; + use ghostlight_dungeon::elaboration::{ + ElaborationDispatch, ElaboratorTitle, WorldComplexityFissionQualification, + WorldComplexityProposal, WorldComplexitySemanticQualification, + }; + use std::collections::{BTreeMap, BTreeSet}; + + let invocation = |ordinal: u64, + parent: &str, + specific_id: &str, + residual_id: &str, + specific_name: &str| + -> ComplexityPreviewInvocation { + ComplexityPreviewInvocation { + dispatch: ElaborationDispatch { + schema: "ghostlight.elaboration_dispatch.v1".into(), + budget_ordinal: ordinal, + ordinal, + title: ElaboratorTitle::Charter, + title_weight: 1, + total_enabled_weight: 1, + requested_share_millionths: 1_000_000, + title_dispatch_count: ordinal, + }, + parent_binding: format!("binding:{parent}"), + proposal: WorldComplexityProposal::Fission { + preview: GestaltFissionPreview { + schema: "ghostlight.gestalt_fission_preview.v1".into(), + campaign_id: uuid::Uuid::nil(), + expected_world_revision: 0, + parent_gestalt_id: parent.into(), + partition_axis: AgencyAxis::Authority, + children: vec![ + GestaltPersonaState { + schema: "ghostlight.gestalt_persona.v1".into(), + id: specific_id.into(), + name: specific_name.into(), + version: 0, + home_location_id: "yard".into(), + shared_capabilities: BTreeSet::new(), + shared_knowledge: BTreeSet::new(), + resources: BTreeSet::new(), + goals: Vec::new(), + pressures: Vec::new(), + }, + GestaltPersonaState { + schema: "ghostlight.gestalt_persona.v1".into(), + id: residual_id.into(), + name: format!("{parent} remainder"), + version: 0, + home_location_id: "yard".into(), + shared_capabilities: BTreeSet::new(), + shared_knowledge: BTreeSet::new(), + resources: BTreeSet::new(), + goals: Vec::new(), + pressures: Vec::new(), + }, + ], + child_partition_values: BTreeMap::from([ + (specific_id.into(), "licensed".into()), + (residual_id.into(), "unresolved".into()), + ]), + residual_child_id: residual_id.into(), + member_child_assignments: BTreeMap::new(), + resource_child_assignments: BTreeMap::new(), + evidence_receipt_ids: Vec::new(), + gaps: Vec::new(), + canon_candidates: Vec::new(), + requires_approval: true, + }, + qualification: WorldComplexityFissionQualification { + schema: "ghostlight.world_complexity_fission_qualification.v1".into(), + title: ElaboratorTitle::Charter, + jurisdiction_location_id: "yard".into(), + target_actionable_gain: 1, + semantic: WorldComplexitySemanticQualification::default(), + }, + }, + model_receipt_hashes: vec![format!("receipt:{ordinal}")], + } + }; + let invocations = vec![ + invocation(3, "yard-carriers", "licensed", "residual", "Ledger Keepers"), + invocation( + 7, + "route-carriers", + "licensed", + "residual", + "Route Charter Hands", + ), + invocation( + 9, + "sluice-carriers", + "distinct-talliers", + "sluice-residual", + "Distinct Talliers", + ), + ]; + + let (retained, superseded) = retain_unique_complexity_invocations(4, &invocations); + + assert_eq!( + retained + .iter() + .map(|invocation| invocation.dispatch.ordinal) + .collect::>(), + [3, 9] ); assert_eq!(superseded.len(), 1); - assert_eq!(superseded[0].dispatch.ordinal, 12); - assert_eq!(superseded[0].retained_dispatch_ordinal, 9); - assert_eq!(superseded[0].canonical_member_id, "tarin-vel"); + assert_eq!(superseded[0].dispatch.ordinal, 7); + assert_eq!(superseded[0].retained_dispatch_ordinal, 3); + assert_eq!( + superseded[0].canonical_subject_ids, + ["licensed", "residual"] + ); + assert_eq!(superseded[0].public_identity_keys, ["routecharterhands"]); } fn civic_manifest( @@ -4140,6 +6643,17 @@ mod tests { } } + #[test] + fn strategic_foundation_shape_requires_the_requested_exact_population_and_institution_counts() { + let mut civic = civic_manifest(0, ""); + civic.resident_population_ids = (0..4).map(|index| format!("people-{index}")).collect(); + civic.governing_institution_ids = (0..6).map(|index| format!("office-{index}")).collect(); + validate_strategic_foundation_civic_shape(&civic).unwrap(); + + civic.resident_population_ids.remove("people-3"); + assert!(validate_strategic_foundation_civic_shape(&civic).is_err()); + } + #[test] fn resumed_foundation_bindings_follow_complete_fission_lineage() { use std::collections::{BTreeMap, BTreeSet}; @@ -4753,11 +7267,39 @@ mod tests { assert!(request.contains("Seed Vault")); assert!(request.contains("loc-seed-vault")); - assert!(request.contains("exactly four non-overlapping resident population leaves")); - assert!(request.contains("exactly six distinct institutions")); + assert!(request.contains("Exactly 4 resident populations and 6 institutions")); assert!(request.contains("authority, succession, revenue, redress")); - assert!(request.contains("public notice or report channel")); + assert!(request.contains("public notice channel")); + assert!(request.contains("Pressure: an intricately witnessed")); + assert!(request.ends_with('…')); assert!(request.chars().count() <= 500); + + let maximum_identity_request = strategic_locality_request( + &"N".repeat(160), + &"location-id-".repeat(13), + &"pressure-word ".repeat(40), + ); + assert!(maximum_identity_request.chars().count() <= 500); + } + + #[test] + fn bounded_prompt_excerpt_never_cuts_through_a_word() { + let excerpt = bounded_prompt_excerpt( + "constitutional succession remains disputed across the eastern works", + 32, + ); + assert_eq!(excerpt, "constitutional succession…"); + assert!(excerpt.chars().count() <= 32); + } + + #[test] + fn bounded_prompt_excerpt_preserves_an_overlong_first_token() { + let pressure = format!("{} meaningful words follow", "x".repeat(80)); + let excerpt = bounded_prompt_excerpt(&pressure, 32); + + assert_eq!(excerpt.chars().count(), 32); + assert!(excerpt.starts_with(&"x".repeat(31))); + assert!(excerpt.ends_with('…')); } #[test] @@ -4770,13 +7312,90 @@ mod tests { assert!(request.contains("Seed Vault")); assert!(request.contains("loc-seed-vault")); - assert!(request.contains("after its civic foundation has been admitted")); - assert!(request.contains("ordinary life, political leverage, secrets")); + assert!(request.contains("after civic admission")); + assert!(request.contains("ordinary life, material pressure, political leverage")); + assert!(request.contains("Pressure: an intricately witnessed")); + assert!(request.ends_with('…')); assert!(!request.contains("exactly four")); assert!(!request.contains("exactly six")); assert!(request.chars().count() <= 500); } + #[test] + fn titled_semantic_repair_request_is_bounded_and_candidate_specific() { + let location_id = "location-id-".repeat(13); + let diagnostic = format!( + "{{\"failed_checks\":[\"constituencies_cross_boundaries_or_local_homogeneity_justified\"],\"rationale\":\"{}\"}}", + "verifier detail ".repeat(80) + ); + let request = strategic_titled_repair_request( + &"N".repeat(160), + &location_id, + &"pressure-word ".repeat(80), + &diagnostic, + ); + + assert!(request.contains(&location_id)); + assert!(request.contains("same frozen world")); + assert!(request.contains("fresh complete deepening")); + assert!(request.contains("constituencies_cross_boundaries_or_local_homogeneity_justified")); + assert!(request.contains("Pressure:")); + assert!(request.chars().count() <= 500); + } + + #[test] + fn world_scale_completion_requires_the_exact_qualified_count() { + validate_exact_world_scale_count(1_200, 1_200, 128).unwrap(); + for observed in [1_199, 1_201] { + let diagnostic = validate_exact_world_scale_count(observed, 1_200, 128) + .unwrap_err() + .to_string(); + assert!(diagnostic.contains(&observed.to_string())); + assert!(diagnostic.contains("exact target 1200")); + } + } + + #[test] + fn strategic_region_request_preserves_the_complete_operator_brief() { + let operator_brief = format!( + "{} final settlement, route, deep-crisis response, polity, and culture requirements.", + "regional-detail ".repeat(10) + ); + assert!(operator_brief.chars().count() <= 241); + + let request = strategic_region_request(&operator_brief); + + assert!(request.ends_with(&operator_brief)); + assert!(request.contains("exactly 4 resident populations")); + assert!(request.contains("6 governing institutions")); + assert!(request.contains("3+ connected internal places")); + assert!(request.contains("one mixed/cross-border population")); + assert!(request.chars().count() <= 500); + } + + #[test] + fn complexity_semantic_feedback_preserves_each_failed_dimension() { + let verdict = ghostlight_dungeon::elaboration::WorldComplexitySemanticVerification { + public_names_are_legible_identifiers: true, + names_do_not_repeat_an_overused_template: false, + cultural_resemblance_is_grounded_not_quota_cloning: true, + causal_additions_are_materially_distinct: true, + causal_additions_do_not_repeat_an_overused_procedural_template: false, + rationale: "The names share one mold and the causal additions repeat a filing ritual." + .into(), + }; + + let diagnostic = complexity_semantic_rejection_diagnostic(&verdict).unwrap(); + let compacted = bounded_prompt_excerpt(&diagnostic, 800); + + assert!(compacted.contains("\"names_do_not_repeat_an_overused_template\":false")); + assert!( + compacted.contains( + "\"causal_additions_do_not_repeat_an_overused_procedural_template\":false" + ) + ); + } + #[test] fn static_fixture_obeys_the_same_channel_and_knowledge_invariant() { let campaign = strategic_campaign(); diff --git a/crates/ghostlight-dungeon/src/bin/ghostlight-world-explore.rs b/crates/ghostlight-dungeon/src/bin/ghostlight-world-explore.rs index 9080a13..5f32956 100644 --- a/crates/ghostlight-dungeon/src/bin/ghostlight-world-explore.rs +++ b/crates/ghostlight-dungeon/src/bin/ghostlight-world-explore.rs @@ -6,7 +6,7 @@ use std::collections::{BTreeMap, BTreeSet}; fn main() -> Result<()> { let mut args = std::env::args().skip(1); let store_path = args.next().context( - "usage: ghostlight-world-explore [query]", + "usage: ghostlight-world-explore [query]", )?; let command = args.next().unwrap_or_else(|| "overview".into()); let operand = args.next(); @@ -17,6 +17,7 @@ fn main() -> Result<()> { let campaign = load_campaign(&store_path)?; let output = match command.as_str() { "overview" => overview(&campaign), + "quality" => quality(&campaign), "search" => search( &campaign, operand @@ -35,12 +36,607 @@ fn main() -> Result<()> { .as_deref() .context("subject requires one exact subject ID")?, )?, - other => bail!("unknown command {other:?}; expected overview, search, region, or subject"), + other => bail!( + "unknown command {other:?}; expected overview, quality, search, region, or subject" + ), }; println!("{}", serde_json::to_string_pretty(&output)?); Ok(()) } +fn normalized_text(value: &str) -> String { + value + .chars() + .filter(|character| character.is_alphanumeric()) + .flat_map(char::to_lowercase) + .collect() +} + +fn terminal_name_token(value: &str) -> String { + value + .split(|character: char| !character.is_alphanumeric()) + .filter(|token| !token.is_empty()) + .last() + .map(normalized_text) + .unwrap_or_default() +} + +fn duplicate_groups(entries: impl IntoIterator) -> Value { + let mut groups: BTreeMap> = BTreeMap::new(); + for (key, id) in entries { + if !key.is_empty() { + groups.entry(key).or_default().push(id); + } + } + let mut groups = groups + .into_iter() + .filter(|(_, ids)| ids.len() > 1) + .map(|(key, mut ids)| { + ids.sort(); + let count = ids.len(); + ids.truncate(20); + json!({"key":key,"count":count,"subjectIdSample":ids}) + }) + .collect::>(); + groups.sort_by(|left, right| { + right["count"] + .as_u64() + .cmp(&left["count"].as_u64()) + .then_with(|| left["key"].as_str().cmp(&right["key"].as_str())) + }); + let group_count = groups.len(); + let occurrence_count = groups + .iter() + .filter_map(|group| group["count"].as_u64()) + .sum::(); + let member_sample_truncated = groups.iter().any(|group| { + group["count"].as_u64().unwrap_or(0) + > group["subjectIdSample"] + .as_array() + .map_or(0, |sample| sample.len()) as u64 + }); + groups.truncate(20); + json!({ + "groupCount":group_count, + "occurrenceCount":occurrence_count, + "groupsSample":groups, + "truncated":group_count > 20 || member_sample_truncated, + "groupSampleLimit":20, + "subjectIdSampleLimitPerGroup":20, + }) +} + +fn gestalt_semantic_key(gestalt: &ghostlight_dungeon::domain::GestaltPersonaState) -> String { + serde_json::to_string(&( + &gestalt.shared_capabilities, + &gestalt.shared_knowledge, + &gestalt.goals, + &gestalt.pressures, + )) + .unwrap_or_default() +} + +fn lineage_depth(campaign: &Campaign, subject_id: &str) -> usize { + let mut depth = 0; + let mut current = subject_id; + let mut visited = BTreeSet::new(); + while visited.insert(current.to_owned()) { + let Some(parent) = campaign + .agency_profiles + .get(current) + .and_then(|profile| profile.parent_subject_id.as_deref()) + else { + break; + }; + depth += 1; + current = parent; + } + depth +} + +fn repeated_texts<'a>(values: impl IntoIterator) -> Value { + duplicate_groups( + values + .into_iter() + .map(|(id, value)| (normalized_text(value), id.to_owned())), + ) +} + +fn has_structurally_complete_named_person_shape( + has_goal: bool, + has_memory: bool, + has_detail_state: bool, +) -> bool { + has_goal && has_memory && has_detail_state +} + +fn quality(campaign: &Campaign) -> Value { + let residual_ids = campaign + .gestalt_lineages + .values() + .map(|lineage| lineage.residual_child_id.clone()) + .collect::>(); + let raw_actionable_ids = campaign + .agency_profiles + .values() + .filter(|profile| profile.active_leaf && profile.simulation_eligible) + .map(|profile| profile.subject_id.clone()) + .collect::>(); + let qualified_ids = ghostlight_dungeon::elaboration::canonical_actionable_subject_ids(campaign); + let qualified_count = + ghostlight_dungeon::elaboration::canonical_actionable_subject_count(campaign); + let twenty_percent_target = u32::from(campaign.resolution_policy.active_cell_budget) * 5; + let qualified_target_delta = i64::from(twenty_percent_target) - i64::from(qualified_count); + + let materialized_member_actor_ids = campaign + .gestalt_members + .values() + .flat_map(|member| { + member + .materialized_actor_id + .iter() + .cloned() + .chain(std::iter::once(format!("member:{}", member.id))) + }) + .collect::>(); + let person_name_records = campaign + .actors + .values() + .filter(|actor| { + actor.id != campaign.player_actor_id + && !materialized_member_actor_ids.contains(&actor.id) + }) + .map(|actor| (actor.name.clone(), format!("actor:{}", actor.id))) + .chain( + campaign + .gestalt_members + .values() + .map(|member| (member.name.clone(), format!("member:{}", member.id))), + ) + .collect::>(); + let population_name_records = campaign + .gestalts + .values() + .filter(|gestalt| qualified_ids.contains(&gestalt.id)) + .map(|gestalt| (gestalt.name.clone(), gestalt.id.clone())) + .collect::>(); + let invalid_person_identity_ids = person_name_records + .iter() + .filter(|(name, _)| normalized_text(name).is_empty()) + .map(|(_, id)| id.clone()) + .collect::>(); + let invalid_population_identity_ids = population_name_records + .iter() + .filter(|(name, _)| normalized_text(name).is_empty()) + .map(|(_, id)| id.clone()) + .collect::>(); + let duplicate_person_names = duplicate_groups( + person_name_records + .iter() + .map(|(name, id)| (normalized_text(name), id.clone())), + ); + let duplicate_population_names = duplicate_groups( + population_name_records + .iter() + .map(|(name, id)| (normalized_text(name), id.clone())), + ); + let repeated_person_terminal_tokens = duplicate_groups( + person_name_records + .iter() + .map(|(name, id)| (terminal_name_token(name), id.clone())), + ); + let repeated_population_terminal_tokens = duplicate_groups( + population_name_records + .iter() + .map(|(name, id)| (terminal_name_token(name), id.clone())), + ); + + // This is a structural record-shape count, not a judgment of semantic quality. + // A counted standalone actor has at least one goal, at least one memory, and at + // least one capability, knowledge item, equipment item, condition, obligation, + // or relationship. The player and actor projections of gestalt members are + // excluded. A counted gestalt member has at least one goal, at least one memory, + // and at least one capability addition, knowledge addition, equipment item, + // condition, obligation, or relationship. Naming validity is measured separately. + let structurally_complete_actor_count = campaign + .actors + .values() + .filter(|actor| { + actor.id != campaign.player_actor_id + && !materialized_member_actor_ids.contains(&actor.id) + }) + .filter(|actor| { + has_structurally_complete_named_person_shape( + !actor.goals.is_empty(), + !actor.memories.is_empty(), + !actor.capabilities.is_empty() + || !actor.knowledge.is_empty() + || !actor.equipment.is_empty() + || !actor.conditions.is_empty() + || !actor.obligations.is_empty() + || !actor.relationships.is_empty(), + ) + }) + .count(); + let structurally_complete_member_count = campaign + .gestalt_members + .values() + .filter(|member| { + has_structurally_complete_named_person_shape( + !member.goals.is_empty(), + !member.memories.is_empty(), + !member.capability_additions.is_empty() + || !member.knowledge_additions.is_empty() + || !member.equipment.is_empty() + || !member.conditions.is_empty() + || !member.obligations.is_empty() + || !member.relationships.is_empty(), + ) + }) + .count(); + let actor_person_count = campaign + .actors + .values() + .filter(|actor| { + actor.id != campaign.player_actor_id + && !materialized_member_actor_ids.contains(&actor.id) + }) + .count(); + let named_person_count = actor_person_count + campaign.gestalt_members.len(); + + let qualified_gestalts = campaign + .gestalts + .values() + .filter(|gestalt| qualified_ids.contains(&gestalt.id)) + .collect::>(); + let semantic_clone_groups = duplicate_groups( + qualified_gestalts + .iter() + .map(|gestalt| (gestalt_semantic_key(gestalt), gestalt.id.clone())), + ); + let repeated_goals = repeated_texts(qualified_gestalts.iter().flat_map(|gestalt| { + gestalt + .goals + .iter() + .map(|value| (gestalt.id.as_str(), value)) + })); + let repeated_pressures = repeated_texts(qualified_gestalts.iter().flat_map(|gestalt| { + gestalt + .pressures + .iter() + .map(|value| (gestalt.id.as_str(), value)) + })); + let direct_fission_deltas = qualified_gestalts + .iter() + .filter_map(|gestalt| { + let parent_id = campaign + .agency_profiles + .get(&gestalt.id)? + .parent_subject_id + .as_ref()?; + let parent = campaign.gestalts.get(parent_id)?; + Some(( + gestalt.id.as_str(), + gestalt + .shared_capabilities + .difference(&parent.shared_capabilities) + .cloned() + .collect::>(), + gestalt + .shared_knowledge + .difference(&parent.shared_knowledge) + .cloned() + .collect::>(), + gestalt + .goals + .iter() + .filter(|goal| !parent.goals.contains(goal)) + .cloned() + .collect::>(), + gestalt + .pressures + .iter() + .filter(|pressure| !parent.pressures.contains(pressure)) + .cloned() + .collect::>(), + )) + }) + .collect::>(); + let repeated_direct_added_goals = repeated_texts( + direct_fission_deltas + .iter() + .flat_map(|(id, _, _, goals, _)| goals.iter().map(|value| (*id, value))), + ); + let repeated_direct_added_pressures = repeated_texts( + direct_fission_deltas + .iter() + .flat_map(|(id, _, _, _, pressures)| pressures.iter().map(|value| (*id, value))), + ); + let exact_direct_delta_clone_groups = duplicate_groups(direct_fission_deltas.iter().map( + |(id, capabilities, knowledge, goals, pressures)| { + ( + serde_json::to_string(&(capabilities, knowledge, goals, pressures)) + .unwrap_or_default(), + (*id).to_owned(), + ) + }, + )); + + let mut raw_relation_degrees = qualified_ids + .iter() + .map(|id| (id.clone(), 0usize)) + .collect::>(); + let mut non_inherited_relation_degrees = raw_relation_degrees.clone(); + let mut raw_relation_kind_counts = BTreeMap::::new(); + let mut non_inherited_relation_kind_counts = BTreeMap::::new(); + let mut mechanically_inherited_relation_count = 0usize; + for relation in campaign + .agency_relations + .values() + .filter(|relation| relation.active) + { + if let Some(degree) = raw_relation_degrees.get_mut(&relation.from_subject_id) { + *degree += 1; + } + if let Some(degree) = raw_relation_degrees.get_mut(&relation.to_subject_id) { + *degree += 1; + } + let kind = format!("{:?}", relation.kind).to_lowercase(); + *raw_relation_kind_counts.entry(kind.clone()).or_default() += 1; + if relation.id.contains(":fission:") { + mechanically_inherited_relation_count += 1; + continue; + } + if let Some(degree) = non_inherited_relation_degrees.get_mut(&relation.from_subject_id) { + *degree += 1; + } + if let Some(degree) = non_inherited_relation_degrees.get_mut(&relation.to_subject_id) { + *degree += 1; + } + *non_inherited_relation_kind_counts.entry(kind).or_default() += 1; + } + let mut individual_relationship_entries = 0usize; + let mut record_individual_relationship = |from_subject_id: &str, to_subject_id: &str| { + individual_relationship_entries += 1; + if let Some(degree) = raw_relation_degrees.get_mut(from_subject_id) { + *degree += 1; + } + if let Some(degree) = raw_relation_degrees.get_mut(to_subject_id) { + *degree += 1; + } + if let Some(degree) = non_inherited_relation_degrees.get_mut(from_subject_id) { + *degree += 1; + } + if let Some(degree) = non_inherited_relation_degrees.get_mut(to_subject_id) { + *degree += 1; + } + *raw_relation_kind_counts + .entry("individual_relationship".into()) + .or_default() += 1; + *non_inherited_relation_kind_counts + .entry("individual_relationship".into()) + .or_default() += 1; + }; + for actor in campaign.actors.values() { + for other_subject_id in actor.relationships.keys() { + record_individual_relationship(&actor.id, other_subject_id); + } + } + for member in campaign.gestalt_members.values() { + let default_actor_id = ghostlight_dungeon::domain::gestalt_member_subject_id(&member.id); + let projected_actor_exists = campaign.actors.contains_key(&default_actor_id) + || member + .materialized_actor_id + .as_ref() + .is_some_and(|actor_id| campaign.actors.contains_key(actor_id)); + if projected_actor_exists { + continue; + } + for other_subject_id in member.relationships.keys() { + record_individual_relationship(&default_actor_id, other_subject_id); + } + } + let raw_isolated_subject_ids = raw_relation_degrees + .iter() + .filter(|(_, degree)| **degree == 0) + .map(|(id, _)| id.clone()) + .collect::>(); + let non_inherited_isolated_subject_ids = non_inherited_relation_degrees + .iter() + .filter(|(_, degree)| **degree == 0) + .map(|(id, _)| id.clone()) + .collect::>(); + let raw_degree_sum = raw_relation_degrees.values().sum::(); + let non_inherited_degree_sum = non_inherited_relation_degrees.values().sum::(); + + let civic_signatures = duplicate_groups(campaign.civic_systems.values().map(|civic| { + let institutions = civic + .governing_institution_ids + .iter() + .filter_map(|id| campaign.institutions.get(id)) + .map(|institution| { + ( + normalized_text(&institution.posture), + institution + .goals + .iter() + .map(|value| normalized_text(value)) + .collect::>(), + institution + .resources + .iter() + .map(|value| normalized_text(value)) + .collect::>(), + ) + }) + .collect::>(); + let residents = civic + .resident_population_ids + .iter() + .filter_map(|id| campaign.gestalts.get(id)) + .map(gestalt_semantic_key) + .collect::>(); + ( + serde_json::to_string(&(institutions, residents)).unwrap_or_default(), + civic.jurisdiction_location_id.clone(), + ) + })); + + let event_kind_counts = + campaign + .events + .iter() + .fold(BTreeMap::::new(), |mut counts, event| { + *counts.entry(event.kind.clone()).or_default() += 1; + counts + }); + let repeated_event_summaries = duplicate_groups( + campaign + .events + .iter() + .map(|event| (normalized_text(&event.summary), event.id.clone())), + ); + let distribution_by_root = campaign + .locations + .values() + .filter(|location| location.container_id.is_none()) + .map(|location| { + let places = descendant_location_ids(campaign, &location.id); + let count = qualified_ids + .iter() + .filter(|id| { + campaign + .agency_profiles + .get(*id) + .is_some_and(|profile| !profile.location_ids.is_disjoint(&places)) + }) + .count(); + (location.id.clone(), count) + }) + .collect::>(); + let spread_by_jurisdiction = campaign + .civic_systems + .keys() + .filter(|location_id| campaign.locations.contains_key(*location_id)) + .map(|location_id| { + let places = descendant_location_ids(campaign, location_id); + let location_counts = places + .iter() + .map(|place_id| { + let count = qualified_ids + .iter() + .filter(|id| { + campaign + .agency_profiles + .get(*id) + .is_some_and(|profile| profile.location_ids.contains(place_id)) + }) + .count(); + (place_id.clone(), count) + }) + .collect::>(); + let occupied_location_count = location_counts + .values() + .filter(|count| **count > 0) + .count(); + let location_assignment_count = location_counts.values().sum::(); + let largest_location_count = location_counts.values().copied().max().unwrap_or(0); + ( + location_id.clone(), + json!({ + "qualifiedSubjects":qualified_ids.iter().filter(|id|campaign.agency_profiles.get(*id).is_some_and(|profile|!profile.location_ids.is_disjoint(&places))).count(), + "occupiedLocationCount":occupied_location_count, + "locationAssignmentCount":location_assignment_count, + "largestLocationShareBasisPoints":if location_assignment_count == 0 { None } else { Some(largest_location_count * 10_000 / location_assignment_count) }, + "qualifiedSubjectCountsByLocation":location_counts, + }), + ) + }) + .collect::>(); + let occupied_location_count = campaign + .locations + .keys() + .filter(|location_id| { + qualified_ids.iter().any(|id| { + campaign + .agency_profiles + .get(id) + .is_some_and(|profile| profile.location_ids.contains(*location_id)) + }) + }) + .count(); + + json!({ + "schema":"ghostlight.world_quality.v1", + "campaign":{"id":campaign.id,"revision":campaign.revision,"strategicTickCount":campaign.strategic_tick_count}, + "scale":{ + "rawActionableSubjects":raw_actionable_ids.len(), + "qualifiedActionableSubjects":qualified_count, + "residualActionableSubjects":raw_actionable_ids.intersection(&residual_ids).count(), + "twentyPercentTarget":twenty_percent_target, + "qualifiedTargetDelta":qualified_target_delta, + "qualifiedCoverBasisPoints":if qualified_count == 0 { None } else { Some(u32::from(campaign.resolution_policy.active_cell_budget) * 10_000 / qualified_count) }, + "rawCoverBasisPoints":if raw_actionable_ids.is_empty() { None } else { Some(usize::from(campaign.resolution_policy.active_cell_budget) * 10_000 / raw_actionable_ids.len()) }, + "maximumQualifiedLineageDepth":qualified_ids.iter().map(|id|lineage_depth(campaign,id)).max().unwrap_or(0), + "qualifiedDistributionByRootLocation":distribution_by_root, + "occupiedLocationCount":occupied_location_count, + "qualifiedSpreadByJurisdiction":spread_by_jurisdiction, + "geographyFissionCount":campaign.gestalt_lineages.values().filter(|lineage|lineage.partition_axis == ghostlight_dungeon::domain::AgencyAxis::Geography).count(), + }, + "identity":{ + "namedPersons":named_person_count, + "structurallyCompleteNamedPersons":structurally_complete_actor_count + structurally_complete_member_count, + "invalidPublicPersonIdentityIds":invalid_person_identity_ids, + "invalidQualifiedPopulationIdentityIds":invalid_population_identity_ids, + "duplicatePersonNames":duplicate_person_names, + "duplicateQualifiedPopulationNames":duplicate_population_names, + "repeatedPersonTerminalNameTokens":repeated_person_terminal_tokens, + "repeatedQualifiedPopulationTerminalNameTokens":repeated_population_terminal_tokens, + "lexicalDiagnosticsOnly":true, + }, + "causality":{ + "qualifiedGestalts":qualified_gestalts.len(), + "qualifiedGestaltsWithoutGoals":qualified_gestalts.iter().filter(|gestalt|gestalt.goals.is_empty()).count(), + "qualifiedGestaltsWithoutPressures":qualified_gestalts.iter().filter(|gestalt|gestalt.pressures.is_empty()).count(), + "qualifiedGestaltsWithoutCapabilities":qualified_gestalts.iter().filter(|gestalt|gestalt.shared_capabilities.is_empty()).count(), + "qualifiedGestaltsWithoutKnowledge":qualified_gestalts.iter().filter(|gestalt|gestalt.shared_knowledge.is_empty()).count(), + "inheritanceInclusiveExactSemanticCloneGroups":semantic_clone_groups, + "inheritanceInclusiveRepeatedGoals":repeated_goals, + "inheritanceInclusiveRepeatedPressures":repeated_pressures, + "exactDirectFissionDeltaCloneGroups":exact_direct_delta_clone_groups, + "repeatedDirectAddedGoals":repeated_direct_added_goals, + "repeatedDirectAddedPressures":repeated_direct_added_pressures, + }, + "relationships":{ + "collectiveAgencyActiveRelations":campaign.agency_relations.values().filter(|relation|relation.active).count(), + "individualRelationshipEntries":individual_relationship_entries, + "rawActiveRelationshipEntries":campaign.agency_relations.values().filter(|relation|relation.active).count() + individual_relationship_entries, + "estimatedMechanicallyInheritedFissionRelationsByIdConvention":mechanically_inherited_relation_count, + "nonInheritedActiveRelationshipEntries":campaign.agency_relations.values().filter(|relation|relation.active && !relation.id.contains(":fission:")).count() + individual_relationship_entries, + "rawRelationKindCounts":raw_relation_kind_counts, + "nonInheritedRelationKindCounts":non_inherited_relation_kind_counts, + "rawIsolatedQualifiedSubjects":raw_isolated_subject_ids.len(), + "rawIsolatedQualifiedSubjectSample":raw_isolated_subject_ids.into_iter().take(50).collect::>(), + "nonInheritedIsolatedQualifiedSubjects":non_inherited_isolated_subject_ids.len(), + "nonInheritedIsolatedQualifiedSubjectSample":non_inherited_isolated_subject_ids.into_iter().take(50).collect::>(), + "rawMeanQualifiedDegree":if raw_relation_degrees.is_empty() { 0.0 } else { raw_degree_sum as f64 / raw_relation_degrees.len() as f64 }, + "nonInheritedMeanQualifiedDegree":if non_inherited_relation_degrees.is_empty() { 0.0 } else { non_inherited_degree_sum as f64 / non_inherited_relation_degrees.len() as f64 }, + "inheritanceClassification":"estimate based on the current :fission: relation-ID convention; authored IDs can confound it", + }, + "politics":{ + "civicJurisdictions":campaign.civic_systems.len(), + "exactCivicSignatureCloneGroups":civic_signatures, + "exactSignatureDiagnosticsDoNotMeasureSemanticPoliticalDiversity":true, + }, + "behavior":{ + "eventKindCounts":event_kind_counts, + "exactRepeatedEventSummaryGroups":repeated_event_summaries, + "newsIssues":campaign.news.len(), + "exactSummaryDiagnosticsDoNotMeasureSemanticDramaticDiversity":true, + }, + }) +} + fn load_campaign(path: &str) -> Result { let store = CampaignStore::open(path)?; let key = store @@ -388,3 +984,50 @@ fn location_name<'a>(campaign: &'a Campaign, id: &str) -> Option<&'a str> { .get(id) .map(|location| location.name.as_str()) } + +#[cfg(test)] +mod tests { + use super::{duplicate_groups, has_structurally_complete_named_person_shape}; + + #[test] + fn named_person_structural_completeness_requires_goal_memory_and_detail_state() { + assert!(has_structurally_complete_named_person_shape( + true, true, true + )); + assert!(!has_structurally_complete_named_person_shape( + false, true, true + )); + assert!(!has_structurally_complete_named_person_shape( + true, false, true + )); + assert!(!has_structurally_complete_named_person_shape( + true, true, false + )); + } + + #[test] + fn duplicate_report_preserves_complete_prevalence_while_sampling_large_results() { + let mut entries = (0..25) + .map(|index| ("shared".to_owned(), format!("shared-{index:02}"))) + .collect::>(); + for group in 0..21 { + entries.push((format!("group-{group:02}"), format!("group-{group:02}-a"))); + entries.push((format!("group-{group:02}"), format!("group-{group:02}-b"))); + } + + let report = duplicate_groups(entries); + + assert_eq!(report["groupCount"], 22); + assert_eq!(report["occurrenceCount"], 67); + assert_eq!(report["groupsSample"].as_array().unwrap().len(), 20); + assert_eq!(report["groupsSample"][0]["count"], 25); + assert_eq!( + report["groupsSample"][0]["subjectIdSample"] + .as_array() + .unwrap() + .len(), + 20 + ); + assert_eq!(report["truncated"], true); + } +} diff --git a/crates/ghostlight-dungeon/src/compiler.rs b/crates/ghostlight-dungeon/src/compiler.rs index 5f58634..9a3f1a7 100644 --- a/crates/ghostlight-dungeon/src/compiler.rs +++ b/crates/ghostlight-dungeon/src/compiler.rs @@ -132,6 +132,8 @@ impl std::error::Error for DestinationCompilationFailure {} #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct CivicElaborationVerificationFailure { pub message: String, + #[serde(default)] + pub semantic_diagnostic: Option, pub model_receipts: Vec, } @@ -682,10 +684,62 @@ struct CivicSystemVerification { redress_legible: bool, institutional_relations_coherent: bool, resident_answer_grounded: bool, + political_form_distinct_or_deliberately_shared: bool, + opposed_interests_have_distinct_means: bool, + constituencies_cross_boundaries_or_local_homogeneity_justified: bool, + explicit_request_constraints_satisfied: bool, + public_names_do_not_repeat_an_overused_template: bool, + candidate_text_intact: bool, #[schemars(length(min = 1, max = 1_000))] rationale: String, } +fn civic_verification_semantic_diagnostic(verdict: &CivicSystemVerification) -> String { + let failed_checks = [ + ("authority_legible", verdict.authority_legible), + ( + "selection_or_succession_legible", + verdict.selection_or_succession_legible, + ), + ("public_resources_legible", verdict.public_resources_legible), + ("redress_legible", verdict.redress_legible), + ( + "institutional_relations_coherent", + verdict.institutional_relations_coherent, + ), + ("resident_answer_grounded", verdict.resident_answer_grounded), + ( + "political_form_distinct_or_deliberately_shared", + verdict.political_form_distinct_or_deliberately_shared, + ), + ( + "opposed_interests_have_distinct_means", + verdict.opposed_interests_have_distinct_means, + ), + ( + "constituencies_cross_boundaries_or_local_homogeneity_justified", + verdict.constituencies_cross_boundaries_or_local_homogeneity_justified, + ), + ( + "explicit_request_constraints_satisfied", + verdict.explicit_request_constraints_satisfied, + ), + ( + "public_names_do_not_repeat_an_overused_template", + verdict.public_names_do_not_repeat_an_overused_template, + ), + ("candidate_text_intact", verdict.candidate_text_intact), + ] + .into_iter() + .filter_map(|(name, accepted)| (!accepted).then_some(name)) + .collect::>(); + serde_json::json!({ + "failed_checks":failed_checks, + "rationale":verdict.rationale, + }) + .to_string() +} + #[derive(Clone, Debug, Serialize, PartialEq)] struct CompiledResidentFactKnowledge { population_id: String, @@ -2057,6 +2111,7 @@ impl WorldCompiler { }) .collect::>(); let current_civic_context = civic_context(campaign, expansion_origin_location_id); + let peer_civic_context = peer_civic_contexts(campaign, expansion_origin_location_id); let scope_instruction = if let Some(target_id) = existing_destination_id.as_deref() { format!( "The primary destination already exists as exact canonical location {target_id}. Elaborate it in place. Never emit that location again, rename it, replace it, or alter its existing routes or container. Every new location must be a bounded child whose containment chain reaches {target_id}; origin_routes are new local routes owned by {target_id}." @@ -2078,15 +2133,20 @@ impl WorldCompiler { serde_json::to_string(¤t_civic_context).map_err(|error| { destination_compilation_failure(error, completed_model_receipts(&[])) })?; + let peer_civic_apparatuses = + serde_json::to_string(&peer_civic_context).map_err(|error| { + destination_compilation_failure(error, completed_model_receipts(&[])) + })?; let source_population_context = serde_json::to_string(&origin_population_context).map_err(|error| { destination_compilation_failure(error, completed_model_receipts(&[])) })?; let base_prompt = format!( - "Compile only the requested bounded destination region. {scope_instruction} Every new location id must be new. Return explicit origin_routes records owned by expansion anchor id {} into the new region or child locality, and give every such destination a reciprocal route record back to the anchor with the same positive travel time. Every route record needs a stable route_id local to its exact origin, an exact destination_id, a distance, and positive travel_minutes; the same local route_id may exist under another origin without naming the same route. Do not rewrite existing geography. Every place has a non-empty name, valid container, and concrete persistent features. Any locally observable clue must already exist as a fact and list exact discoverable_at_location_ids from the combined existing and new topology; later action assessment can reveal facts but cannot invent them.\n\nUse evidence as canon constraints, not as an exhaustive game map. Missing game-scale routes, geometry, people, ordinary procedures, supplies, local responsibilities, capacity choices, and operating doctrine require the smallest coherent playable elaboration. Mark the resulting facts branch_local or provisional_local and disclose consequential inventions in branch_assumptions. Variation between campaigns is permitted; if a detail must not vary, it belongs in the Vault. Never put a compatible elaboration in gaps merely because the Vault is silent.\n\nWhen CURRENT CIVIC APPARATUS is null, a playable inhabited destination needs one to eight non-overlapping population leaves and two to twelve distinct institutions. When it is present, preserve it and add only genuinely new detail needed by the request; population and institution arrays may be empty. New local relations may join new subjects to exact subjects in the current apparatus. Never duplicate a resident body, office, fact, or relation under a fresh name. Each new population home_location_id must be one new location. Each new institution location_ids set may name the jurisdiction or its new children. shared_fact_ids may contain exact fact IDs from the current apparatus or this candidate, never free-text knowledge. collective_authority_id may be null or the exact ID of one new population and denotes real shared authority.\n\nReturn one complete civic_system manifest for every inhabited candidate. On an existing apparatus it is the next version and must retain every existing governing institution, resident population, public fact, and political relation while adding any new IDs. It must identify the exact jurisdiction, its governing institutions and resident populations, at least one committed public fact in each of four domains—current authority, selection or succession, public resources or revenue, and redress or appeal—and the political relations that make implementation, hierarchy, or contestation legible. Every named resident population must share those public civic facts. If REQUEST presupposes a mayor, election, throne, council, or other office that this locality does not use, commit facts that let a resident correct the premise; never manufacture the requested institution merely to agree with the question. The question selects the missing domain, not its answer.\n\nThe gaps array is legal only when no compatible elaboration can preserve an exact clause of REQUEST without choosing between contradictory canon baselines, inventing an unanchored canon baseline explicitly required by the request, or exceeding an approved capability. Every gap must name that exact premise clause and the exact table choice blocking compilation. `The Vault does not specify X` is never sufficient. Use an empty gaps array when branch-local invention preserves the request.\n\nA migration relation is a directed available path for a later voluntary strategic choice; it does not move anyone, establish that admission occurred, or erase destination-community agency. It may originate only from one exact co-located active population ID supplied below and may target only one new population ID. Emit a relation only when the request and supplied source population/member goals support that migration possibility. Never invent a source population or named member. The approval preview must make all branch-local assumptions explicit without misclassifying them as canon gaps.\n\nCAMPAIGN LOCATIONS:\n{}\nCURRENT CIVIC APPARATUS:\n{}\nCO-LOCATED SOURCE POPULATIONS AND NAMED MEMBER DELTAS:\n{}\nREQUEST:\n{}\nEVIDENCE:\n{}", + "Compile only the requested bounded destination region. {scope_instruction} Every new location id must be new. Return explicit origin_routes records owned by expansion anchor id {} into the new region or child locality, and give every such destination a reciprocal route record back to the anchor with the same positive travel time. Every route record needs a stable route_id local to its exact origin, an exact destination_id, a distance, and positive travel_minutes; the same local route_id may exist under another origin without naming the same route. Do not rewrite existing geography. Every place has a non-empty name, valid container, and concrete persistent features. Any locally observable clue must already exist as a fact and list exact discoverable_at_location_ids from the combined existing and new topology; later action assessment can reveal facts but cannot invent them.\n\nUse evidence as canon constraints, not as an exhaustive game map. Missing game-scale routes, geometry, people, ordinary procedures, supplies, local responsibilities, capacity choices, and operating doctrine require the smallest coherent playable elaboration. Mark the resulting facts branch_local or provisional_local and disclose consequential inventions in branch_assumptions. Variation between campaigns is permitted; if a detail must not vary, it belongs in the Vault. Never put a compatible elaboration in gaps merely because the Vault is silent.\n\nWhen CURRENT CIVIC APPARATUS is null, a playable inhabited destination needs one to eight non-overlapping population leaves and two to twelve distinct institutions. Each population and institution needs a materially executable means such as custody, work, mobility, ritual authority, price setting, force, refusal, or mutual aid, not merely a notice channel or aspiration. When it is present, preserve it and add only genuinely new detail needed by the request; population and institution arrays may be empty. New local relations may join new subjects to exact subjects in the current apparatus. Never duplicate a resident body, office, fact, or relation under a fresh name. Each new population home_location_id must be one new location. Each new institution location_ids set may name the jurisdiction or its new children. shared_fact_ids may contain exact fact IDs from the current apparatus or this candidate, never free-text knowledge. collective_authority_id may be null or the exact ID of one new population and denotes real shared authority.\n\nPEER CIVIC APPARATUSES are contrast evidence. Reuse a political form only when shared history or material dependence explains it. Redress need not be a tribunal or petition: exit, kin restitution, feud mediation, ritual appeal, patronage, strike, refusal, and other locally coherent remedies are valid when their power and limits are legible. Preserve mixed, diasporic, and cross-border constituencies; ancestry or species alone is not a sufficient civic partition. Return one complete civic_system manifest for every inhabited candidate. On an existing apparatus it is the next version and must retain every existing governing institution, resident population, public fact, and political relation while adding any new IDs. It must identify the exact jurisdiction, its governing institutions and resident populations, at least one committed public fact in each of four domains—current authority, selection or succession, public resources or revenue, and redress or appeal—and the political relations that make implementation, hierarchy, or contestation legible. Every named resident population must share those public civic facts. If REQUEST presupposes a mayor, election, throne, council, or other office that this locality does not use, commit facts that let a resident correct the premise; never manufacture the requested institution merely to agree with the question. The question selects the missing domain, not its answer.\n\nThe gaps array is legal only when no compatible elaboration can preserve an exact clause of REQUEST without choosing between contradictory canon baselines, inventing an unanchored canon baseline explicitly required by the request, or exceeding an approved capability. Every gap must name that exact premise clause and the exact table choice blocking compilation. `The Vault does not specify X` is never sufficient. Use an empty gaps array when branch-local invention preserves the request.\n\nA migration relation is a directed available path for a later voluntary strategic choice; it does not move anyone, establish that admission occurred, or erase destination-community agency. It may originate only from one exact co-located active population ID supplied below and may target only one new population ID. Emit a relation only when the request and supplied source population/member goals support that migration possibility. Never invent a source population or named member. The approval preview must make all branch-local assumptions explicit without misclassifying them as canon gaps.\n\nCAMPAIGN LOCATIONS:\n{}\nCURRENT CIVIC APPARATUS:\n{}\nPEER CIVIC APPARATUSES:\n{}\nCO-LOCATED SOURCE POPULATIONS AND NAMED MEMBER DELTAS:\n{}\nREQUEST:\n{}\nEVIDENCE:\n{}", expansion_origin_location_id, campaign_locations, current_civic_apparatus, + peer_civic_apparatuses, source_population_context, destination_request, evidence_text(&receipts) @@ -2111,6 +2171,7 @@ impl WorldCompiler { let mut compiler_receipts: Vec = Vec::new(); let mut correction = String::new(); let mut local_corrections = 0; + let mut semantic_regenerations = 0; let (seed, expansion) = loop { let output = self .structured( @@ -2141,6 +2202,9 @@ impl WorldCompiler { }; if let Err(error) = validate_compiled_material_gaps(&seed.gaps, &receipts) .and_then(|_| validate_branch_assumptions(&seed.branch_assumptions)) + .and_then(|_| { + validate_compiled_destination_population_names(campaign, &seed.populations) + }) { mark_semantic_invalid( compiler_receipts @@ -2253,10 +2317,21 @@ impl WorldCompiler { } CivicSystemVerificationOutcome::Rejected { verdict, receipts } => { compiler_receipts.extend(receipts); - repair_finding = - Some(DestinationReconciliationFinding::CivicVerifier { - verdict, - }); + if semantic_regenerations == 0 { + semantic_regenerations += 1; + correction.push_str(&format!( + "\n\nINDEPENDENT CIVIC VERIFIER REJECTED THE PREVIOUS COMPLETE CANDIDATE:\n{}\nRegenerate the complete destination candidate. You own every candidate field; correct the rejected semantics and prose without weakening the request, evidence, topology, or civic invariants.", + serde_json::to_string(&verdict).unwrap_or_else(|_| verdict.rationale.clone()) + )); + continue; + } + return Err(destination_compilation_failure( + format!( + "destination civic verifier rejected the regenerated candidate: {}", + verdict.rationale.trim() + ), + completed_model_receipts(&compiler_receipts), + )); } CivicSystemVerificationOutcome::Failed { message, receipts } => { compiler_receipts.extend(receipts); @@ -3078,6 +3153,11 @@ impl WorldCompiler { .collect::>(); let candidate = match serde_json::to_string(&serde_json::json!({ "previous_civic_apparatus":current_civic_context, + "peer_civic_apparatuses":peer_civic_contexts( + campaign, + &civic_system.jurisdiction_location_id, + ), + "complete_candidate":expansion, "previous_resident_fact_knowledge":previous_resident_fact_knowledge, "civic_system":civic_system, "new_facts":&expansion.facts, @@ -3094,7 +3174,7 @@ impl WorldCompiler { } }; let base_prompt = format!( - "Independently verify the admitted civic apparatus. Judge meaning, not JSON shape. The public facts must actually explain current authority, selection or succession, public resources or revenue, and redress or appeal. The institutions and political relations must form a coherent local apparatus. Use previous_resident_fact_knowledge and each new resident population's exact shared_fact_ids as the authoritative identity projection for deciding whether every resident can ground an ordinary answer about local government; raw shared_knowledge elsewhere is canonical statement text, not a missing ID field. A question may select a civic domain but must not have forced its presupposed office, election, or answer into the candidate. Do not rewrite or complete the candidate; return verdicts only.\n\nREQUEST:\n{}\nEVIDENCE:\n{}\nCANDIDATE:\n{}", + "Independently verify the admitted civic apparatus. Judge meaning, not JSON shape. The public facts must actually explain current authority, selection or succession, public resources or revenue, and redress or appeal. The institutions and political relations must form a coherent local apparatus. Use previous_resident_fact_knowledge and each new resident population's exact shared_fact_ids as the authoritative identity projection for deciding whether every resident can ground an ordinary answer about local government; raw shared_knowledge elsewhere is canonical statement text, not a missing ID field. A question may select a civic domain but must not have forced its presupposed office, election, or answer into the candidate. Compare complete_candidate with peer_civic_apparatuses. A reused political form passes only when shared history or material dependence explains it; redress may be exit, restitution, mediation, ritual appeal, patronage, strike, refusal, or another locally effective path rather than a tribunal. Confirm opposed populations and institutions possess different executable means in their capabilities, resources, relationships, and operating posture, not merely different notices. Confirm constituencies cross ancestry, species, and jurisdictional boundaries where the setting supports it; a locally homogeneous constituency passes only when the candidate gives a material reason rather than using ancestry or species as its civic partition by default. Separately confirm every explicit REQUEST constraint is satisfied: a request that requires a mixed, diasporic, or cross-border constituency cannot be waived by a good explanation for local homogeneity, and exact requested counts or forms remain exact. Reject public population or institution names that repeat an overused surname, epithet, grammatical mold, bureaucratic construction, or lightly varied quota template across this candidate or peer civic apparatuses unless shared history or material dependence grounds the resemblance. Inspect every human-readable string field in complete_candidate, excluding stable IDs and schema tags. Reject clipped words, unfinished sentences, placeholder fragments, and corrupted prose in facts, names, goals, pressures, capabilities, knowledge, resources, posture, persistent features, route distances, facet values, information channels, and relation descriptions. Do not rewrite or complete the candidate; return verdicts only.\n\nREQUEST:\n{}\nEVIDENCE:\n{}\nCANDIDATE:\n{}", destination_request, evidence, candidate, ); let schema = match serde_json::to_value(schema_for!(CivicSystemVerification)) { @@ -3142,7 +3222,13 @@ impl WorldCompiler { && verdict.public_resources_legible && verdict.redress_legible && verdict.institutional_relations_coherent - && verdict.resident_answer_grounded; + && verdict.resident_answer_grounded + && verdict.political_form_distinct_or_deliberately_shared + && verdict.opposed_interests_have_distinct_means + && verdict.constituencies_cross_boundaries_or_local_homogeneity_justified + && verdict.explicit_request_constraints_satisfied + && verdict.public_names_do_not_repeat_an_overused_template + && verdict.candidate_text_intact; if !accepted { let error = anyhow!( "destination civic verifier rejected the candidate: {}", @@ -3191,9 +3277,9 @@ impl WorldCompiler { } /// Independently verifies a complete titled-elaboration candidate without - /// granting the verifier any rewrite authority. The returned receipt is - /// rebound to the exact compiler candidate and can be consumed only by the - /// deterministic finalization boundary. + /// granting the verifier any rewrite authority. Accepted and rejected + /// receipts are bound to the exact compiler candidate before inference so + /// recovery cannot attach a verdict to another draft. pub async fn verify_titled_locality_elaboration( &self, campaign: &Campaign, @@ -3205,10 +3291,7 @@ impl WorldCompiler { validate_locality_elaboration(campaign, elaboration)?; let mut expansion = elaboration.expansion.clone(); let current_civic_context = civic_context(campaign, &elaboration.target_location_id); - let snapshot = format!( - "campaign:{}:revision:{}:titled-elaboration-verification", - campaign.id, campaign.revision - ); + let snapshot = titled_civic_verifier_binding(campaign, elaboration)?; match self .verify_destination_civic_system( campaign, @@ -3225,17 +3308,20 @@ impl WorldCompiler { .pop() .ok_or_else(|| anyhow!("civic verifier accepted without a receipt")), CivicSystemVerificationOutcome::Rejected { verdict, receipts } => { + let semantic_diagnostic = civic_verification_semantic_diagnostic(&verdict); Err(anyhow::Error::new(CivicElaborationVerificationFailure { message: format!( "titled elaboration civic verifier rejected the candidate: {}", verdict.rationale.trim() ), + semantic_diagnostic: Some(semantic_diagnostic), model_receipts: receipts, })) } CivicSystemVerificationOutcome::Failed { message, receipts } => { Err(anyhow::Error::new(CivicElaborationVerificationFailure { message, + semantic_diagnostic: None, model_receipts: receipts, })) } @@ -3387,12 +3473,27 @@ impl DestinationReconciliationAgentTool<'_> { } } CivicSystemVerificationOutcome::Rejected { verdict, receipts } => { - ModelAgentToolOutcome::Continue { - observation: self.observation( - applied_edits, - DestinationReconciliationFinding::CivicVerifier { verdict }, - ), - receipts, + if !verdict.candidate_text_intact + || !verdict.constituencies_cross_boundaries_or_local_homogeneity_justified + || !verdict.explicit_request_constraints_satisfied + || !verdict.public_names_do_not_repeat_an_overused_template + { + ModelAgentToolOutcome::Failed { + message: format!( + "full destination owner must regenerate the broad civic candidate: {}", + serde_json::to_string(&verdict) + .unwrap_or_else(|_| verdict.rationale.clone()) + ), + receipts, + } + } else { + ModelAgentToolOutcome::Continue { + observation: self.observation( + applied_edits, + DestinationReconciliationFinding::CivicVerifier { verdict }, + ), + receipts, + } } } CivicSystemVerificationOutcome::Failed { message, receipts } => { @@ -4131,6 +4232,28 @@ fn validate_branch_assumptions(assumptions: &[String]) -> Result<()> { Ok(()) } +fn validate_compiled_destination_population_names( + campaign: &Campaign, + populations: &[CompiledDestinationPopulation], +) -> Result<()> { + let established = campaign + .gestalts + .values() + .map(|population| crate::resolution::public_identity_key(&population.name)) + .collect::>(); + let mut candidate = BTreeSet::new(); + for population in populations { + let key = crate::resolution::public_identity_key(&population.name); + if key.is_empty() || established.contains(&key) || !candidate.insert(key) { + return Err(anyhow!( + "destination population {} needs a distinct public population identity", + population.id + )); + } + } + Ok(()) +} + fn material_gap_text(gap: &CompiledMaterialGap) -> String { format!( "{} Affected approved premise: {} Approval must decide: {}", @@ -5131,6 +5254,41 @@ fn civic_context(campaign: &Campaign, location_id: &str) -> Option Vec { + campaign + .civic_systems + .values() + .filter(|system| system.jurisdiction_location_id != excluded_location_id) + .take(8) + .map(|system| { + serde_json::json!({ + "jurisdiction_location_id":system.jurisdiction_location_id, + "jurisdiction_name":campaign.locations.get(&system.jurisdiction_location_id).map(|location|location.name.as_str()), + "institutions":system.governing_institution_ids.iter().filter_map(|id|campaign.institutions.get(id)).map(|institution|serde_json::json!({ + "id":institution.id, + "name":institution.name, + "goals":institution.goals, + "resources":institution.resources, + "posture":institution.posture, + })).collect::>(), + "resident_populations":system.resident_population_ids.iter().filter_map(|id|campaign.gestalts.get(id)).map(|population|serde_json::json!({ + "id":population.id, + "name":population.name, + "goals":population.goals, + "pressures":population.pressures, + })).collect::>(), + "public_facts":system.public_authority_fact_ids.iter() + .chain(system.public_selection_fact_ids.iter()) + .chain(system.public_resource_fact_ids.iter()) + .chain(system.public_redress_fact_ids.iter()) + .filter_map(|id|campaign.facts.get(id)) + .map(|fact|fact.statement.as_str()).collect::>(), + "political_relations":system.political_relation_ids.iter().filter_map(|id|campaign.agency_relations.get(id)).collect::>(), + }) + }) + .collect() +} + pub(crate) fn civic_candidate_digest(expansion: &crate::domain::RegionExpansion) -> Result { let mut candidate = expansion.clone(); if let Some(system) = &mut candidate.civic_system { @@ -5149,6 +5307,17 @@ pub(crate) fn civic_verifier_binding(campaign: &Campaign, candidate_digest: &str ) } +pub fn titled_civic_verifier_binding( + campaign: &Campaign, + elaboration: &LocalityElaboration, +) -> Result { + validate_locality_elaboration(campaign, elaboration)?; + Ok(civic_verifier_binding( + campaign, + &civic_candidate_digest(&elaboration.expansion)?, + )) +} + pub(crate) fn validate_civic_admission_receipts( campaign: &Campaign, expansion: &crate::domain::RegionExpansion, @@ -5329,7 +5498,6 @@ pub fn validate_region_expansion( )); } } - let mut attached = false; for location in &expansion.locations { if location.name.trim().is_empty() || location @@ -5361,14 +5529,8 @@ pub fn validate_region_expansion( { return Err(anyhow!("destination expansion has a dangling route")); } - if route.destination_id == expansion.origin_location_id { - attached = true; - } } } - if !attached { - return Err(anyhow!("destination expansion is not attached to origin")); - } for location in &expansion.locations { let mut seen = BTreeSet::from([location.id.as_str()]); let mut cursor = location.container_id.as_deref(); @@ -5389,6 +5551,78 @@ pub fn validate_region_expansion( }); } } + let mut directed_routes = BTreeMap::>::new(); + for location in campaign.locations.values() { + directed_routes + .entry(location.id.clone()) + .or_default() + .extend( + location + .routes + .values() + .map(|route| route.destination_id.clone()), + ); + } + directed_routes + .entry(expansion.origin_location_id.clone()) + .or_default() + .extend( + expansion + .origin_routes + .values() + .map(|route| route.destination_id.clone()), + ); + for location in &expansion.locations { + directed_routes + .entry(location.id.clone()) + .or_default() + .extend( + location + .routes + .values() + .map(|route| route.destination_id.clone()), + ); + } + let reachable_from = |routes: &BTreeMap>, start: &str| { + let mut reachable = BTreeSet::from([start.to_owned()]); + let mut frontier = std::collections::VecDeque::from([start.to_owned()]); + while let Some(origin) = frontier.pop_front() { + for destination in routes.get(&origin).into_iter().flatten() { + if reachable.insert(destination.clone()) { + frontier.push_back(destination.clone()); + } + } + } + reachable + }; + let outward_reachable = reachable_from(&directed_routes, &expansion.origin_location_id); + let mut reverse_routes = BTreeMap::>::new(); + for (origin, destinations) in &directed_routes { + for destination in destinations { + reverse_routes + .entry(destination.clone()) + .or_default() + .insert(origin.clone()); + } + } + let return_reachable = reachable_from(&reverse_routes, &expansion.origin_location_id); + let unreachable_outward = new_ids + .iter() + .filter(|id| !outward_reachable.contains(**id)) + .copied() + .collect::>(); + let unreachable_return = new_ids + .iter() + .filter(|id| !return_reachable.contains(**id)) + .copied() + .collect::>(); + if !unreachable_outward.is_empty() || !unreachable_return.is_empty() { + return Err(anyhow!( + "destination expansion does not provide directed origin reachability for every new location; unreachable outward: {:?}; unreachable return: {:?}", + unreachable_outward, + unreachable_return + )); + } if let Some(finding) = destination_fact_findings(campaign, &expansion.facts).first() { return Err(anyhow!( "destination expansion fact namespace rejected candidate: {}", @@ -5436,6 +5670,24 @@ pub fn validate_region_expansion( "destination populations need at most eight unique new canonical subject IDs" )); } + let established_population_names = campaign + .gestalts + .values() + .map(|population| crate::resolution::public_identity_key(&population.name)) + .collect::>(); + let mut candidate_population_names = BTreeSet::new(); + for population in &expansion.populations { + let name_key = crate::resolution::public_identity_key(&population.name); + if name_key.is_empty() + || established_population_names.contains(&name_key) + || !candidate_population_names.insert(name_key) + { + return Err(anyhow!( + "destination population {} needs a distinct public population identity", + population.id + )); + } + } let profile_ids = expansion .population_profiles .iter() @@ -6829,6 +7081,78 @@ pub fn validate_campaign_runtime(c: &Campaign) -> Result<()> { validate_campaign(c, false) } +fn validate_campaign_public_identity_names(campaign: &Campaign) -> Result<()> { + let mut projected_member_actor_ids = BTreeSet::new(); + for member in campaign.gestalt_members.values() { + let default_actor_id = crate::domain::gestalt_member_subject_id(&member.id); + if campaign.actors.contains_key(&default_actor_id) { + projected_member_actor_ids.insert(default_actor_id); + } + if let Some(actor_id) = &member.materialized_actor_id { + projected_member_actor_ids.insert(actor_id.clone()); + if let Some(actor) = campaign.actors.get(actor_id) + && crate::resolution::public_identity_key(&actor.name) + != crate::resolution::public_identity_key(&member.name) + { + return Err(anyhow!( + "materialized member {} and Actor {} disagree on public identity", + member.id, + actor.id + )); + } + } + } + + let mut person_names = BTreeMap::::new(); + let person_identities = campaign + .actors + .values() + .filter(|actor| !projected_member_actor_ids.contains(&actor.id)) + .map(|actor| (format!("actor:{}", actor.id), actor.name.as_str())) + .chain( + campaign + .gestalt_members + .values() + .map(|member| (format!("member:{}", member.id), member.name.as_str())), + ); + for (subject_id, name) in person_identities { + let key = crate::resolution::public_identity_key(name); + if key.is_empty() { + return Err(anyhow!( + "public person identity {subject_id} has no identity characters" + )); + } + if let Some(existing) = person_names.insert(key, subject_id.clone()) { + return Err(anyhow!( + "public person identities {existing} and {subject_id} collide" + )); + } + } + + let mut population_names = BTreeMap::::new(); + let residual_population_ids = crate::elaboration::residual_gestalt_ids(campaign); + for population in campaign + .gestalts + .values() + .filter(|population| !residual_population_ids.contains(&population.id)) + { + let key = crate::resolution::public_identity_key(&population.name); + if key.is_empty() { + return Err(anyhow!( + "public population identity {} has no identity characters", + population.id + )); + } + if let Some(existing) = population_names.insert(key, population.id.clone()) { + return Err(anyhow!( + "public population identities {existing} and {} collide", + population.id + )); + } + } + Ok(()) +} + fn validate_campaign(c: &Campaign, fresh_seed: bool) -> Result<()> { if c.tick_hours == 0 { return Err(anyhow!("strategic tick duration must be positive")); @@ -6839,6 +7163,7 @@ fn validate_campaign(c: &Campaign, fresh_seed: bool) -> Result<()> { { return Err(anyhow!("primary controlled subject is missing")); } + validate_campaign_public_identity_names(c)?; crate::resolution::validate_policy(&c.resolution_policy)?; crate::resolution::validate_pins(c, &c.resolution_pins)?; if let Some(institution) = c.institutions.values().find(|institution| { @@ -8253,6 +8578,12 @@ mod tests { "redress_legible":accepted, "institutional_relations_coherent":accepted, "resident_answer_grounded":accepted, + "political_form_distinct_or_deliberately_shared":accepted, + "opposed_interests_have_distinct_means":accepted, + "constituencies_cross_boundaries_or_local_homogeneity_justified":accepted, + "explicit_request_constraints_satisfied":accepted, + "public_names_do_not_repeat_an_overused_template":accepted, + "candidate_text_intact":accepted, "rationale":if accepted { "The refuge facts and institutions form a legible civic apparatus grounded in every resident population." } else { @@ -8574,6 +8905,12 @@ mod tests { "redress_legible":true, "institutional_relations_coherent":true, "resident_answer_grounded":true, + "political_form_distinct_or_deliberately_shared":true, + "opposed_interests_have_distinct_means":true, + "constituencies_cross_boundaries_or_local_homogeneity_justified":true, + "explicit_request_constraints_satisfied":true, + "public_names_do_not_repeat_an_overused_template":true, + "candidate_text_intact":true, "rationale":"Harrow's exact public facts explain its divided authority, ballot, revenue, and appeal path to every resident population." }) .to_string()), @@ -9744,8 +10081,69 @@ mod tests { ); } + #[test] + fn destination_population_names_are_unique_in_the_public_population_namespace() { + let mut campaign = seed_to_campaign(private_actor_test_seed(), &[]).unwrap(); + campaign.gestalts.insert( + "ledger-keepers".into(), + GestaltPersonaState { + schema: "ghostlight.gestalt_persona_state.v1".into(), + id: "ledger-keepers".into(), + name: "Ledger-Keepers".into(), + version: 0, + home_location_id: "convoy-staging".into(), + shared_capabilities: BTreeSet::new(), + shared_knowledge: BTreeSet::new(), + resources: BTreeSet::new(), + goals: Vec::new(), + pressures: Vec::new(), + }, + ); + let population = |id: &str, name: &str| CompiledDestinationPopulation { + id: id.into(), + name: name.into(), + home_location_id: "refuge".into(), + shared_capabilities: BTreeSet::new(), + shared_fact_ids: BTreeSet::new(), + resources: BTreeSet::new(), + goals: vec!["keep the crossing usable".into()], + pressures: Vec::new(), + collective_authority_id: None, + facets: BTreeMap::>::new().into(), + information_channels: BTreeSet::new(), + }; + + assert!( + validate_compiled_destination_population_names( + &campaign, + &[population("new", "ledger keepers")] + ) + .unwrap_err() + .to_string() + .contains("distinct public population identity") + ); + assert!( + validate_compiled_destination_population_names( + &campaign, + &[ + population("one", "River Hands"), + population("two", "river-hands"), + ] + ) + .is_err() + ); + validate_compiled_destination_population_names( + &campaign, + &[ + population("one", "River Hands"), + population("two", "Stone Sounders"), + ], + ) + .unwrap(); + } + #[tokio::test] - async fn destination_agent_owns_the_reattempt_after_one_rejected_civic_verdict() { + async fn destination_full_owner_regenerates_after_one_rejected_civic_verdict() { let model = Arc::new(DestinationElaborationModel { saw_branch_assumption_boundary: AtomicBool::new(false), saw_balanced_civic_reconciliation: AtomicBool::new(false), @@ -9778,26 +10176,25 @@ mod tests { .iter() .filter(|receipt| receipt.stage == "destination_reconciliation_agent_action") .collect::>(); + let compile_receipts = receipts + .iter() + .filter(|receipt| receipt.stage == "destination_compile") + .collect::>(); assert_eq!(model.civic_verification_calls.load(Ordering::SeqCst), 2); assert_eq!(verification_receipts.len(), 2); - assert_eq!(reconciliation_receipts.len(), 1); + assert_eq!(compile_receipts.len(), 2); + assert!(reconciliation_receipts.is_empty()); assert_eq!( verification_receipts[0].validation_result, "semantic_invalid" ); assert_eq!(verification_receipts[1].validation_result, "valid"); - assert!( - reconciliation_receipts[0] - .source_receipt_ids - .iter() - .any(|source| source == verification_receipts[0].storage_key()) - ); assert!( verification_receipts[1] .source_receipt_ids .iter() - .any(|source| source == reconciliation_receipts[0].storage_key()) + .any(|source| source == compile_receipts[1].storage_key()) ); assert_eq!( preview @@ -9870,6 +10267,35 @@ mod tests { assert_eq!(rationale["maxLength"], 1_000); } + #[test] + fn civic_repair_diagnostic_leads_with_every_failed_dimension() { + let verdict = CivicSystemVerification { + authority_legible: true, + selection_or_succession_legible: true, + public_resources_legible: true, + redress_legible: true, + institutional_relations_coherent: true, + resident_answer_grounded: true, + political_form_distinct_or_deliberately_shared: true, + opposed_interests_have_distinct_means: true, + constituencies_cross_boundaries_or_local_homogeneity_justified: false, + explicit_request_constraints_satisfied: true, + public_names_do_not_repeat_an_overused_template: true, + candidate_text_intact: false, + rationale: "The late civic dimensions failed despite an otherwise legible apparatus." + .into(), + }; + + let diagnostic = civic_verification_semantic_diagnostic(&verdict); + + assert!(diagnostic.starts_with("{\"failed_checks\":[")); + assert!( + diagnostic.contains("constituencies_cross_boundaries_or_local_homogeneity_justified") + ); + assert!(diagnostic.contains("candidate_text_intact")); + assert!(!diagnostic.contains("authority_legible")); + } + #[test] fn destination_reconciliation_edit_batch_is_transactional() { let mut seed = civic_reconciliation_seed(vec![]); @@ -10045,7 +10471,7 @@ mod tests { } #[tokio::test] - async fn destination_civic_rejection_returns_to_one_balanced_agent_owner() { + async fn repeated_broad_civic_rejection_never_enters_the_narrow_reconciler() { let model = Arc::new(DestinationElaborationModel { saw_branch_assumption_boundary: AtomicBool::new(false), saw_balanced_civic_reconciliation: AtomicBool::new(false), @@ -10059,75 +10485,48 @@ mod tests { seed.opening_narration = "The convoy waits in the rain.".into(); let campaign = seed_to_campaign(seed, &[]).unwrap(); - let (_, receipts) = compiler + let error = compiler .compile_destination( &campaign, "convoy-staging", "a playable storm refuge with ordinary repair and admission procedure", ) .await - .unwrap(); - let civic_receipts = receipts + .unwrap_err(); + let failure = error + .downcast_ref::() + .expect("terminal destination failure preserves model receipts"); + let civic_receipts = failure + .model_receipts .iter() .filter(|receipt| receipt.stage == "destination_civic_verification") .collect::>(); - let compile_receipts = receipts + let compile_receipts = failure + .model_receipts .iter() .filter(|receipt| receipt.stage == "destination_compile") .collect::>(); - let reconciliation_receipts = receipts + let reconciliation_receipts = failure + .model_receipts .iter() .filter(|receipt| receipt.stage == "destination_reconciliation_agent_action") .collect::>(); assert!( - model + !model .saw_balanced_civic_reconciliation .load(Ordering::SeqCst) ); - assert_eq!(compile_receipts.len(), 1); - assert_eq!(reconciliation_receipts.len(), 2); - assert!( - reconciliation_receipts - .iter() - .all(|receipt| receipt.model == MODEL_BALANCED) - ); - assert_eq!(civic_receipts.len(), 3); - assert!( - reconciliation_receipts[0] - .source_receipt_ids - .iter() - .any(|source| source == compile_receipts[0].storage_key()) - ); - assert!( - reconciliation_receipts[0] - .source_receipt_ids - .iter() - .any(|source| source == civic_receipts[0].storage_key()) - ); - assert!( - civic_receipts[1] - .source_receipt_ids - .iter() - .any(|source| source == reconciliation_receipts[0].storage_key()) - ); - assert!( - reconciliation_receipts[1] - .source_receipt_ids - .iter() - .any(|source| source == civic_receipts[1].storage_key()) - ); - assert!( - civic_receipts[2] - .source_receipt_ids - .iter() - .any(|source| source == reconciliation_receipts[1].storage_key()) - ); + assert_eq!(compile_receipts.len(), 2); + assert!(reconciliation_receipts.is_empty()); + assert_eq!(civic_receipts.len(), 2); assert_eq!(civic_receipts[0].validation_result, "semantic_invalid"); assert_eq!(civic_receipts[1].validation_result, "semantic_invalid"); - assert_eq!(civic_receipts[2].validation_result, "valid"); - assert_eq!(reconciliation_receipts[0].validation_result, "valid"); - assert_eq!(reconciliation_receipts[1].validation_result, "valid"); + assert!( + error + .to_string() + .contains("destination civic verifier rejected the regenerated candidate") + ); } #[tokio::test] @@ -10407,7 +10806,7 @@ mod tests { saw_branch_assumption_boundary: AtomicBool::new(false), saw_balanced_civic_reconciliation: AtomicBool::new(false), civic_verification_calls: AtomicUsize::new(0), - civic_verification_rejections: 2, + civic_verification_rejections: 0, reconciliation_steals_non_civic_fact: true, }); let compiler = WorldCompiler::new(vault(), model.clone(), "flash", "pro"); @@ -10430,7 +10829,7 @@ mod tests { .compile_destination( &campaign, "convoy-staging", - "a playable storm refuge with ordinary repair and admission procedure", + "a candidate fact collision fixture with ordinary repair and admission procedure", ) .await .unwrap_err(); @@ -10461,7 +10860,7 @@ mod tests { .iter() .any(|source| source == pair[0].storage_key()) })); - assert_eq!(model.civic_verification_calls.load(Ordering::SeqCst), 1); + assert_eq!(model.civic_verification_calls.load(Ordering::SeqCst), 0); } #[tokio::test] @@ -10489,9 +10888,9 @@ mod tests { .unwrap_err(); assert!( - error.to_string().contains( - "model agent destination_reconciliation_agent_action exhausted 4 semantic steps" - ) && error.to_string().contains("civic_verifier_rejection"), + error + .to_string() + .contains("destination civic verifier rejected the regenerated candidate"), "{error:#}" ); let failure = error @@ -10512,23 +10911,17 @@ mod tests { .iter() .filter(|receipt| receipt.stage == "destination_reconciliation_agent_action") .collect::>(); - assert_eq!(compile_receipts.len(), 1); - assert_eq!(reconciliation_receipts.len(), 4); - assert_eq!(civic_receipts.len(), 5); + assert_eq!(compile_receipts.len(), 2); + assert!(reconciliation_receipts.is_empty()); + assert_eq!(civic_receipts.len(), 2); assert!( civic_receipts .iter() .all(|receipt| receipt.validation_result == "semantic_invalid") ); - assert_eq!(model.civic_verification_calls.load(Ordering::SeqCst), 5); - assert!(reconciliation_receipts.windows(2).all(|pair| { - pair[1] - .source_receipt_ids - .iter() - .any(|source| source == pair[0].storage_key()) - })); + assert_eq!(model.civic_verification_calls.load(Ordering::SeqCst), 2); assert!( - model + !model .saw_balanced_civic_reconciliation .load(Ordering::SeqCst) ); @@ -11524,7 +11917,27 @@ mod tests { } #[test] - fn inhabited_expansion_admits_only_reachable_population_migration_edges() { + fn region_expansion_rejects_a_disconnected_contained_location() { + let campaign = crate::resolution::tests::campaign(0, 1); + let mut expansion = valid_region_expansion(); + expansion.locations.push(Location { + id: "sealed-gallery".into(), + name: "Sealed Gallery".into(), + container_id: Some("annex".into()), + routes: BTreeMap::new(), + persistent_features: vec!["a locked internal gallery".into()], + }); + + let error = validate_region_expansion(&campaign, &expansion) + .unwrap_err() + .to_string(); + + assert!(error.contains("directed origin reachability")); + assert!(error.contains("sealed-gallery")); + } + + #[test] + fn inhabited_expansion_requires_connected_population_migration_topology() { let mut campaign = crate::resolution::tests::campaign(0, 1); campaign.gestalts.insert( "refugees".into(), @@ -11647,7 +12060,7 @@ mod tests { validate_region_expansion(&campaign, &expansion) .unwrap_err() .to_string() - .contains("strategic travel horizon") + .contains("directed origin reachability") ); } diff --git a/crates/ghostlight-dungeon/src/domain.rs b/crates/ghostlight-dungeon/src/domain.rs index a898847..97729ac 100644 --- a/crates/ghostlight-dungeon/src/domain.rs +++ b/crates/ghostlight-dungeon/src/domain.rs @@ -1236,6 +1236,20 @@ pub enum WorldCommand { #[serde(default)] model_stage_receipts: Vec, }, + ElaborateGestaltFission { + expected_revision: u64, + preview: GestaltFissionPreview, + qualification: crate::elaboration::WorldComplexityFissionQualification, + #[serde(default)] + model_stage_receipts: Vec, + }, + ElaborateGestaltIndividuation { + expected_revision: u64, + individuation: GestaltIndividuation, + qualification: crate::elaboration::WorldComplexityIndividuationQualification, + #[serde(default)] + model_stage_receipts: Vec, + }, ReconcileFissionCivicBindings { expected_revision: u64, }, diff --git a/crates/ghostlight-dungeon/src/elaboration.rs b/crates/ghostlight-dungeon/src/elaboration.rs index 1ed11ce..4aa8ce3 100644 --- a/crates/ghostlight-dungeon/src/elaboration.rs +++ b/crates/ghostlight-dungeon/src/elaboration.rs @@ -27,6 +27,13 @@ impl WorldScaleIntent { target_active_cover_basis_points: 1_000, } } + + pub fn twenty_percent() -> Self { + Self { + schema: "ghostlight.world_scale_intent.v1".into(), + target_active_cover_basis_points: 2_000, + } + } } /// Deterministic elaboration pressure derived from scale intent and current @@ -48,20 +55,91 @@ pub struct WorldElaborationDemand { pub realm_subject_targets: BTreeMap, } -/// Counts canonical active agency leaves that could own an action. Dormant -/// member records and census texture do not satisfy world-complexity demand; -/// an individual counts only after grounded elaboration promotes them into an -/// Actor leaf, and a population subdivision counts only after admitted fission -/// creates active child Gestalts. +pub fn residual_gestalt_ids(campaign: &crate::domain::Campaign) -> BTreeSet { + campaign + .gestalt_lineages + .values() + .map(|lineage| lineage.residual_child_id.clone()) + .collect() +} + +fn semantically_qualified_fission_child( + campaign: &crate::domain::Campaign, + child_id: &str, + parent_id: &str, +) -> bool { + let Some(child) = campaign.gestalts.get(child_id) else { + return false; + }; + let Some(parent) = campaign.gestalts.get(parent_id) else { + return false; + }; + if !child + .shared_capabilities + .is_superset(&parent.shared_capabilities) + || !child.shared_knowledge.is_superset(&parent.shared_knowledge) + || !child.goals.starts_with(&parent.goals) + || !child.pressures.starts_with(&parent.pressures) + { + return false; + } + [ + child.shared_capabilities.len() > parent.shared_capabilities.len(), + child.shared_knowledge.len() > parent.shared_knowledge.len(), + child.goals.len() > parent.goals.len(), + child.pressures.len() > parent.pressures.len(), + ] + .into_iter() + .filter(|present| *present) + .count() + >= 2 +} + +/// Returns active action owners whose current state can justify complexity +/// credit. Compiler-admitted roots and individuated people are admitted by +/// their owning gates. A fission child must additionally retain two or more +/// semantic dimensions beyond its exact parent; a merely relabelled ordinary +/// split remains simulatable but cannot pay the world-size bill. +pub fn canonical_actionable_subject_ids(campaign: &crate::domain::Campaign) -> BTreeSet { + let residual_ids = residual_gestalt_ids(campaign); + let fission_parents = campaign + .gestalt_lineages + .values() + .flat_map(|lineage| { + lineage + .child_gestalt_ids + .iter() + .map(move |child_id| (child_id.clone(), lineage.parent_gestalt_id.clone())) + }) + .collect::>(); + campaign + .agency_profiles + .values() + .filter(|profile| { + profile.active_leaf + && profile.simulation_eligible + && !residual_ids.contains(&profile.subject_id) + && fission_parents + .get(&profile.subject_id) + .is_none_or(|parent_id| { + semantically_qualified_fission_child( + campaign, + &profile.subject_id, + parent_id, + ) + }) + }) + .map(|profile| profile.subject_id.clone()) + .collect() +} + +/// Counts qualified active agency leaves that could own an action. Dormant +/// member records, census texture, and unresolved residual populations do not +/// satisfy world-complexity demand. Residuals preserve unresolved lineage and +/// membership state, but are not simulation-eligible action owners and cannot +/// be recursively split for quota. pub fn canonical_actionable_subject_count(campaign: &crate::domain::Campaign) -> u32 { - u32::try_from( - campaign - .agency_profiles - .values() - .filter(|profile| profile.active_leaf && profile.simulation_eligible) - .count(), - ) - .unwrap_or(u32::MAX) + u32::try_from(canonical_actionable_subject_ids(campaign).len()).unwrap_or(u32::MAX) } /// Resolve exact operating places through canonical containment to one @@ -570,8 +648,10 @@ pub async fn compact_elaborator_session( "target location {target_location_id:?} is absent from the canonical campaign" )); } - if journal.is_empty() { - findings.push("journal must contain at least one admitted mutation".to_owned()); + if journal.is_empty() && recent_rejection_findings.is_empty() { + findings.push( + "compaction requires an admitted mutation or a semantic rejection finding".to_owned(), + ); } if journal.len() > 64 { findings.push(format!( @@ -608,6 +688,20 @@ pub async fn compact_elaborator_session( )); } } + if recent_rejection_findings.len() > 32 { + findings.push(format!( + "recent_rejection_findings contains {} entries; the maximum is 32", + recent_rejection_findings.len() + )); + } + for (index, finding) in recent_rejection_findings.iter().enumerate() { + let length = finding.trim().chars().count(); + if length == 0 || length > 800 { + findings.push(format!( + "recent_rejection_findings[{index}] contains {length} characters; the permitted range is 1 through 800" + )); + } + } if !findings.is_empty() { return Err(anyhow!( "elaborator session compaction preflight failed: {}", @@ -1313,7 +1407,7 @@ impl ModelWorldElaborationWorker { /// Provider-backed worker for one causally meaningful Gestalt subdivision. /// A round assigns distinct active parents against one frozen campaign. The /// worker proposes only; callers must revalidate and commit each preview -/// through `WorldKernel::FissionGestalt`. +/// through `WorldKernel::ElaborateGestaltFission`. pub struct ModelWorldComplexityWorker { model: Arc, campaign: Arc, @@ -1321,6 +1415,7 @@ pub struct ModelWorldComplexityWorker { first_dispatch_ordinal: u64, parent_gestalt_ids: Vec, parent_jurisdiction_ids: BTreeMap, + target_actionable_gains: BTreeMap, session_checkpoints: BTreeMap, } @@ -1331,6 +1426,7 @@ impl ModelWorldComplexityWorker { first_dispatch_ordinal: u64, parent_gestalt_ids: Vec, parent_jurisdiction_ids: BTreeMap, + actionable_subject_deficit: u32, session_checkpoints: BTreeMap, ) -> Result { if parent_gestalt_ids.is_empty() @@ -1340,6 +1436,14 @@ impl ModelWorldComplexityWorker { "complexity round requires distinct assigned parent Gestalts" )); } + if actionable_subject_deficit == 0 + || u32::try_from(parent_gestalt_ids.len()).unwrap_or(u32::MAX) + > actionable_subject_deficit + { + return Err(anyhow!( + "complexity round cannot dispatch more parents than its remaining subject deficit" + )); + } for parent_id in &parent_gestalt_ids { let profile = campaign.agency_profiles.get(parent_id); if !campaign.gestalts.contains_key(parent_id) @@ -1377,6 +1481,26 @@ impl ModelWorldComplexityWorker { "complexity round session jurisdictions do not match assigned parents" )); } + let planned_gain = actionable_subject_deficit.min( + u32::try_from(parent_gestalt_ids.len()) + .unwrap_or(u32::MAX) + .saturating_mul(6), + ); + let parent_count = u32::try_from(parent_gestalt_ids.len()).unwrap_or(u32::MAX); + let base_gain = planned_gain / parent_count; + let remainder = planned_gain % parent_count; + let target_actionable_gains = parent_gestalt_ids + .iter() + .enumerate() + .map(|(index, _)| { + let index = u32::try_from(index).unwrap_or(u32::MAX); + let gain = base_gain.saturating_add(u32::from(index < remainder)); + Ok(( + first_dispatch_ordinal.saturating_add(u64::from(index)), + u8::try_from(gain.clamp(1, 6))?, + )) + }) + .collect::, std::num::TryFromIntError>>()?; for (session_id, checkpoint) in &session_checkpoints { if checkpoint.session_id != *session_id || elaborator_session_id(checkpoint.title, &checkpoint.target_location_id) @@ -1405,6 +1529,7 @@ impl ModelWorldComplexityWorker { first_dispatch_ordinal, parent_gestalt_ids, parent_jurisdiction_ids, + target_actionable_gains, session_checkpoints, }) } @@ -1446,7 +1571,9 @@ enum WorldComplexityOperation { fn complexity_operation(title: ElaboratorTitle) -> WorldComplexityOperation { match title { - ElaboratorTitle::Hearth | ElaboratorTitle::Veil => WorldComplexityOperation::Individuate, + ElaboratorTitle::Hearth | ElaboratorTitle::Tangle | ElaboratorTitle::Veil => { + WorldComplexityOperation::Individuate + } _ => WorldComplexityOperation::Fission, } } @@ -1460,12 +1587,49 @@ fn complexity_operation(title: ElaboratorTitle) -> WorldComplexityOperation { pub enum WorldComplexityProposal { Fission { preview: crate::domain::GestaltFissionPreview, + qualification: WorldComplexityFissionQualification, }, Individuate { individuation: crate::domain::GestaltIndividuation, + qualification: WorldComplexityIndividuationQualification, }, } +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] +#[serde(deny_unknown_fields)] +pub struct WorldComplexitySemanticQualification { + pub schema: String, + pub identity_namespace: String, + pub frozen_campaign_id: Uuid, + pub frozen_world_revision: u64, + pub established_names_digest: String, + pub semantic_context_digest: String, + pub owner_lane_digest: String, + pub candidate_digest: String, + pub semantic_verification_binding: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct WorldComplexityFissionQualification { + pub schema: String, + pub title: ElaboratorTitle, + pub jurisdiction_location_id: String, + pub target_actionable_gain: u8, + #[serde(default)] + pub semantic: WorldComplexitySemanticQualification, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct WorldComplexityIndividuationQualification { + pub schema: String, + pub title: ElaboratorTitle, + pub jurisdiction_location_id: String, + #[serde(default)] + pub semantic: WorldComplexitySemanticQualification, +} + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] #[serde(deny_unknown_fields)] struct WorldComplexityAction { @@ -1493,6 +1657,11 @@ struct WorldComplexityChildDraft { id: String, name: String, partition_value: String, + home_location_id: String, + capability_additions: BTreeSet, + knowledge_additions: BTreeSet, + goals_add: Vec, + pressures_add: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] @@ -1534,15 +1703,22 @@ impl From for WorldComplexityMemberDraft { impl WorldComplexityProposal { pub fn parent_gestalt_id(&self) -> &str { match self { - Self::Fission { preview } => &preview.parent_gestalt_id, - Self::Individuate { individuation } => &individuation.gestalt_id, + Self::Fission { preview, .. } => &preview.parent_gestalt_id, + Self::Individuate { individuation, .. } => &individuation.gestalt_id, } } pub fn mutation_kind(&self) -> &'static str { match self { - Self::Fission { .. } => "fission_gestalt", - Self::Individuate { .. } => "individuate_gestalt_member", + Self::Fission { .. } => "elaborate_gestalt_fission", + Self::Individuate { .. } => "elaborate_gestalt_individuation", + } + } + + pub fn expected_world_revision(&self) -> u64 { + match self { + Self::Fission { preview, .. } => preview.expected_world_revision, + Self::Individuate { qualification, .. } => qualification.semantic.frozen_world_revision, } } } @@ -1581,15 +1757,33 @@ pub fn rebase_world_complexity_proposal( )); } match &mut proposal { - WorldComplexityProposal::Fission { preview } => { + WorldComplexityProposal::Fission { + preview, + qualification, + } => { + if qualification.schema != "ghostlight.world_complexity_fission_qualification.v1" + || !(1..=6).contains(&qualification.target_actionable_gain) + { + return Err(anyhow!( + "complexity fission qualification is unsupported or out of range" + )); + } if preview.campaign_id != current.id { return Err(anyhow!("complexity fission belongs to another campaign")); } preview.expected_world_revision = current.revision; reconcile_fission_child_id_collisions(current, preview)?; - crate::resolution::validate_fission(current, preview)?; + crate::resolution::validate_elaborated_fission(current, preview)?; + validate_complexity_fission_semantics( + current, + ¤t.gestalts[&preview.parent_gestalt_id], + preview, + qualification.title, + &qualification.jurisdiction_location_id, + qualification.target_actionable_gain, + )?; } - WorldComplexityProposal::Individuate { individuation } => { + WorldComplexityProposal::Individuate { individuation, .. } => { crate::resolution::validate_gestalt_individuation(current, individuation)?; } } @@ -1602,7 +1796,7 @@ fn reconcile_fission_child_id_collisions( ) -> Result<()> { let mut replacements = BTreeMap::new(); for child in &preview.children { - if current.gestalts.contains_key(&child.id) { + if crate::resolution::canonical_subject_id_is_occupied(current, &child.id) { let digest = crate::legacy_transition::digest_serializable(&( "ghostlight.fission_child_id.v1", current.id, @@ -1616,7 +1810,7 @@ fn reconcile_fission_child_id_collisions( .take(16) .collect::(); let replacement = format!("{}:fission:{suffix}", preview.parent_gestalt_id); - if current.gestalts.contains_key(&replacement) + if crate::resolution::canonical_subject_id_is_occupied(current, &replacement) || preview .children .iter() @@ -1673,14 +1867,697 @@ struct WorldComplexityFinding { diagnostic: String, } +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct WorldComplexitySemanticVerification { + pub public_names_are_legible_identifiers: bool, + pub names_do_not_repeat_an_overused_template: bool, + pub cultural_resemblance_is_grounded_not_quota_cloning: bool, + pub causal_additions_are_materially_distinct: bool, + pub causal_additions_do_not_repeat_an_overused_procedural_template: bool, + #[schemars(length(min = 1, max = 1_000))] + pub rationale: String, +} + struct WorldComplexityTool<'a> { campaign: &'a crate::domain::Campaign, parent_gestalt_id: &'a str, + title: ElaboratorTitle, operation: WorldComplexityOperation, partition_axis: crate::domain::AgencyAxis, + jurisdiction_location_id: &'a str, + target_actionable_gain: u8, workbench: serde_json::Value, } +pub fn locations_in_jurisdiction( + campaign: &crate::domain::Campaign, + jurisdiction_location_id: &str, +) -> Vec { + let jurisdictions = BTreeSet::from([jurisdiction_location_id.to_owned()]); + campaign + .locations + .keys() + .filter(|location_id| { + unique_containing_jurisdiction( + campaign, + &BTreeSet::from([(*location_id).clone()]), + &jurisdictions, + ) + .as_deref() + == Some(jurisdiction_location_id) + }) + .cloned() + .collect() +} + +fn semantic_text_key(value: &str) -> String { + value + .split_whitespace() + .flat_map(|part| part.chars()) + .filter(|character| character.is_alphanumeric()) + .flat_map(char::to_lowercase) + .collect() +} + +fn world_complexity_semantic_candidate_digest( + proposal: &WorldComplexityProposal, +) -> Result { + let candidate = match proposal { + WorldComplexityProposal::Fission { preview, .. } => serde_json::json!({ + "operation":"fission", + "parent_gestalt_id":preview.parent_gestalt_id, + "partition_axis":preview.partition_axis, + "specific_children":preview.children.iter() + .filter(|child|child.id != preview.residual_child_id) + .map(|child|serde_json::json!({ + "name":child.name, + "partition_value":preview.child_partition_values.get(&child.id), + "home_location_id":child.home_location_id, + "capabilities":child.shared_capabilities, + "knowledge":child.shared_knowledge, + "goals":child.goals, + "pressures":child.pressures, + })).collect::>(), + }), + WorldComplexityProposal::Individuate { individuation, .. } => serde_json::json!({ + "operation":"individuate", + "parent_gestalt_id":individuation.gestalt_id, + "location_id":individuation.location_id, + "member":individuation.member, + }), + }; + crate::legacy_transition::digest_serializable(&( + "ghostlight.world_complexity_semantic_candidate.v1", + candidate, + )) +} + +fn world_complexity_semantic_verification_binding( + semantic: &WorldComplexitySemanticQualification, +) -> Result { + crate::legacy_transition::digest_serializable(&( + "ghostlight.world_complexity_semantic_verification.v1", + semantic.frozen_campaign_id, + semantic.frozen_world_revision, + &semantic.identity_namespace, + &semantic.established_names_digest, + &semantic.semantic_context_digest, + &semantic.owner_lane_digest, + &semantic.candidate_digest, + )) +} + +fn world_complexity_owner_lane_digest(proposal: &WorldComplexityProposal) -> Result { + let lane = match proposal { + WorldComplexityProposal::Fission { qualification, .. } => serde_json::json!({ + "operation":"fission", + "title":qualification.title, + "jurisdiction_location_id":qualification.jurisdiction_location_id, + "target_actionable_gain":qualification.target_actionable_gain, + }), + WorldComplexityProposal::Individuate { qualification, .. } => serde_json::json!({ + "operation":"individuate", + "title":qualification.title, + "jurisdiction_location_id":qualification.jurisdiction_location_id, + }), + }; + crate::legacy_transition::digest_serializable(&( + "ghostlight.world_complexity_owner_lane.v1", + lane, + )) +} + +pub fn validate_world_complexity_semantic_qualification_shape( + proposal: &WorldComplexityProposal, + semantic: &WorldComplexitySemanticQualification, +) -> Result<()> { + let expected_namespace = match proposal { + WorldComplexityProposal::Fission { .. } => "population", + WorldComplexityProposal::Individuate { .. } => "person", + }; + if semantic.schema != "ghostlight.world_complexity_semantic_qualification.v1" + || semantic.identity_namespace != expected_namespace + || semantic.candidate_digest != world_complexity_semantic_candidate_digest(proposal)? + || semantic.established_names_digest.trim().is_empty() + || semantic.semantic_context_digest.trim().is_empty() + || semantic.owner_lane_digest != world_complexity_owner_lane_digest(proposal)? + || semantic.semantic_verification_binding + != world_complexity_semantic_verification_binding(semantic)? + { + return Err(anyhow!( + "world-complexity semantic qualification is malformed or bound to another candidate" + )); + } + Ok(()) +} + +pub fn validate_world_complexity_semantic_qualification( + campaign: &crate::domain::Campaign, + proposal: &WorldComplexityProposal, + semantic: &WorldComplexitySemanticQualification, +) -> Result<()> { + validate_world_complexity_semantic_qualification_shape(proposal, semantic)?; + let expected_namespace = match proposal { + WorldComplexityProposal::Fission { .. } => "population", + WorldComplexityProposal::Individuate { .. } => "person", + }; + let established_names = match expected_namespace { + "population" => { + let residual_ids = residual_gestalt_ids(campaign); + campaign + .gestalts + .values() + .filter(|gestalt| !residual_ids.contains(&gestalt.id)) + .map(|gestalt| gestalt.name.as_str()) + .collect::>() + } + "person" => campaign + .actors + .values() + .map(|actor| actor.name.as_str()) + .chain(campaign.gestalt_members.values().filter_map(|member| { + member + .materialized_actor_id + .is_none() + .then_some(member.name.as_str()) + })) + .collect::>(), + _ => unreachable!("semantic namespace is fixed above"), + }; + let expected_names_digest = crate::legacy_transition::digest_serializable(&( + "ghostlight.world_complexity_established_names.v1", + expected_namespace, + established_names, + ))?; + if semantic.frozen_campaign_id != campaign.id + || semantic.frozen_world_revision != campaign.revision + || semantic.established_names_digest != expected_names_digest + { + return Err(anyhow!( + "world-complexity semantic qualification is stale, malformed, or bound to another candidate" + )); + } + Ok(()) +} + +pub fn validate_world_complexity_semantic_receipt_provenance( + proposal: &WorldComplexityProposal, + semantic: &WorldComplexitySemanticQualification, + receipts: &[crate::model::ModelStageReceipt], +) -> Result<()> { + validate_world_complexity_semantic_qualification_shape(proposal, semantic)?; + let receipt_keys = receipts + .iter() + .map(|receipt| receipt.storage_key().to_owned()) + .collect::>(); + if receipt_keys.len() != receipts.len() { + return Err(anyhow!( + "world-complexity semantic provenance contains duplicate model receipts" + )); + } + let verifier_receipts = receipts + .iter() + .filter(|receipt| receipt.stage == "world-complexity-semantic-verification") + .collect::>(); + if verifier_receipts.len() != 1 { + return Err(anyhow!( + "world-complexity semantic qualification needs exactly one valid bound verifier receipt" + )); + } + let verifier = verifier_receipts[0]; + let expected_sources = receipts + .iter() + .filter(|receipt| receipt.stage != "world-complexity-semantic-verification") + .map(|receipt| receipt.storage_key().to_owned()) + .collect::>(); + let actual_sources = verifier + .source_receipt_ids + .iter() + .cloned() + .collect::>(); + if verifier.model != crate::model::MODEL_BALANCED + || verifier.validation_result != "valid" + || verifier.local_validation_error.is_some() + || verifier.snapshot_binding != semantic.semantic_verification_binding + || expected_sources.is_empty() + || actual_sources.len() != verifier.source_receipt_ids.len() + || actual_sources != expected_sources + { + return Err(anyhow!( + "world-complexity semantic verifier ancestry does not exactly cover its generation receipts" + )); + } + Ok(()) +} + +pub(crate) fn validate_world_complexity_semantic_provenance( + campaign: &crate::domain::Campaign, + proposal: &WorldComplexityProposal, + semantic: &WorldComplexitySemanticQualification, + receipts: &[crate::model::ModelStageReceipt], +) -> Result<()> { + validate_world_complexity_semantic_qualification(campaign, proposal, semantic)?; + validate_world_complexity_semantic_receipt_provenance(proposal, semantic, receipts) +} + +impl WorldComplexitySemanticVerification { + pub fn accepted(&self) -> bool { + self.public_names_are_legible_identifiers + && self.names_do_not_repeat_an_overused_template + && self.cultural_resemblance_is_grounded_not_quota_cloning + && self.causal_additions_are_materially_distinct + && self.causal_additions_do_not_repeat_an_overused_procedural_template + && !self.rationale.trim().is_empty() + } +} + +pub async fn qualify_world_complexity_proposal_semantics( + model: &dyn crate::model::ModelPort, + campaign: &crate::domain::Campaign, + semantic_comparison_context: &serde_json::Value, + mut proposal: WorldComplexityProposal, + source_receipt_ids: Vec, +) -> Result<( + WorldComplexityProposal, + WorldComplexitySemanticVerification, + crate::model::ModelStageReceipt, +)> { + if source_receipt_ids.is_empty() { + return Err(anyhow!( + "world-complexity semantic verification has no source generation receipt" + )); + } + let (identity_namespace, established_names) = match &proposal { + WorldComplexityProposal::Fission { .. } => { + let residual_ids = residual_gestalt_ids(campaign); + ( + "population", + campaign + .gestalts + .values() + .filter(|gestalt| !residual_ids.contains(&gestalt.id)) + .map(|gestalt| gestalt.name.as_str()) + .collect::>(), + ) + } + WorldComplexityProposal::Individuate { .. } => ( + "person", + campaign + .actors + .values() + .map(|actor| actor.name.as_str()) + .chain(campaign.gestalt_members.values().filter_map(|member| { + member + .materialized_actor_id + .is_none() + .then_some(member.name.as_str()) + })) + .collect::>(), + ), + }; + let established_names_digest = crate::legacy_transition::digest_serializable(&( + "ghostlight.world_complexity_established_names.v1", + identity_namespace, + &established_names, + ))?; + let semantic_context_digest = crate::legacy_transition::digest_serializable(&( + "ghostlight.world_complexity_semantic_context.v1", + semantic_comparison_context, + ))?; + let candidate_digest = world_complexity_semantic_candidate_digest(&proposal)?; + let mut qualification = WorldComplexitySemanticQualification { + schema: "ghostlight.world_complexity_semantic_qualification.v1".into(), + identity_namespace: identity_namespace.into(), + frozen_campaign_id: campaign.id, + frozen_world_revision: campaign.revision, + established_names_digest, + semantic_context_digest, + owner_lane_digest: world_complexity_owner_lane_digest(&proposal)?, + candidate_digest, + semantic_verification_binding: String::new(), + }; + qualification.semantic_verification_binding = + world_complexity_semantic_verification_binding(&qualification)?; + let parent = campaign + .gestalts + .get(proposal.parent_gestalt_id()) + .ok_or_else(|| anyhow!("world-complexity semantic proposal lost its parent"))?; + let prompt = format!( + "Independently judge one proposed world-complexity mutation against the current canonical world and the supplied recent peer context. This is semantic review, not a keyword or spelling rule. Every public name must be a legible person or population identifier, not a role label, serial placeholder, or lightly varied quota template. Reject an overused surname, epithet, grammatical mold, bureaucratic construction, or cultureless fantasy-name pattern unless actual kinship or local culture grounds the resemblance. Also reject goals, pressures, capabilities, knowledge, obligations, relationships, or memories that merely paraphrase an overused procedural mold (for example everybody filing, registering, petitioning, or serving on another council) without adding materially different means, stakes, affordances, or causal behavior. Repeated vocabulary is allowed when the concrete mechanism and consequence differ. Judge the proposal as a whole and return only the verdict object.\n\nIDENTITY NAMESPACE:\n{}\nCOMPLETE ESTABLISHED NAMES:\n{}\nPARENT POPULATION:\n{}\nRECENT SEMANTIC COMPARISON CONTEXT:\n{}\nCANDIDATE PROPOSAL:\n{}", + identity_namespace, + serde_json::to_string(&established_names)?, + serde_json::to_string(parent)?, + serde_json::to_string(semantic_comparison_context)?, + serde_json::to_string(&proposal)?, + ); + let request = crate::model::ModelStageRequest { + stage: "world-complexity-semantic-verification".into(), + model: crate::model::MODEL_BALANCED.into(), + snapshot_binding: qualification.semantic_verification_binding.clone(), + lived_stream: prompt, + output_schema: Some(serde_json::to_value(schema_for!( + WorldComplexitySemanticVerification + ))?), + source_receipt_ids, + temperature: Some(0.0), + max_output_tokens: Some(1_000), + }; + let output = crate::model::run_validated_stage(model, &request).await?; + let verdict = serde_json::from_value::( + output + .structured + .ok_or_else(|| anyhow!("semantic verifier returned no structured verdict"))?, + )?; + let mut receipt = output.receipt; + if !verdict.accepted() { + crate::model::mark_model_receipt_semantic_invalid( + &mut receipt, + &format!( + "world-complexity semantic verifier rejected the candidate: {}", + serde_json::to_string(&verdict).unwrap_or_else(|_| verdict.rationale.clone()) + ), + ); + } + match &mut proposal { + WorldComplexityProposal::Fission { + qualification: owner, + .. + } => owner.semantic = qualification, + WorldComplexityProposal::Individuate { + qualification: owner, + .. + } => owner.semantic = qualification, + } + Ok((proposal, verdict, receipt)) +} + +fn validate_new_semantic_texts<'a>( + label: &str, + additions: impl IntoIterator, + inherited: impl IntoIterator, + established: impl IntoIterator, +) -> Result> { + let inherited = inherited + .into_iter() + .map(|value| semantic_text_key(value)) + .collect::>(); + let established = established + .into_iter() + .map(|value| semantic_text_key(value)) + .collect::>(); + let mut keys = BTreeSet::new(); + for value in additions { + let key = semantic_text_key(value); + if value.trim().is_empty() || value.chars().count() > 300 || key.is_empty() { + return Err(anyhow!( + "{label} must contain 1 through 300 readable characters" + )); + } + if inherited.contains(&key) || established.contains(&key) || !keys.insert(key) { + return Err(anyhow!( + "{label} must add text distinct from its parent, siblings, and established world" + )); + } + } + Ok(keys) +} + +pub(crate) fn validate_complexity_fission_semantics( + campaign: &crate::domain::Campaign, + parent: &crate::domain::GestaltPersonaState, + preview: &crate::domain::GestaltFissionPreview, + title: ElaboratorTitle, + jurisdiction_location_id: &str, + target_actionable_gain: u8, +) -> Result<()> { + if complexity_operation(title) != WorldComplexityOperation::Fission { + return Err(anyhow!( + "complexity fission qualification names a non-fission elaborator" + )); + } + if preview.partition_axis != complexity_partition_axis(title) { + return Err(anyhow!( + "complexity fission partition axis does not belong to its elaborator title" + )); + } + let actual_gain = preview.children.len().saturating_sub(2); + if actual_gain != usize::from(target_actionable_gain) { + return Err(anyhow!( + "complexity fission must add exactly {target_actionable_gain} qualified subjects" + )); + } + let residual = preview + .children + .iter() + .find(|child| child.id == preview.residual_child_id) + .ok_or_else(|| anyhow!("complexity fission lost its residual child"))?; + if residual.home_location_id != parent.home_location_id + || residual.shared_capabilities != parent.shared_capabilities + || residual.shared_knowledge != parent.shared_knowledge + || residual.goals != parent.goals + || residual.pressures != parent.pressures + { + return Err(anyhow!( + "the residual child is unresolved background and cannot claim new semantic detail" + )); + } + + let residual_ids = residual_gestalt_ids(campaign); + let established_names = campaign + .gestalts + .values() + .filter(|gestalt| !residual_ids.contains(&gestalt.id)) + .map(|gestalt| crate::resolution::public_identity_key(&gestalt.name)) + .collect::>(); + let allowed_locations = locations_in_jurisdiction(campaign, jurisdiction_location_id) + .into_iter() + .collect::>(); + let mut child_names = BTreeSet::new(); + let mut sibling_capabilities = BTreeSet::new(); + let mut sibling_knowledge = BTreeSet::new(); + let mut sibling_goals = BTreeSet::new(); + let mut sibling_pressures = BTreeSet::new(); + let mut specific_homes = BTreeSet::new(); + for child in preview + .children + .iter() + .filter(|child| child.id != preview.residual_child_id) + { + let name_key = crate::resolution::public_identity_key(&child.name); + if name_key.is_empty() + || established_names.contains(&name_key) + || !child_names.insert(name_key) + { + return Err(anyhow!( + "every fission child needs a distinct public population identity" + )); + } + if !allowed_locations.contains(&child.home_location_id) { + return Err(anyhow!( + "fission child {} is outside its assigned jurisdiction", + child.id + )); + } + if preview.partition_axis != crate::domain::AgencyAxis::Geography + && child.home_location_id != parent.home_location_id + { + return Err(anyhow!( + "only a geography fission may distribute children to other admitted places" + )); + } + specific_homes.insert(child.home_location_id.clone()); + let capability_additions = child + .shared_capabilities + .difference(&parent.shared_capabilities) + .cloned() + .collect::>(); + let knowledge_additions = child + .shared_knowledge + .difference(&parent.shared_knowledge) + .cloned() + .collect::>(); + let goal_additions = child + .goals + .iter() + .filter(|goal| !parent.goals.contains(goal)) + .cloned() + .collect::>(); + let pressure_additions = child + .pressures + .iter() + .filter(|pressure| !parent.pressures.contains(pressure)) + .cloned() + .collect::>(); + let semantic_dimensions = [ + !capability_additions.is_empty(), + !knowledge_additions.is_empty(), + !goal_additions.is_empty(), + !pressure_additions.is_empty(), + ] + .into_iter() + .filter(|present| *present) + .count(); + if semantic_dimensions < 2 + || goal_additions.len() > 1 + || pressure_additions.len() > 1 + || capability_additions.len() > 2 + || knowledge_additions.len() > 2 + { + return Err(anyhow!( + "each specific child needs two or more bounded causal dimensions without template padding" + )); + } + let title_shape_is_valid = match title { + ElaboratorTitle::Patina => { + !goal_additions.is_empty() && !capability_additions.is_empty() + } + ElaboratorTitle::Charter | ElaboratorTitle::Ledger => { + !goal_additions.is_empty() + && !pressure_additions.is_empty() + && (!capability_additions.is_empty() || !knowledge_additions.is_empty()) + } + ElaboratorTitle::Ember => { + !pressure_additions.is_empty() + && (!capability_additions.is_empty() || !knowledge_additions.is_empty()) + } + ElaboratorTitle::Numen => { + !goal_additions.is_empty() + && !pressure_additions.is_empty() + && !knowledge_additions.is_empty() + } + ElaboratorTitle::Hearth | ElaboratorTitle::Tangle | ElaboratorTitle::Veil => false, + }; + if !title_shape_is_valid { + return Err(anyhow!( + "specific child additions do not satisfy the assigned elaborator's causal lane" + )); + } + let capability_keys = validate_new_semantic_texts( + "child capability", + capability_additions.iter(), + parent.shared_capabilities.iter(), + campaign + .gestalts + .values() + .flat_map(|gestalt| gestalt.shared_capabilities.iter()), + )?; + let knowledge_keys = validate_new_semantic_texts( + "child knowledge", + knowledge_additions.iter(), + parent.shared_knowledge.iter(), + campaign + .gestalts + .values() + .flat_map(|gestalt| gestalt.shared_knowledge.iter()), + )?; + let goal_keys = validate_new_semantic_texts( + "child goal", + goal_additions.iter(), + parent.goals.iter(), + campaign + .gestalts + .values() + .flat_map(|gestalt| gestalt.goals.iter()), + )?; + let pressure_keys = validate_new_semantic_texts( + "child pressure", + pressure_additions.iter(), + parent.pressures.iter(), + campaign + .gestalts + .values() + .flat_map(|gestalt| gestalt.pressures.iter()), + )?; + if !capability_keys.is_disjoint(&sibling_capabilities) + || !knowledge_keys.is_disjoint(&sibling_knowledge) + || !goal_keys.is_disjoint(&sibling_goals) + || !pressure_keys.is_disjoint(&sibling_pressures) + { + return Err(anyhow!( + "specific fission children must not repeat one another's causal additions" + )); + } + sibling_capabilities.extend(capability_keys); + sibling_knowledge.extend(knowledge_keys); + sibling_goals.extend(goal_keys); + sibling_pressures.extend(pressure_keys); + } + if preview.partition_axis == crate::domain::AgencyAxis::Geography + && allowed_locations.len() > 1 + && specific_homes.len() < 2 + { + return Err(anyhow!( + "geography fission must distribute specific children across at least two admitted places" + )); + } + Ok(()) +} + +pub(crate) fn validate_complexity_individuation_semantics( + campaign: &crate::domain::Campaign, + title: ElaboratorTitle, + individuation: &crate::domain::GestaltIndividuation, +) -> Result<()> { + if !matches!( + title, + ElaboratorTitle::Hearth | ElaboratorTitle::Tangle | ElaboratorTitle::Veil + ) { + return Err(anyhow!( + "only Hearth, Tangle, or Veil may own a complexity individuation" + )); + } + let member = &individuation.member; + let parent = &campaign.gestalts[&individuation.gestalt_id]; + let dimensions = [ + !member.capability_additions.is_empty(), + !member.knowledge_additions.is_empty(), + !member.equipment.is_empty(), + !member.obligations.is_empty(), + !member.relationships.is_empty(), + !member.goals.is_empty(), + !member.memories.is_empty(), + ] + .into_iter() + .filter(|present| *present) + .count(); + if dimensions < 3 || member.goals.is_empty() || member.memories.is_empty() { + return Err(anyhow!( + "complexity individuation needs a goal, a concrete memory, and at least one additional identity-bearing difference" + )); + } + validate_new_semantic_texts( + "member goal", + member.goals.iter(), + parent.goals.iter(), + campaign + .gestalt_members + .values() + .flat_map(|existing| existing.goals.iter()), + )?; + validate_new_semantic_texts( + "member memory", + member.memories.iter(), + std::iter::empty(), + campaign + .gestalt_members + .values() + .flat_map(|existing| existing.memories.iter()), + )?; + match title { + ElaboratorTitle::Hearth if member.obligations.is_empty() => Err(anyhow!( + "Hearth individuation requires one concrete care or belonging obligation" + )), + ElaboratorTitle::Tangle if member.relationships.is_empty() => Err(anyhow!( + "Tangle individuation requires a relationship to an existing canonical subject" + )), + ElaboratorTitle::Veil if member.knowledge_additions.is_empty() => Err(anyhow!( + "Veil individuation requires uneven knowledge not held by the parent population" + )), + _ => Ok(()), + } +} + impl WorldComplexityTool<'_> { fn materialize_proposal( &self, @@ -1731,6 +2608,15 @@ impl WorldComplexityTool<'_> { state.id = child.id.clone(); state.name = child.name.clone(); state.version = 0; + state.home_location_id = child.home_location_id.clone(); + state + .shared_capabilities + .extend(child.capability_additions.iter().cloned()); + state + .shared_knowledge + .extend(child.knowledge_additions.iter().cloned()); + state.goals.extend(child.goals_add.iter().cloned()); + state.pressures.extend(child.pressures_add.iter().cloned()); state.resources = resource_child_assignments .iter() .filter(|(_, child_id)| *child_id == &child.id) @@ -1758,8 +2644,25 @@ impl WorldComplexityTool<'_> { canon_candidates: Vec::new(), requires_approval: true, }; - crate::resolution::validate_fission(self.campaign, &preview)?; - Ok(WorldComplexityProposal::Fission { preview }) + crate::resolution::validate_elaborated_fission(self.campaign, &preview)?; + validate_complexity_fission_semantics( + self.campaign, + parent, + &preview, + self.title, + self.jurisdiction_location_id, + self.target_actionable_gain, + )?; + Ok(WorldComplexityProposal::Fission { + preview, + qualification: WorldComplexityFissionQualification { + schema: "ghostlight.world_complexity_fission_qualification.v1".into(), + title: self.title, + jurisdiction_location_id: self.jurisdiction_location_id.into(), + target_actionable_gain: self.target_actionable_gain, + semantic: WorldComplexitySemanticQualification::default(), + }, + }) } WorldComplexityMutationDraft::Individuate { member } => { if self.operation != WorldComplexityOperation::Individuate { @@ -1798,7 +2701,20 @@ impl WorldComplexityTool<'_> { location_id, }; crate::resolution::validate_gestalt_individuation(self.campaign, &individuation)?; - Ok(WorldComplexityProposal::Individuate { individuation }) + validate_complexity_individuation_semantics( + self.campaign, + self.title, + &individuation, + )?; + Ok(WorldComplexityProposal::Individuate { + individuation, + qualification: WorldComplexityIndividuationQualification { + schema: "ghostlight.world_complexity_individuation_qualification.v1".into(), + title: self.title, + jurisdiction_location_id: self.jurisdiction_location_id.into(), + semantic: WorldComplexitySemanticQualification::default(), + }, + }) } } } @@ -1814,6 +2730,49 @@ impl crate::agent::ModelAgentTool for WorldComplexityTool<'_> { let mut schema = serde_json::to_value(schema_for!(WorldComplexityAction)) .map_err(|error| error.to_string())?; schema["properties"]["schema"] = exact_schema("ghostlight.world_complexity_action.v1"); + if self.operation == WorldComplexityOperation::Fission { + let allowed_locations = + locations_in_jurisdiction(self.campaign, self.jurisdiction_location_id); + let child = &mut schema["$defs"]["WorldComplexityChildDraft"]; + child["properties"]["home_location_id"] = serde_json::json!({ + "type":"string", + "enum":allowed_locations, + }); + child["properties"]["id"]["minLength"] = serde_json::json!(1); + child["properties"]["id"]["maxLength"] = serde_json::json!(160); + child["properties"]["name"]["minLength"] = serde_json::json!(1); + child["properties"]["name"]["maxLength"] = serde_json::json!(160); + for field in ["capability_additions", "knowledge_additions"] { + child["properties"][field]["maxItems"] = serde_json::json!(2); + child["properties"][field]["items"]["minLength"] = serde_json::json!(1); + child["properties"][field]["items"]["maxLength"] = serde_json::json!(300); + } + for field in ["goals_add", "pressures_add"] { + child["properties"][field]["maxItems"] = serde_json::json!(1); + child["properties"][field]["items"]["minLength"] = serde_json::json!(1); + child["properties"][field]["items"]["maxLength"] = serde_json::json!(300); + } + } else { + let member = &mut schema["$defs"]["WorldComplexityMemberDraft"]; + member["properties"]["name"]["minLength"] = serde_json::json!(1); + member["properties"]["name"]["maxLength"] = serde_json::json!(160); + for field in ["goals", "memories"] { + member["properties"][field]["minItems"] = serde_json::json!(1); + member["properties"][field]["maxItems"] = serde_json::json!(8); + } + match self.title { + ElaboratorTitle::Hearth => { + member["properties"]["obligations"]["minItems"] = serde_json::json!(1); + } + ElaboratorTitle::Tangle => { + member["properties"]["relationships"]["minProperties"] = serde_json::json!(1); + } + ElaboratorTitle::Veil => { + member["properties"]["knowledge_additions"]["minItems"] = serde_json::json!(1); + } + _ => {} + } + } let variants = schema .pointer_mut("/$defs/WorldComplexityMutationDraft/oneOf") .and_then(serde_json::Value::as_array_mut) @@ -1839,8 +2798,9 @@ impl crate::agent::ModelAgentTool for WorldComplexityTool<'_> { .pointer_mut("/properties/children") .and_then(serde_json::Value::as_object_mut) .ok_or_else(|| "complexity fission schema has no children".to_owned())?; - children.insert("minItems".into(), serde_json::json!(2)); - children.insert("maxItems".into(), serde_json::json!(8)); + let child_count = usize::from(self.target_actionable_gain).saturating_add(2); + children.insert("minItems".into(), serde_json::json!(child_count)); + children.insert("maxItems".into(), serde_json::json!(child_count)); let parent = &self.campaign.gestalts[self.parent_gestalt_id]; selected["properties"]["resource_child_assignments"] = exact_keyed_string_map_schema(parent.resources.iter().cloned()); @@ -1928,19 +2888,29 @@ impl ElaborationSubAgentPort for ModelWorldComplexityWo model_stage_receipts: Vec::new(), } })?; + let target_actionable_gain = *self + .target_actionable_gains + .get(&invocation.dispatch.ordinal) + .ok_or_else(|| ElaborationSubAgentFailure { + diagnostic: "complexity dispatch has no bounded actionable gain".into(), + model_stage_receipts: Vec::new(), + })?; let session_id = elaborator_session_id(invocation.dispatch.title, jurisdiction_id); let assignment = match operation { WorldComplexityOperation::Fission => serde_json::json!({ "operation":"fission_gestalt", "parent_gestalt_id":parent_id, "partition_axis":axis, + "target_actionable_gain":target_actionable_gain, + "exact_child_count":usize::from(target_actionable_gain) + 2, "requirements":[ "Return a ghostlight.world_complexity_action.v1 envelope whose mutation has operation=fission.", - "Propose only child ids, names, partition values, the residual child id, and exact member/resource assignments; the deterministic tool constructs inherited child state and frozen bindings.", - "Create 2 through 8 non-overlapping children with distinct partition values.", - "Exactly one residual child must have partition value other/unknown.", + "Create the exact assigned number of non-overlapping children with distinct partition values.", + "Exactly one residual child must have partition value other/unknown, remain at the parent home, and provide empty capability_additions, knowledge_additions, goals_add, and pressures_add.", + "Every specific child needs a unique public name and title-appropriate causal additions. Patina adds a concrete practice plus a goal; Charter and Ledger add a goal, pressure, and operational capability or knowledge; Ember adds pressure plus an operational capability or knowledge; Numen adds a goal, pressure, and uneven numinous knowledge.", + "A geography fission distributes specific children among admitted places in this jurisdiction. Other axes keep the parent home.", "Assign every scarce resource and named member exactly once; materialized people retain their Actor identity while changing population affiliation.", - "Do not repeat inherited capabilities, knowledge, goals, pressures, evidence, or approval fields." + "Do not restate inherited capabilities, knowledge, goals, pressures, evidence, or approval fields in the additive fields." ] }), WorldComplexityOperation::Individuate => serde_json::json!({ @@ -1950,7 +2920,8 @@ impl ElaborationSubAgentPort for ModelWorldComplexityWo "requirements":[ "Return a ghostlight.world_complexity_action.v1 envelope whose mutation has operation=individuate and one compact member draft.", "The deterministic tool owns schema, exact parent id, parent version, assigned location, materialization state, and relevance revisions; those fields are absent from your draft.", - "Create one consequential named person whose goals, knowledge, relationships, and memories are grounded in the parent state.", + "Create one consequential named person with a distinct full public name, at least one new goal, one situated memory, and one additional identity-bearing difference grounded in the parent state.", + "Hearth requires a concrete care or belonging obligation. Tangle requires leverage expressed as a relationship to one supplied canonical target. Veil requires uneven knowledge not held by the parent population.", "Use a new local member id without the member: prefix.", "Do not create quota names, unsupported relationships, or decorative biography." ] @@ -1961,6 +2932,32 @@ impl ElaborationSubAgentPort for ModelWorldComplexityWo invocation.dispatch.title.display_name(), invocation.dispatch.title.mandate(), ); + let residual_ids = residual_gestalt_ids(&self.campaign); + let (public_identity_namespace, established_public_names) = match operation { + WorldComplexityOperation::Fission => ( + "population", + self.campaign + .gestalts + .values() + .filter(|gestalt| !residual_ids.contains(&gestalt.id)) + .map(|gestalt| gestalt.name.as_str()) + .collect::>(), + ), + WorldComplexityOperation::Individuate => ( + "person", + self.campaign + .actors + .values() + .map(|actor| actor.name.as_str()) + .chain( + self.campaign + .gestalt_members + .values() + .map(|member| member.name.as_str()), + ) + .collect::>(), + ), + }; let workbench = serde_json::json!({ "schema":"ghostlight.world_complexity_workbench.v1", "frozen_world":{ @@ -1969,6 +2966,14 @@ impl ElaborationSubAgentPort for ModelWorldComplexityWo }, "assignment":assignment, "session_jurisdiction_id":jurisdiction_id, + "admitted_home_location_ids":locations_in_jurisdiction(&self.campaign, jurisdiction_id), + "canonical_relationship_target_ids":self.campaign.civic_systems.get(jurisdiction_id) + .into_iter() + .flat_map(|civic| civic.governing_institution_ids.iter().chain(civic.resident_population_ids.iter())) + .filter(|id|id.as_str()!=parent_id) + .take(64).cloned().collect::>(), + "public_identity_namespace":public_identity_namespace, + "established_names_in_this_identity_namespace":established_public_names, "required_resource_assignment_ids":parent.resources, "required_member_assignment_ids":self.campaign.gestalt_members.values() .filter(|member|member.gestalt_id == parent_id) @@ -1985,8 +2990,11 @@ impl ElaborationSubAgentPort for ModelWorldComplexityWo let mut tool = WorldComplexityTool { campaign: &self.campaign, parent_gestalt_id: parent_id, + title: invocation.dispatch.title, operation, partition_axis: axis, + jurisdiction_location_id: jurisdiction_id, + target_actionable_gain, workbench, }; let spec = crate::agent::ModelAgentSpec { @@ -2693,7 +3701,7 @@ pub struct WorldElaborationRejection { /// Deterministic result of admitting one successful parallel elaboration wave. /// The candidate remains non-canonical and carries an empty civic verifier /// binding until an independent semantic verifier finalizes it. -#[derive(Clone, Debug, Serialize, JsonSchema, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] #[serde(deny_unknown_fields)] pub struct WorldElaborationAdmission { schema: String, @@ -2713,7 +3721,7 @@ pub struct WorldElaborationAdmission { /// The only elaboration value accepted by `WorldKernel::commit_elaboration`. /// It binds the immutable admission result to an independently generated /// semantic-verifier receipt without letting that verifier rewrite the draft. -#[derive(Clone, Debug, Serialize, JsonSchema, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] #[serde(deny_unknown_fields)] pub struct FinalizedWorldElaboration { schema: String, @@ -3136,7 +4144,18 @@ impl WorldElaborationAdmission { &self.digest } - fn valid_candidate( + pub fn expected_revision(&self) -> u64 { + self.expected_revision + } + + pub fn target_location_id(&self) -> &str { + &self.target_location_id + } + + /// Revalidates the complete admission against the current campaign and + /// derives its candidate from the admitted operations. Recovery code uses + /// this instead of trusting a serialized candidate projection. + pub fn valid_candidate( &self, campaign: &crate::domain::Campaign, ) -> Result { @@ -3193,6 +4212,10 @@ impl FinalizedWorldElaboration { &self.digest } + pub fn semantic_verifier_receipt(&self) -> &crate::model::ModelStageReceipt { + &self.semantic_verifier_receipt + } + pub(crate) fn into_kernel_parts( self, campaign: &crate::domain::Campaign, @@ -3533,6 +4556,20 @@ mod tests { assert_eq!(demand.realm_subject_targets["rail"], 800); } + #[test] + fn twenty_percent_cover_derives_twelve_hundred_qualified_subjects() { + let demand = derive_world_elaboration_demand( + 240, + 80, + &WorldScaleIntent::twenty_percent(), + BTreeMap::from([("delvehold".into(), 1)]), + ) + .unwrap(); + assert_eq!(demand.target_actionable_subjects, 1_200); + assert_eq!(demand.actionable_subject_deficit, 1_120); + assert_eq!(demand.round_mutation_budget, 224); + } + #[test] fn elaboration_demand_never_reopens_a_satisfied_subject_target() { let demand = derive_world_elaboration_demand( @@ -3575,6 +4612,18 @@ mod tests { campaign .agency_profiles .insert(eligible_id.clone(), eligible); + campaign.gestalt_lineages.insert( + "source-parent".into(), + crate::domain::GestaltLineage { + schema: "ghostlight.gestalt_lineage.v1".into(), + parent_gestalt_id: "source-parent".into(), + child_gestalt_ids: vec![eligible_id.clone()], + partition_axis: crate::domain::AgencyAxis::Authority, + partition_values: BTreeMap::from([(eligible_id.clone(), "other/unknown".into())]), + residual_child_id: eligible_id.clone(), + source_revision: 1, + }, + ); let mut retired = campaign.agency_profiles[&eligible_id].clone(); retired.id = "retired-profile".into(); retired.subject_id = "retired-subject".into(); @@ -3586,12 +4635,97 @@ mod tests { let expected = campaign .agency_profiles .values() - .filter(|profile| profile.active_leaf && profile.simulation_eligible) + .filter(|profile| { + profile.active_leaf + && profile.simulation_eligible + && profile.subject_id != eligible_id + }) .count() as u32; assert_eq!(canonical_actionable_subject_count(&campaign), expected); + assert!(campaign.agency_profiles[&eligible_id].simulation_eligible); assert!(!campaign.agency_profiles[&campaign.player_actor_id].simulation_eligible); } + #[test] + fn ordinary_fission_children_cannot_pay_the_qualified_complexity_bill() { + let mut campaign = campaign_with_civic_room(); + crate::resolution::ensure_agency_profiles(&mut campaign); + let parent_id = "source-population".to_owned(); + let parent = crate::domain::GestaltPersonaState { + schema: "ghostlight.gestalt_persona_state.v1".into(), + id: parent_id.clone(), + name: "Source Population".into(), + version: 0, + home_location_id: "room".into(), + shared_capabilities: BTreeSet::new(), + shared_knowledge: BTreeSet::new(), + resources: BTreeSet::new(), + goals: Vec::new(), + pressures: Vec::new(), + }; + campaign.gestalts.insert(parent_id.clone(), parent.clone()); + let mut parent_profile = campaign.agency_profiles[&campaign.player_actor_id].clone(); + parent_profile.id = format!("profile:{parent_id}"); + parent_profile.subject_id = parent_id.clone(); + parent_profile.subject_kind = crate::domain::AgencySubjectKind::Gestalt; + parent_profile.active_leaf = true; + parent_profile.simulation_eligible = true; + campaign + .agency_profiles + .insert(parent_id.clone(), parent_profile.clone()); + let baseline = canonical_actionable_subject_count(&campaign); + + let mut ordinary = parent.clone(); + ordinary.id = "ordinary-child".into(); + ordinary.name = "Ordinary Child".into(); + let mut qualified = parent.clone(); + qualified.id = "qualified-child".into(); + qualified.name = "Qualified Child".into(); + qualified + .shared_capabilities + .insert("operate the west sluice".into()); + qualified + .goals + .push("secure an independent flood rota".into()); + let mut residual = parent.clone(); + residual.id = "residual-child".into(); + residual.name = "Residual Child".into(); + for child in [&ordinary, &qualified, &residual] { + campaign.gestalts.insert(child.id.clone(), child.clone()); + let mut profile = parent_profile.clone(); + profile.id = format!("profile:{}", child.id); + profile.subject_id = child.id.clone(); + profile.parent_subject_id = Some(parent_id.clone()); + campaign.agency_profiles.insert(child.id.clone(), profile); + } + campaign.gestalt_lineages.insert( + parent_id.clone(), + crate::domain::GestaltLineage { + schema: "ghostlight.gestalt_lineage.v1".into(), + parent_gestalt_id: parent_id, + child_gestalt_ids: vec![ + ordinary.id.clone(), + qualified.id.clone(), + residual.id.clone(), + ], + partition_axis: crate::domain::AgencyAxis::Authority, + partition_values: BTreeMap::from([ + (ordinary.id.clone(), "ordinary".into()), + (qualified.id.clone(), "qualified".into()), + (residual.id.clone(), "other/unknown".into()), + ]), + residual_child_id: residual.id.clone(), + source_revision: campaign.revision, + }, + ); + + let ids = canonical_actionable_subject_ids(&campaign); + assert!(!ids.contains(&ordinary.id)); + assert!(ids.contains(&qualified.id)); + assert!(!ids.contains(&residual.id)); + assert_eq!(canonical_actionable_subject_count(&campaign), baseline + 1); + } + #[test] fn elaborator_session_compaction_binds_narrative_memory_to_exact_commits() { let campaign = campaign_with_civic_room(); @@ -3732,6 +4866,25 @@ mod tests { }; let mut licensed = child("licensed-river-households", "Licensed River Households"); licensed.resources.insert("ferry charter".into()); + licensed + .shared_capabilities + .insert("withhold licensed ferry service".into()); + licensed + .goals + .push("keep licensed crossings under household control".into()); + licensed + .pressures + .push("the charter renewal is being challenged".into()); + let mut unlicensed = child("unlicensed-river-households", "Unlicensed River Households"); + unlicensed + .shared_knowledge + .insert("unmarked low-water crossings".into()); + unlicensed + .goals + .push("open one crossing beyond charter custody".into()); + unlicensed + .pressures + .push("licensed patrols impound unmarked ferries".into()); crate::domain::GestaltFissionPreview { schema: "ghostlight.gestalt_fission_preview.v1".into(), campaign_id: campaign.id, @@ -3740,6 +4893,7 @@ mod tests { partition_axis: crate::domain::AgencyAxis::Authority, children: vec![ licensed, + unlicensed, child("unrecorded-river-households", "Unrecorded River Households"), ], child_partition_values: BTreeMap::from([ @@ -3747,6 +4901,10 @@ mod tests { "licensed-river-households".into(), "licensed tenders".into(), ), + ( + "unlicensed-river-households".into(), + "unlicensed tenders".into(), + ), ("unrecorded-river-households".into(), "other/unknown".into()), ]), residual_child_id: "unrecorded-river-households".into(), @@ -3762,6 +4920,16 @@ mod tests { } } + fn river_fission_qualification() -> WorldComplexityFissionQualification { + WorldComplexityFissionQualification { + schema: "ghostlight.world_complexity_fission_qualification.v1".into(), + title: ElaboratorTitle::Charter, + jurisdiction_location_id: "room".into(), + target_actionable_gain: 1, + semantic: WorldComplexitySemanticQualification::default(), + } + } + #[tokio::test] async fn complexity_tool_admits_only_the_assigned_meaningful_fission() { let campaign = campaign_with_fission_parent(); @@ -3769,8 +4937,11 @@ mod tests { let mut tool = WorldComplexityTool { campaign: &campaign, parent_gestalt_id: "river-households", + title: ElaboratorTitle::Charter, operation: WorldComplexityOperation::Fission, partition_axis: crate::domain::AgencyAxis::Authority, + jurisdiction_location_id: "room", + target_actionable_gain: 1, workbench: serde_json::json!({"schema":"test.workbench.v1"}), }; let context = crate::agent::ModelAgentToolContext { @@ -3809,6 +4980,33 @@ mod tests { id: child.id.clone(), name: child.name.clone(), partition_value: preview.child_partition_values[&child.id].clone(), + home_location_id: child.home_location_id.clone(), + capability_additions: child + .shared_capabilities + .difference(&campaign.gestalts["river-households"].shared_capabilities) + .cloned() + .collect(), + knowledge_additions: child + .shared_knowledge + .difference(&campaign.gestalts["river-households"].shared_knowledge) + .cloned() + .collect(), + goals_add: child + .goals + .iter() + .filter(|goal| !campaign.gestalts["river-households"].goals.contains(goal)) + .cloned() + .collect(), + pressures_add: child + .pressures + .iter() + .filter(|pressure| { + !campaign.gestalts["river-households"] + .pressures + .contains(pressure) + }) + .cloned() + .collect(), }) .collect(), residual_child_id: preview.residual_child_id.clone(), @@ -3826,17 +5024,27 @@ mod tests { ) .await; let crate::agent::ModelAgentToolOutcome::Accepted { - output: WorldComplexityProposal::Fission { preview: admitted }, + output: + WorldComplexityProposal::Fission { + preview: admitted, .. + }, .. } = admitted else { panic!("compact fission draft was not admitted") }; - assert!(admitted.children.iter().all(|child| { - child.shared_knowledge == campaign.gestalts["river-households"].shared_knowledge - && child.shared_capabilities - == campaign.gestalts["river-households"].shared_capabilities - })); + assert_eq!(admitted.children.len(), 3); + assert!( + admitted + .children + .iter() + .filter(|child| child.id != admitted.residual_child_id) + .all(|child| { + child.goals.len() > campaign.gestalts["river-households"].goals.len() + && child.pressures.len() + > campaign.gestalts["river-households"].pressures.len() + }) + ); let invalid = WorldComplexityMutationDraft::Fission { children: preview @@ -3846,6 +5054,11 @@ mod tests { id: child.id.clone(), name: child.name.clone(), partition_value: "same partition".into(), + home_location_id: child.home_location_id.clone(), + capability_additions: BTreeSet::new(), + knowledge_additions: BTreeSet::new(), + goals_add: Vec::new(), + pressures_add: Vec::new(), }) .collect(), residual_child_id: preview.residual_child_id, @@ -3896,11 +5109,22 @@ mod tests { relevance_lease_until_revision: 0, }, }; + let mut punctuation_only = individuation.clone(); + punctuation_only.member.name = "---".into(); + assert!( + crate::resolution::validate_gestalt_individuation(&campaign, &punctuation_only) + .unwrap_err() + .to_string() + .contains("no public identity characters") + ); let mut tool = WorldComplexityTool { campaign: &campaign, parent_gestalt_id: "river-households", + title: ElaboratorTitle::Veil, operation: WorldComplexityOperation::Individuate, partition_axis: crate::domain::AgencyAxis::Information, + jurisdiction_location_id: "room", + target_actionable_gain: 1, workbench: serde_json::json!({"schema":"test.workbench.v1"}), }; let context = crate::agent::ModelAgentToolContext { @@ -3970,8 +5194,11 @@ mod tests { let mut tool = WorldComplexityTool { campaign: &campaign, parent_gestalt_id: "river-households", + title: ElaboratorTitle::Veil, operation: WorldComplexityOperation::Individuate, partition_axis: crate::domain::AgencyAxis::Information, + jurisdiction_location_id: "room", + target_actionable_gain: 1, workbench: serde_json::json!({"schema":"test.workbench.v1"}), }; let context = crate::agent::ModelAgentToolContext { @@ -4010,10 +5237,10 @@ mod tests { &context, ) .await; - assert!(matches!( - same_name, - crate::agent::ModelAgentToolOutcome::Accepted { .. } - )); + let crate::agent::ModelAgentToolOutcome::Rejected { finding, .. } = same_name else { + panic!("duplicate public name was admitted") + }; + assert!(finding.diagnostic.contains("public Actor identity")); let mut invalid = valid_same_name; invalid.id = "new-weirkeeper".into(); invalid.goals = (0..9).map(|index| format!("goal {index}")).collect(); @@ -4038,14 +5265,14 @@ mod tests { assert!( finding .diagnostic - .contains("duplicates an established Actor identity") + .contains("duplicates an established canonical subject identity") ); assert!( finding .diagnostic .contains("unsupported subjects: actor:invented-stranger") ); - assert!(!finding.diagnostic.contains("Actor name")); + assert!(finding.diagnostic.contains("public Actor identity")); } #[test] @@ -4061,10 +5288,14 @@ mod tests { ¤t, WorldComplexityProposal::Fission { preview: preview.clone(), + qualification: river_fission_qualification(), }, ) .unwrap(); - let WorldComplexityProposal::Fission { preview: rebased } = rebased else { + let WorldComplexityProposal::Fission { + preview: rebased, .. + } = rebased + else { panic!("rebased mutation changed kind") }; assert_eq!(rebased.expected_world_revision, current.revision); @@ -4079,7 +5310,10 @@ mod tests { rebase_world_complexity_proposal( &binding, ¤t, - WorldComplexityProposal::Fission { preview }, + WorldComplexityProposal::Fission { + preview, + qualification: river_fission_qualification(), + }, ) .is_err() ); @@ -4091,19 +5325,43 @@ mod tests { let preview = valid_river_fission(&frozen); let mut current = frozen.clone(); current.revision = current.revision.saturating_add(1); - current.gestalts.insert( - "licensed-river-households".into(), - preview.children[0].clone(), - ); + let mut collision = preview.children[0].clone(); + collision.name = "Preexisting Licensed Register".into(); + let parent = &frozen.gestalts["river-households"]; + collision.shared_capabilities = parent.shared_capabilities.clone(); + collision + .shared_capabilities + .insert("maintain tidal sluice clocks".into()); + collision.shared_knowledge = parent.shared_knowledge.clone(); + collision + .shared_knowledge + .insert("salt-rot marks on the old quay".into()); + collision.goals = parent.goals.clone(); + collision + .goals + .push("restore the abandoned east-quay sluice".into()); + collision.pressures = parent.pressures.clone(); + collision + .pressures + .push("spring tides are cracking the quay gates".into()); + current + .gestalts + .insert("licensed-river-households".into(), collision); let binding = world_complexity_parent_binding(&frozen, "river-households").unwrap(); let rebased = rebase_world_complexity_proposal( &binding, ¤t, - WorldComplexityProposal::Fission { preview }, + WorldComplexityProposal::Fission { + preview, + qualification: river_fission_qualification(), + }, ) .unwrap(); - let WorldComplexityProposal::Fission { preview: rebased } = rebased else { + let WorldComplexityProposal::Fission { + preview: rebased, .. + } = rebased + else { panic!("rebased mutation changed kind") }; let renamed = rebased @@ -4120,6 +5378,53 @@ mod tests { assert_eq!(rebased.residual_child_id, "unrecorded-river-households"); } + #[test] + fn complexity_fission_rebase_sees_agency_profile_id_collisions() { + let frozen = campaign_with_fission_parent(); + let preview = valid_river_fission(&frozen); + let colliding_id = preview.children[0].id.clone(); + let mut current = frozen.clone(); + current.revision = current.revision.saturating_add(1); + let mut reservation = current.agency_profiles["river-households"].clone(); + reservation.id = colliding_id; + reservation.subject_id = "detached-profile-subject".into(); + reservation.active_leaf = false; + reservation.simulation_eligible = false; + current + .agency_profiles + .insert("detached-profile-key".into(), reservation); + + let binding = world_complexity_parent_binding(&frozen, "river-households").unwrap(); + let rebase = || { + rebase_world_complexity_proposal( + &binding, + ¤t, + WorldComplexityProposal::Fission { + preview: preview.clone(), + qualification: river_fission_qualification(), + }, + ) + .unwrap() + }; + let renamed_id = |proposal: WorldComplexityProposal| { + let WorldComplexityProposal::Fission { preview, .. } = proposal else { + panic!("rebased mutation changed kind") + }; + preview + .children + .iter() + .find(|child| child.name == "Licensed River Households") + .unwrap() + .id + .clone() + }; + + let first = renamed_id(rebase()); + let second = renamed_id(rebase()); + assert!(first.starts_with("river-households:fission:")); + assert_eq!(first, second); + } + fn profile(weights: &[(ElaboratorTitle, u16)]) -> WorldElaborationProfile { WorldElaborationProfile { schema: "ghostlight.world_elaboration_profile.v1".into(), @@ -4518,6 +5823,8 @@ mod tests { struct ExactSessionCompactionModel; + struct RejectionOnlySessionCompactionModel; + struct ReceiptlessWorldWorker; #[async_trait] @@ -4634,6 +5941,28 @@ mod tests { } } + #[async_trait] + impl crate::model::ModelPort for RejectionOnlySessionCompactionModel { + async fn run(&self, request: &crate::model::ModelStageRequest) -> Result { + assert!(request.stage.ends_with("session-compaction")); + assert!( + request + .lived_stream + .contains("causal_additions_do_not_repeat_an_overused_procedural_template") + ); + Ok(serde_json::json!({ + "schema":"ghostlight.elaborator_session_compaction_draft.v1", + "frontier_summary":"The rejected filing mold remains a live design constraint.", + "unresolved_leads":["Replace procedure with a materially executable conflict."] + }) + .to_string()) + } + + fn provider(&self) -> &'static str { + "rejection-session-compaction-fixture" + } + } + #[tokio::test] async fn session_compactor_preserves_exact_journal_ancestry() { let campaign = campaign_with_civic_room(); @@ -4664,6 +5993,31 @@ mod tests { .unwrap(); } + #[tokio::test] + async fn session_compactor_carries_a_semantic_rejection_without_a_commit() { + let campaign = campaign_with_civic_room(); + let finding = "{\"causal_additions_do_not_repeat_an_overused_procedural_template\":false}"; + let (checkpoint, receipts) = compact_elaborator_session( + &RejectionOnlySessionCompactionModel, + &campaign, + "room", + ElaboratorTitle::Charter, + "charter:room", + None, + &[], + vec![finding.into()], + ) + .await + .unwrap(); + + assert!(checkpoint.recent_commit_receipt_ids.is_empty()); + assert_eq!(checkpoint.recent_rejection_findings, [finding]); + assert_eq!(receipts.len(), 1); + checkpoint + .validate_for(&campaign, "room", ElaboratorTitle::Charter) + .unwrap(); + } + #[tokio::test] async fn session_compactor_reports_every_deterministic_preflight_mismatch() { let campaign = campaign_with_civic_room(); @@ -4748,6 +6102,7 @@ mod tests { 1, vec!["river-households".into()], BTreeMap::from([("river-households".into(), "room".into())]), + 1, BTreeMap::from([("patina:room".into(), checkpoint)]), ); @@ -4779,6 +6134,7 @@ mod tests { 1, vec!["river-households".into()], BTreeMap::from([("river-households".into(), "realm".into())]), + 1, BTreeMap::new(), ); diff --git a/crates/ghostlight-dungeon/src/kernel.rs b/crates/ghostlight-dungeon/src/kernel.rs index 650f34f..c8b546b 100644 --- a/crates/ghostlight-dungeon/src/kernel.rs +++ b/crates/ghostlight-dungeon/src/kernel.rs @@ -1144,6 +1144,74 @@ fn execute( Some((transition, mutation_receipt)), ) } + WorldCommand::ElaborateGestaltFission { + expected_revision, + preview, + qualification, + model_stage_receipts, + } => { + require_revision(&campaign, expected_revision)?; + if qualification.schema != "ghostlight.world_complexity_fission_qualification.v1" + || !(1..=6).contains(&qualification.target_actionable_gain) + || model_stage_receipts.is_empty() + { + return Err(KernelError::Invalid( + "elaborated fission lacks its exact qualification or model provenance".into(), + )); + } + let parent = campaign + .gestalts + .get(&preview.parent_gestalt_id) + .ok_or_else(|| KernelError::Invalid("fission parent is unknown".into()))?; + let proposal = crate::elaboration::WorldComplexityProposal::Fission { + preview: preview.clone(), + qualification: qualification.clone(), + }; + crate::resolution::validate_elaborated_fission(&campaign, &preview) + .and_then(|_| { + crate::elaboration::validate_complexity_fission_semantics( + &campaign, + parent, + &preview, + qualification.title, + &qualification.jurisdiction_location_id, + qualification.target_actionable_gain, + ) + }) + .and_then(|_| { + crate::elaboration::validate_world_complexity_semantic_provenance( + &campaign, + &proposal, + &qualification.semantic, + &model_stage_receipts, + ) + }) + .map_err(|error| KernelError::Invalid(error.to_string()))?; + let transition = crate::legacy_transition::lower_elaborated_fission( + &campaign, + &preview, + Utc::now() + Duration::minutes(5), + ) + .map_err(|error| KernelError::Invalid(error.to_string()))?; + let mutation_receipt = crate::legacy_transition::apply_lowered_elaborated_fission( + &mut campaign, + &preview, + &transition, + Utc::now(), + ) + .map_err(|error| KernelError::Invalid(error.to_string()))?; + commit_with_records( + store, + row, + campaign, + "elaborate_gestalt_fission", + Vec::new(), + Vec::new(), + model_stage_receipts, + None, + Some((transition, mutation_receipt)), + ) + } WorldCommand::ReconcileFissionCivicBindings { expected_revision } => { require_revision(&campaign, expected_revision)?; let transition = crate::legacy_transition::lower_fission_civic_reconciliation( @@ -1618,6 +1686,7 @@ fn execute( campaign, "materialize_gestalt_member", "direct materialization command", + Vec::new(), ) } WorldCommand::IndividuateGestaltMember { @@ -1634,6 +1703,67 @@ fn execute( campaign, "individuate_gestalt_member", "direct individuation command", + Vec::new(), + ) + } + WorldCommand::ElaborateGestaltIndividuation { + expected_revision, + individuation, + qualification, + model_stage_receipts, + } => { + require_revision(&campaign, expected_revision)?; + if qualification.schema != "ghostlight.world_complexity_individuation_qualification.v1" + || model_stage_receipts.is_empty() + { + return Err(KernelError::Invalid( + "elaborated individuation lacks its exact qualification or model provenance" + .into(), + )); + } + let proposal = crate::elaboration::WorldComplexityProposal::Individuate { + individuation: individuation.clone(), + qualification: qualification.clone(), + }; + if crate::elaboration::unique_containing_jurisdiction( + &campaign, + &BTreeSet::from([individuation.location_id.clone()]), + &BTreeSet::from([qualification.jurisdiction_location_id.clone()]), + ) + .as_deref() + != Some(qualification.jurisdiction_location_id.as_str()) + { + return Err(KernelError::Invalid( + "elaborated individuation escaped its qualified jurisdiction".into(), + )); + } + crate::resolution::validate_gestalt_individuation(&campaign, &individuation) + .and_then(|_| { + crate::elaboration::validate_complexity_individuation_semantics( + &campaign, + qualification.title, + &individuation, + ) + }) + .and_then(|_| { + crate::elaboration::validate_world_complexity_semantic_provenance( + &campaign, + &proposal, + &qualification.semantic, + &model_stage_receipts, + ) + }) + .map_err(|error| KernelError::Invalid(error.to_string()))?; + let before = campaign.clone(); + apply_individuation(&mut campaign, &individuation)?; + commit_gestalt_presence( + store, + row, + before, + campaign, + "elaborate_gestalt_individuation", + "model-qualified world-complexity individuation", + model_stage_receipts, ) } WorldCommand::DematerializeGestaltMember { @@ -1650,6 +1780,7 @@ fn execute( campaign, "dematerialize_gestalt_member", "direct dematerialization command", + Vec::new(), ) } WorldCommand::ReconcileGestaltPresence { @@ -1722,6 +1853,7 @@ fn execute( candidate, "reconcile_gestalt_presence", &reason, + Vec::new(), ) } WorldCommand::ResolveReactionWave { @@ -2235,7 +2367,7 @@ fn apply_promotion( } let member = campaign .gestalt_members - .get_mut(&promotion.member_id) + .get(&promotion.member_id) .ok_or_else(|| KernelError::Invalid("gestalt member is unknown".into()))?; if member.gestalt_id != promotion.gestalt_id || member.version != promotion.expected_member_version @@ -2250,11 +2382,19 @@ fn apply_promotion( )); } let actor_id = crate::domain::gestalt_member_subject_id(&member.id); - if campaign.actors.contains_key(&actor_id) { + if crate::resolution::canonical_subject_id_is_occupied_except_member( + campaign, + &actor_id, + Some(&promotion.member_id), + ) { return Err(KernelError::Invalid( - "materialized actor id collides".into(), + "materialized actor id collides with an established canonical subject identity".into(), )); } + let member = campaign + .gestalt_members + .get_mut(&promotion.member_id) + .ok_or_else(|| KernelError::Invalid("gestalt member is unknown".into()))?; let actor = materialize_actor(&gestalt, member, &actor_id, &promotion.location_id); member.materialized_actor_id = Some(actor_id.clone()); member.last_location_id = Some(promotion.location_id.clone()); @@ -3762,6 +3902,7 @@ fn commit_gestalt_presence( mut campaign: Campaign, kind: &str, reason: &str, + model_receipts: Vec, ) -> Result { let previous_resolution_epoch = campaign.resolution_policy.resolution_epoch; crate::resolution::ensure_agency_profiles(&mut campaign); @@ -3853,7 +3994,14 @@ fn commit_gestalt_presence( }; let key = format!("{}-{}", campaign.id, campaign.revision); store - .append_gestalt_presence(&row, &campaign, &key, &receipt, &gestalt_receipt) + .append_gestalt_presence( + &row, + &campaign, + &key, + &receipt, + &gestalt_receipt, + &model_receipts, + ) .map_err(persist)?; Ok(CommandResult::Committed { campaign, receipt }) } @@ -5301,6 +5449,92 @@ pub(crate) mod tests { assert_eq!(persisted.actors["taren"].name, "Taren"); } + #[test] + fn individuation_validation_and_application_share_the_canonical_subject_namespace() { + let mut value = campaign(); + value.gestalts.insert( + "refugees".into(), + GestaltPersonaState { + schema: "ghostlight.gestalt_persona_state.v1".into(), + id: "refugees".into(), + name: "Refugees".into(), + version: 0, + home_location_id: "room".into(), + shared_capabilities: BTreeSet::new(), + shared_knowledge: BTreeSet::new(), + resources: BTreeSet::new(), + goals: vec![], + pressures: vec![], + }, + ); + crate::resolution::ensure_agency_profiles(&mut value); + let member = GestaltMemberDelta { + schema: "ghostlight.gestalt_member_delta.v1".into(), + id: "relay-volunteer".into(), + gestalt_id: "refugees".into(), + version: 0, + name: "Relay Volunteer".into(), + capability_additions: BTreeSet::new(), + capability_removals: BTreeSet::new(), + knowledge_additions: BTreeSet::new(), + knowledge_removals: BTreeSet::new(), + equipment: BTreeSet::new(), + conditions: BTreeSet::new(), + obligations: BTreeSet::new(), + relationships: BTreeMap::new(), + goals: vec![], + memories: vec![], + last_location_id: Some("room".into()), + materialized_actor_id: None, + last_relevant_revision: 0, + relevance_lease_until_revision: 0, + }; + let actor_id = crate::domain::gestalt_member_subject_id(&member.id); + let mut reservation = value.agency_profiles["refugees"].clone(); + reservation.id = actor_id.clone(); + reservation.subject_id = "detached-profile-subject".into(); + reservation.active_leaf = false; + reservation.simulation_eligible = false; + value + .agency_profiles + .insert("detached-profile-key".into(), reservation); + let individuation = GestaltIndividuation { + gestalt_id: "refugees".into(), + expected_gestalt_version: 0, + member: member.clone(), + location_id: "room".into(), + }; + + let diagnostic = crate::resolution::validate_gestalt_individuation(&value, &individuation) + .unwrap_err() + .to_string(); + assert!(diagnostic.contains("established canonical subject identity")); + + value.gestalt_members.insert(member.id.clone(), member); + let error = apply_promotion( + &mut value, + &GestaltPromotion { + gestalt_id: "refugees".into(), + expected_gestalt_version: 0, + member_id: "relay-volunteer".into(), + expected_member_version: 0, + location_id: "room".into(), + }, + ) + .unwrap_err(); + assert!( + error + .to_string() + .contains("established canonical subject identity") + ); + assert!(!value.actors.contains_key(&actor_id)); + assert!( + value.gestalt_members["relay-volunteer"] + .materialized_actor_id + .is_none() + ); + } + #[test] fn composite_selected_action_commits_atomically_and_rebuilds_derived_lanes() { let mut value = campaign(); diff --git a/crates/ghostlight-dungeon/src/legacy_transition.rs b/crates/ghostlight-dungeon/src/legacy_transition.rs index 0f6813f..f8e9f5e 100644 --- a/crates/ghostlight-dungeon/src/legacy_transition.rs +++ b/crates/ghostlight-dungeon/src/legacy_transition.rs @@ -153,7 +153,28 @@ pub fn lower_fission( preview: &GestaltFissionPreview, expires_at: DateTime, ) -> Result { - crate::resolution::validate_fission(campaign, preview)?; + lower_fission_with_validation(campaign, preview, expires_at, false) +} + +pub fn lower_elaborated_fission( + campaign: &Campaign, + preview: &GestaltFissionPreview, + expires_at: DateTime, +) -> Result { + lower_fission_with_validation(campaign, preview, expires_at, true) +} + +fn lower_fission_with_validation( + campaign: &Campaign, + preview: &GestaltFissionPreview, + expires_at: DateTime, + allow_semantic_additions: bool, +) -> Result { + if allow_semantic_additions { + crate::resolution::validate_elaborated_fission(campaign, preview)?; + } else { + crate::resolution::validate_fission(campaign, preview)?; + } let preview_digest = digest_serializable(preview)?; let source_receipt_id = format!( "fission-preview:{}", @@ -196,6 +217,81 @@ pub fn lower_fission( .collect(), remainder_population: Some(population_subject(&preview.residual_child_id)), }); + if allow_semantic_additions { + for child in &preview.children { + let subject = population_subject(&child.id); + for capability in child + .shared_capabilities + .difference(&campaign.gestalts[&preview.parent_gestalt_id].shared_capabilities) + { + mutations.push(WorldMutation::ChangeCapability { + subject: subject.clone(), + operation: CapabilityMutationOperation::Grant, + capability_id: capability.clone(), + description: Some(capability.clone()), + }); + } + for statement in child + .shared_knowledge + .difference(&campaign.gestalts[&preview.parent_gestalt_id].shared_knowledge) + { + let (proposition, needs_admission) = + elaborated_fission_proposition(campaign, statement)?; + if needs_admission { + mutations.push(WorldMutation::AdmitEntity { + subject: proposition.clone(), + initial_components: BTreeSet::from([ + WorldComponentKind::Knowledge, + WorldComponentKind::PropositionContent, + ]), + initial_place: None, + initial_profile: Some(AdmittedEntityProfile::Proposition { + statement: statement.clone(), + scope: FactScope::ProvisionalLocal, + evidence_receipt_ids: BTreeSet::new(), + discoverable_at_places: BTreeSet::new(), + }), + admission_receipt_id: source_receipt_id.clone(), + }); + } + mutations.push(WorldMutation::ChangeKnowledge { + operation: KnowledgeMutationOperation::Acquire, + proposition, + knower: Some(subject.clone()), + speaker: None, + recipients: Vec::new(), + channel: None, + }); + } + for goal in child + .goals + .iter() + .skip(campaign.gestalts[&preview.parent_gestalt_id].goals.len()) + { + mutations.push(WorldMutation::ChangeCommitment { + subject: subject.clone(), + operation: CommitmentMutationOperation::Create, + kind: CommitmentKind::Goal, + commitment_id: format!("goal:elaboration:{}", short_digest(goal)), + counterparty: None, + description: Some(goal.clone()), + }); + } + for pressure in child.pressures.iter().skip( + campaign.gestalts[&preview.parent_gestalt_id] + .pressures + .len(), + ) { + mutations.push(WorldMutation::ChangePressure { + pressure: gestalt_pressure_subject(&child.id, pressure), + owner: subject.clone(), + operation: PressureMutationOperation::Create, + amount: Some(4), + label: Some(pressure.clone()), + }); + } + } + } for (resource, child_id) in &preview.resource_child_assignments { mutations.push(WorldMutation::TransferCustody { resource: resource_subject(campaign, &parent, resource), @@ -227,6 +323,11 @@ pub fn lower_fission( system, }); } + let effect_ceiling = if allow_semantic_additions { + "Admit the approved child populations, preserve their inherited baseline, add only each child's explicitly qualified capabilities, knowledge, goals, and pressures, partition each exact resource once, transfer each named member once, and record one population lineage split." + } else { + "Admit the approved child populations, preserve their inherited baseline, partition each exact resource once, transfer each named member once, and record one population lineage split." + }; lower_exact_mutations( campaign, mutations, @@ -234,7 +335,7 @@ pub fn lower_fission( None, MutationOutcomeBinding::Deterministic, Some(campaign.resolution_policy.resolution_epoch), - "Admit the approved child populations, preserve their inherited baseline, partition each exact resource once, transfer each named member once, and record one population lineage split.", + effect_ceiling, &source_receipt_id, Some(digest_serializable(&( "fission", @@ -246,6 +347,51 @@ pub fn lower_fission( ) } +fn elaborated_fission_proposition( + campaign: &Campaign, + statement: &str, +) -> Result<(SubjectRef, bool)> { + if let Some(fact) = campaign + .facts + .values() + .find(|fact| fact.statement == statement) + { + return Ok((proposition_subject(&fact.id), false)); + } + let already_known = campaign + .actors + .values() + .any(|actor| actor.knowledge.contains(statement)) + || campaign + .gestalts + .values() + .any(|gestalt| gestalt.shared_knowledge.contains(statement)) + || campaign.gestalt_members.values().any(|member| { + member.knowledge_additions.contains(statement) + && !member.knowledge_removals.contains(statement) + }); + if already_known { + return Ok(( + proposition_subject(&format!("legacy-knowledge:{}", short_digest(statement))), + false, + )); + } + let fact_id = format!( + "fact:complexity-fission:{:x}", + Sha256::digest(statement.as_bytes()) + ); + if let Some(existing) = campaign.facts.get(&fact_id) { + return if existing.statement == statement { + Ok((proposition_subject(&fact_id), false)) + } else { + Err(anyhow!( + "content-addressed complexity knowledge collided with another fact" + )) + }; + } + Ok((proposition_subject(&fact_id), true)) +} + fn fission_civic_system_rebindings( campaign: &Campaign, preview: &GestaltFissionPreview, @@ -425,7 +571,30 @@ pub fn apply_lowered_fission( transition: &LoweredLegacyTransition, now: DateTime, ) -> Result { - crate::resolution::validate_fission(campaign, preview)?; + apply_lowered_fission_with_validation(campaign, preview, transition, now, false) +} + +pub fn apply_lowered_elaborated_fission( + campaign: &mut Campaign, + preview: &GestaltFissionPreview, + transition: &LoweredLegacyTransition, + now: DateTime, +) -> Result { + apply_lowered_fission_with_validation(campaign, preview, transition, now, true) +} + +fn apply_lowered_fission_with_validation( + campaign: &mut Campaign, + preview: &GestaltFissionPreview, + transition: &LoweredLegacyTransition, + now: DateTime, + allow_semantic_additions: bool, +) -> Result { + if allow_semantic_additions { + crate::resolution::validate_elaborated_fission(campaign, preview)?; + } else { + crate::resolution::validate_fission(campaign, preview)?; + } let snapshot = component_snapshot(campaign)?; let application = apply_component_world_batch(&snapshot, &transition.authority, &transition.batch, now)?; @@ -962,6 +1131,116 @@ fn project_accepted_region_expansion( Ok(()) } +fn verify_fission_child_components( + next: &ComponentWorldState, + child: &GestaltPersonaState, +) -> Result<()> { + let subject = population_subject(&child.id); + let capabilities = next + .capabilities + .iter() + .filter(|(key, value)| key.subject == subject && !value.suspended) + .map(|(key, value)| { + if value.description != key.entry_id { + return Err(anyhow!( + "accepted fission capability identity diverged from its description" + )); + } + Ok(key.entry_id.clone()) + }) + .collect::>>()?; + let knowledge = next + .knowledge + .iter() + .filter(|(key, value)| key.knower == subject && value.status == "known") + .map(|(key, _)| { + next.propositions + .get(&key.proposition) + .map(|value| value.statement.clone()) + .ok_or_else(|| anyhow!("accepted fission knowledge lost proposition content")) + }) + .collect::>>()?; + let goals = next + .commitments + .iter() + .filter(|(key, value)| { + key.subject == subject && value.kind == CommitmentKind::Goal && value.status == "active" + }) + .map(|(_, value)| value.description.clone()) + .collect::>(); + let pressures = next + .pressures + .values() + .filter(|value| value.owner == subject && !value.resolved) + .map(|value| value.label.clone()) + .collect::>(); + if capabilities != child.shared_capabilities + || knowledge != child.shared_knowledge + || goals != child.goals.iter().cloned().collect() + || pressures != child.pressures.iter().cloned().collect() + { + return Err(anyhow!( + "accepted fission component semantics diverged from the approved child" + )); + } + Ok(()) +} + +fn project_elaborated_fission_facts( + campaign: &mut Campaign, + preview: &GestaltFissionPreview, + next: &ComponentWorldState, +) -> Result<()> { + let parent = campaign + .gestalts + .get(&preview.parent_gestalt_id) + .ok_or_else(|| anyhow!("accepted complexity fact lost its fission parent"))?; + let mut admitted = BTreeMap::new(); + for child in &preview.children { + let knower = population_subject(&child.id); + for statement in child.shared_knowledge.difference(&parent.shared_knowledge) { + let (proposition_id, needs_admission) = + elaborated_fission_proposition(campaign, statement)?; + if !needs_admission { + continue; + } + let knowledge_key = KnowledgeKey { + knower: knower.clone(), + proposition: proposition_id.clone(), + }; + if !next.knowledge.contains_key(&knowledge_key) { + return Err(anyhow!( + "accepted complexity fact was not acquired by its approved child" + )); + } + let proposition = next + .propositions + .get(&proposition_id) + .ok_or_else(|| anyhow!("accepted complexity fact lost proposition content"))?; + admitted.insert( + proposition_id.id.clone(), + WorldFact { + id: proposition_id.id.clone(), + statement: proposition.statement.clone(), + scope: proposition.scope.clone(), + evidence_receipt_ids: proposition + .evidence_receipt_ids + .iter() + .cloned() + .collect(), + discoverable_at_location_ids: proposition + .discoverable_at_places + .iter() + .map(|place| place.id.clone()) + .collect(), + }, + ); + } + } + campaign.facts.extend(admitted); + Ok(()) +} + fn project_accepted_fission( campaign: &mut Campaign, preview: &GestaltFissionPreview, @@ -982,6 +1261,7 @@ fn project_accepted_fission( }) .cloned() .collect::>(); + project_elaborated_fission_facts(campaign, preview, next)?; for approved in &preview.children { let subject = population_subject(&approved.id); @@ -995,21 +1275,12 @@ fn project_accepted_fission( .get(&format!("identity:canonical:{}", approved.id)) .filter(|identity| identity.subject == subject && identity.active) .ok_or_else(|| anyhow!("accepted fission child lacks exact identity"))?; - campaign.gestalts.insert( - approved.id.clone(), - GestaltPersonaState { - schema: "ghostlight.gestalt_persona_state.v1".into(), - id: approved.id.clone(), - name: identity.value.clone(), - version: 0, - home_location_id: place.id.clone(), - shared_capabilities: parent.shared_capabilities.clone(), - shared_knowledge: parent.shared_knowledge.clone(), - resources: BTreeSet::new(), - goals: parent.goals.clone(), - pressures: parent.pressures.clone(), - }, - ); + verify_fission_child_components(next, approved)?; + let mut accepted = approved.clone(); + accepted.name = identity.value.clone(); + accepted.home_location_id = place.id.clone(); + accepted.resources.clear(); + campaign.gestalts.insert(approved.id.clone(), accepted); } let member_ids = campaign @@ -3888,6 +4159,111 @@ mod tests { .unwrap(); } + #[test] + fn elaborated_fission_commits_qualified_child_semantics_in_one_typed_batch() { + let mut campaign = campaign(); + let inherited_knowledge = campaign.facts["fact:route"].statement.clone(); + let parent = GestaltPersonaState { + schema: "ghostlight.gestalt_persona_state.v1".into(), + id: "residents".into(), + name: "Residents".into(), + version: 0, + home_location_id: "room".into(), + shared_capabilities: BTreeSet::from(["keep the public stair open".into()]), + shared_knowledge: BTreeSet::from([inherited_knowledge.clone()]), + resources: BTreeSet::new(), + goals: vec!["keep the hall inhabited".into()], + pressures: vec!["the night watch is thin".into()], + }; + campaign.gestalts.insert(parent.id.clone(), parent.clone()); + crate::resolution::ensure_agency_profiles(&mut campaign); + + let situated_knowledge = + "The eastern lintel rings when the sealed cistern changes pressure.".to_owned(); + let mut specific = parent.clone(); + specific.id = "lintel-keepers".into(); + specific.name = "Lintel Keepers".into(); + specific + .shared_capabilities + .insert("sound the lintel before opening the cistern".into()); + specific.shared_knowledge.insert(situated_knowledge.clone()); + specific + .goals + .push("win custody of the eastern cistern key".into()); + specific + .pressures + .push("the cistern key is promised to two rival crews".into()); + let mut residual = parent.clone(); + residual.id = "residents-unresolved".into(); + residual.name = "Unresolved Residents".into(); + let preview = GestaltFissionPreview { + schema: "ghostlight.gestalt_fission_preview.v1".into(), + campaign_id: campaign.id, + expected_world_revision: campaign.revision, + parent_gestalt_id: parent.id.clone(), + partition_axis: crate::domain::AgencyAxis::Ideology, + children: vec![specific.clone(), residual], + child_partition_values: BTreeMap::from([ + (specific.id.clone(), "lintel custody".into()), + ("residents-unresolved".into(), "other/unknown".into()), + ]), + residual_child_id: "residents-unresolved".into(), + member_child_assignments: BTreeMap::new(), + resource_child_assignments: BTreeMap::new(), + evidence_receipt_ids: vec![], + gaps: vec![], + canon_candidates: vec![], + requires_approval: true, + }; + + assert!(lower_fission(&campaign, &preview, Utc::now() + Duration::minutes(5)).is_err()); + let transition = + lower_elaborated_fission(&campaign, &preview, Utc::now() + Duration::minutes(5)) + .unwrap(); + assert!(transition.batch.mutations.iter().any(|mutation| matches!( + mutation.mutation, + WorldMutation::AdmitEntity { + initial_profile: Some(AdmittedEntityProfile::Proposition { .. }), + .. + } + ))); + assert!( + transition.batch.mutations.iter().any(|mutation| matches!( + mutation.mutation, + WorldMutation::ChangeCapability { .. } + )) + ); + assert!( + transition + .batch + .mutations + .iter() + .any(|mutation| matches!(mutation.mutation, WorldMutation::ChangeKnowledge { .. })) + ); + assert!( + transition.batch.mutations.iter().any(|mutation| matches!( + mutation.mutation, + WorldMutation::ChangeCommitment { .. } + )) + ); + assert!( + transition + .batch + .mutations + .iter() + .any(|mutation| matches!(mutation.mutation, WorldMutation::ChangePressure { .. })) + ); + + apply_lowered_elaborated_fission(&mut campaign, &preview, &transition, Utc::now()).unwrap(); + assert_eq!(campaign.gestalts[&specific.id], specific); + assert!(campaign.facts.values().any(|fact| { + fact.statement == situated_knowledge + && fact.scope == crate::domain::FactScope::ProvisionalLocal + })); + crate::transition::validate_component_world(&component_snapshot(&campaign).unwrap()) + .unwrap(); + } + #[test] fn shared_learning_updates_the_materialized_member_projection() { let mut campaign = campaign(); diff --git a/crates/ghostlight-dungeon/src/main.rs b/crates/ghostlight-dungeon/src/main.rs index d2e2f03..e437e71 100644 --- a/crates/ghostlight-dungeon/src/main.rs +++ b/crates/ghostlight-dungeon/src/main.rs @@ -5498,12 +5498,14 @@ fn player_http_command_allowed(command: &WorldCommand, player_actor_id: &str) -> | WorldCommand::MaterializeGestaltMember { .. } | WorldCommand::DematerializeGestaltMember { .. } | WorldCommand::IndividuateGestaltMember { .. } + | WorldCommand::ElaborateGestaltIndividuation { .. } | WorldCommand::ReconcileGestaltPresence { .. } | WorldCommand::ResolveReactionWave { .. } | WorldCommand::ResolveNpcAction { .. } | WorldCommand::SetProviderParallelism { .. } | WorldCommand::ReplaceResolutionPins { .. } | WorldCommand::FissionGestalt { .. } + | WorldCommand::ElaborateGestaltFission { .. } | WorldCommand::ReconcileFissionCivicBindings { .. } | WorldCommand::BindClockConsequences { .. } => false, } diff --git a/crates/ghostlight-dungeon/src/persistence.rs b/crates/ghostlight-dungeon/src/persistence.rs index 2422ad8..7b571dd 100644 --- a/crates/ghostlight-dungeon/src/persistence.rs +++ b/crates/ghostlight-dungeon/src/persistence.rs @@ -1174,6 +1174,7 @@ impl CampaignStore { receipt_key: &str, world_receipt: &WorldCommitReceipt, gestalt_receipt: &GestaltMaterializationReceipt, + model_receipts: &[crate::model::ModelStageReceipt], ) -> Result { let next_row = envelope( &expected.r#type, @@ -1181,7 +1182,7 @@ impl CampaignStore { &expected.key, next, )?; - let rows = vec![ + let mut rows = vec![ next_row.clone(), envelope( "world_commit_receipt.v1", @@ -1196,10 +1197,34 @@ impl CampaignStore { gestalt_receipt, )?, ]; - if !self - .inner - .compare_and_swap_batch(std::slice::from_ref(expected), rows)? - { + let mut expected_rows = vec![expected.clone()]; + let mut unique_model_receipts = BTreeMap::::new(); + for item in model_receipts { + if let Some(existing) = unique_model_receipts.get(item.storage_key()) { + if !existing.same_receipted_content(item) { + return Err(anyhow!( + "immutable model-stage receipt conflict: {}", + item.storage_key() + )); + } + continue; + } + unique_model_receipts.insert(item.storage_key().to_owned(), item); + } + for item in unique_model_receipts.into_values() { + if let Some(existing) = self.matching_model_stage_receipt(item)? { + expected_rows.push(existing.clone()); + rows.push(existing); + } else { + rows.push(envelope( + "persona_stage_receipt.v1", + "ghostlight.persona_stage_receipt.v1", + item.storage_key(), + item, + )?); + } + } + if !self.inner.compare_and_swap_batch(&expected_rows, rows)? { return Err(anyhow!("stale CultCache snapshot")); } Ok(next_row) diff --git a/crates/ghostlight-dungeon/src/resolution.rs b/crates/ghostlight-dungeon/src/resolution.rs index 5646f2e..cd4b053 100644 --- a/crates/ghostlight-dungeon/src/resolution.rs +++ b/crates/ghostlight-dungeon/src/resolution.rs @@ -18,6 +18,38 @@ pub fn information_channel_is_concrete(channel: &str) -> bool { !channel.is_empty() && channel.len() <= 160 && !channel.eq_ignore_ascii_case("unknown") } +/// Canonical world subjects and their agency projections share one ID space. +/// Keep this predicate deterministic and side-effect free: validators, rebases, +/// and kernel application all consult the same occupied-set definition. +pub(crate) fn canonical_subject_id_is_occupied(campaign: &Campaign, subject_id: &str) -> bool { + canonical_subject_id_is_occupied_except_member(campaign, subject_id, None) +} + +pub(crate) fn canonical_subject_id_is_occupied_except_member( + campaign: &Campaign, + subject_id: &str, + permitted_member_id: Option<&str>, +) -> bool { + let permitted_member_id = + permitted_member_id.map(crate::domain::canonical_gestalt_member_local_id); + campaign.actors.contains_key(subject_id) + || campaign.institutions.contains_key(subject_id) + || campaign.gestalts.contains_key(subject_id) + || campaign.gestalt_members.iter().any(|(member_id, member)| { + permitted_member_id.as_deref() + != Some(crate::domain::canonical_gestalt_member_local_id(member_id).as_str()) + && crate::domain::gestalt_member_subject_id(&member.id) == subject_id + }) + || campaign + .agency_profiles + .iter() + .any(|(profile_key, profile)| { + profile_key == subject_id + || profile.id == subject_id + || profile.subject_id == subject_id + }) +} + #[derive(Clone, Debug)] struct Candidate { left: String, @@ -2258,6 +2290,27 @@ pub fn validate_gestalt_individuation( if member.name.chars().count() > 160 { findings.push("member name exceeds 160 characters".to_owned()); } + let public_name_key = public_identity_key(&member.name); + if public_name_key.is_empty() { + findings.push("member name has no public identity characters".to_owned()); + } + if campaign + .actors + .values() + .any(|actor| public_identity_key(&actor.name) == public_name_key) + && !public_name_key.is_empty() + { + findings.push("member name duplicates an established public Actor identity".to_owned()); + } + if campaign + .gestalt_members + .values() + .any(|existing| public_identity_key(&existing.name) == public_name_key) + && !public_name_key.is_empty() + { + findings + .push("member name duplicates an established public population identity".to_owned()); + } if member.goals.len() > 8 { findings.push("member has more than eight goals".to_owned()); } @@ -2278,11 +2331,11 @@ pub fn validate_gestalt_individuation( { findings.push("member id duplicates an established population identity".to_owned()); } - if campaign - .actors - .contains_key(&crate::domain::gestalt_member_subject_id(&member.id)) - { - findings.push("member id duplicates an established Actor identity".to_owned()); + if canonical_subject_id_is_occupied( + campaign, + &crate::domain::gestalt_member_subject_id(&member.id), + ) { + findings.push("member id duplicates an established canonical subject identity".to_owned()); } let unsupported_relationships = member .relationships @@ -2313,6 +2366,17 @@ pub fn validate_gestalt_individuation( } } +/// Public names are an interface identity, not merely decorative text. Fold +/// case and punctuation so spelling variants such as `Flint-Measure` and +/// `Flintmeasure` cannot silently create two apparently identical people. +pub fn public_identity_key(value: &str) -> String { + value + .chars() + .filter(|character| character.is_alphanumeric()) + .flat_map(char::to_lowercase) + .collect() +} + pub(crate) fn validate_strategic_individuation_proposals( campaign: &Campaign, proposals: &[StrategicGestaltIndividuation], @@ -3337,6 +3401,9 @@ pub(crate) fn project_fission_resolution( campaign.gestalts.insert(child.id.clone(), child.clone()); let mut profile = profile_for_gestalt(child, &preview.evidence_receipt_ids); profile.parent_subject_id = Some(preview.parent_gestalt_id.clone()); + if child.id == preview.residual_child_id { + profile.simulation_eligible = false; + } profile.facets = inherited.facets.clone(); profile .facets @@ -3365,6 +3432,36 @@ pub(crate) fn project_fission_resolution( } pub fn validate_fission(campaign: &Campaign, preview: &GestaltFissionPreview) -> Result<()> { + validate_fission_shape(campaign, preview, false) +} + +pub fn validate_elaborated_fission( + campaign: &Campaign, + preview: &GestaltFissionPreview, +) -> Result<()> { + validate_fission_shape(campaign, preview, true) +} + +fn preserves_inherited_prefix_with_unique_additions( + values: &[String], + inherited: &[String], +) -> bool { + if !values.starts_with(inherited) { + return false; + } + let inherited_len = inherited.len(); + let inherited = inherited.iter().collect::>(); + let mut additions = BTreeSet::new(); + values[inherited_len..] + .iter() + .all(|value| !inherited.contains(value) && additions.insert(value)) +} + +fn validate_fission_shape( + campaign: &Campaign, + preview: &GestaltFissionPreview, + allow_semantic_additions: bool, +) -> Result<()> { let parent = campaign .gestalts .get(&preview.parent_gestalt_id) @@ -3457,8 +3554,8 @@ pub fn validate_fission(campaign: &Campaign, preview: &GestaltFissionPreview) -> format!("child {label} version is not zero"), ); require( - !campaign.gestalts.contains_key(&child.id), - format!("child {label} already exists"), + !canonical_subject_id_is_occupied(campaign, &child.id), + format!("child {label} collides with an established canonical subject identity"), ); require( campaign.locations.contains_key(&child.home_location_id), @@ -3470,20 +3567,45 @@ pub fn validate_fission(campaign: &Campaign, preview: &GestaltFissionPreview) -> format!("child {label} changed home outside a geography fission"), ); require( - child.shared_capabilities == parent.shared_capabilities, - format!("child {label} changed inherited capabilities"), + if allow_semantic_additions { + child + .shared_capabilities + .is_superset(&parent.shared_capabilities) + } else { + child.shared_capabilities == parent.shared_capabilities + }, + format!("child {label} changed inherited capabilities outside its authority"), ); require( - child.shared_knowledge == parent.shared_knowledge, - format!("child {label} changed inherited knowledge"), + if allow_semantic_additions { + child.shared_knowledge.is_superset(&parent.shared_knowledge) + } else { + child.shared_knowledge == parent.shared_knowledge + }, + format!("child {label} changed inherited knowledge outside its authority"), ); require( - child.goals == parent.goals, - format!("child {label} changed inherited goals"), + if allow_semantic_additions { + preserves_inherited_prefix_with_unique_additions(&child.goals, &parent.goals) + } else { + child.goals == parent.goals + }, + format!( + "child {label} must preserve inherited goals as an exact prefix and add no duplicates" + ), ); require( - child.pressures == parent.pressures, - format!("child {label} changed inherited pressures"), + if allow_semantic_additions { + preserves_inherited_prefix_with_unique_additions( + &child.pressures, + &parent.pressures, + ) + } else { + child.pressures == parent.pressures + }, + format!( + "child {label} must preserve inherited pressures as an exact prefix and add no duplicates" + ), ); require( preview.child_partition_values.contains_key(&child.id), @@ -3659,6 +3781,118 @@ pub(crate) mod tests { value } + #[test] + fn fission_validation_uses_the_unified_canonical_subject_id_namespace() { + let mut value = campaign(1, 1); + value.gestalts.insert( + "villagers".into(), + GestaltPersonaState { + schema: "ghostlight.gestalt_persona_state.v1".into(), + id: "villagers".into(), + name: "Villagers".into(), + version: 0, + home_location_id: "center".into(), + shared_capabilities: BTreeSet::new(), + shared_knowledge: BTreeSet::new(), + resources: BTreeSet::new(), + goals: vec![], + pressures: vec![], + }, + ); + ensure_agency_profiles(&mut value); + let mut detached_profile = value.agency_profiles["villagers"].clone(); + detached_profile.id = "reserved-profile-id".into(); + detached_profile.subject_id = "reserved-profile-subject".into(); + detached_profile.active_leaf = false; + detached_profile.simulation_eligible = false; + value + .agency_profiles + .insert("reserved-profile-key".into(), detached_profile); + value.gestalt_members.insert( + "reserved-member".into(), + GestaltMemberDelta { + schema: "ghostlight.gestalt_member_delta.v1".into(), + id: "reserved-member".into(), + gestalt_id: "villagers".into(), + version: 0, + name: "Reserved Member".into(), + capability_additions: BTreeSet::new(), + capability_removals: BTreeSet::new(), + knowledge_additions: BTreeSet::new(), + knowledge_removals: BTreeSet::new(), + equipment: BTreeSet::new(), + conditions: BTreeSet::new(), + obligations: BTreeSet::new(), + relationships: BTreeMap::new(), + goals: vec![], + memories: vec![], + last_location_id: Some("center".into()), + materialized_actor_id: None, + last_relevant_revision: 0, + relevance_lease_until_revision: 0, + }, + ); + + let child = |id: &str, name: &str| GestaltPersonaState { + schema: "ghostlight.gestalt_persona_state.v1".into(), + id: id.into(), + name: name.into(), + version: 0, + home_location_id: "center".into(), + shared_capabilities: BTreeSet::new(), + shared_knowledge: BTreeSet::new(), + resources: BTreeSet::new(), + goals: vec![], + pressures: vec![], + }; + let preview = GestaltFissionPreview { + schema: "ghostlight.gestalt_fission_preview.v1".into(), + campaign_id: value.id, + expected_world_revision: value.revision, + parent_gestalt_id: "villagers".into(), + partition_axis: AgencyAxis::Ideology, + children: vec![ + child("new-villagers", "New villagers"), + child("other", "Other"), + ], + child_partition_values: BTreeMap::from([ + ("new-villagers".into(), "new".into()), + ("other".into(), "other/unknown".into()), + ]), + residual_child_id: "other".into(), + member_child_assignments: BTreeMap::new(), + resource_child_assignments: BTreeMap::new(), + evidence_receipt_ids: vec![], + gaps: vec![], + canon_candidates: vec![], + requires_approval: true, + }; + + for occupied_id in [ + "player", + "faction-0000", + "villagers", + "reserved-profile-key", + "reserved-profile-id", + "reserved-profile-subject", + "member:reserved-member", + ] { + let mut colliding = preview.clone(); + colliding.children[0].id = occupied_id.into(); + colliding.child_partition_values.remove("new-villagers"); + colliding + .child_partition_values + .insert(occupied_id.into(), "new".into()); + let diagnostic = validate_fission(&value, &colliding) + .unwrap_err() + .to_string(); + assert!( + diagnostic.contains("collides with an established canonical subject identity"), + "{occupied_id} escaped the canonical subject namespace: {diagnostic}" + ); + } + } + #[test] fn profile_maintenance_never_promotes_knowledge_into_public_channels() { let mut value = campaign(1, 1); @@ -5104,6 +5338,8 @@ pub(crate) mod tests { assert!(!value.agency_profiles["villagers"].active_leaf); assert!(value.agency_profiles["traditionalists"].active_leaf); assert!(value.agency_profiles["other"].active_leaf); + assert!(value.agency_profiles["traditionalists"].simulation_eligible); + assert!(!value.agency_profiles["other"].simulation_eligible); assert!(value.gestalts["villagers"].resources.is_empty()); assert!(value.gestalts["traditionalists"].resources.is_empty()); assert_eq!( @@ -5189,6 +5425,8 @@ pub(crate) mod tests { .unwrap(); assert!(!value.agency_profiles["other"].active_leaf); assert!(value.agency_profiles["other-smiths"].active_leaf); + assert!(value.agency_profiles["other-smiths"].simulation_eligible); + assert!(!value.agency_profiles["other-unknown"].simulation_eligible); assert!(value.gestalts["other"].resources.is_empty()); assert!(value.gestalts["other-smiths"].resources.is_empty()); assert_eq!( diff --git a/notes/ghostlight-current-system-map.md b/notes/ghostlight-current-system-map.md index 5c48dd6..b3f2f68 100644 --- a/notes/ghostlight-current-system-map.md +++ b/notes/ghostlight-current-system-map.md @@ -1292,8 +1292,8 @@ outside this authority. - **Outputs:** one frozen ordered `world-regions-plan.json`, one immutable proposal checkpoint before each mutation, one sequential kernel commit per genuinely new realm, and an immutable `world-region-NN-checkpoint.json` - binding request, origin, committed jurisdiction, kernel receipt, and - model-receipt hashes. Committed + binding request, origin, committed jurisdiction, exact compiler mutation + proof, kernel receipt, and model-receipt hashes. Committed jurisdiction IDs become the locality set for later elaboration and strategic simulation. - **Derived state:** `status.json` progress and the jurisdiction list are @@ -1309,8 +1309,10 @@ outside this authority. - **Shared paths:** fresh and resumed runs use the same frozen ordered plan, destination compiler, and kernel admission path. Resume rejects omitted or reordered requests, skips a region only when its checkpoint and canonical - receipt agree, and recovers the kernel-commit/checkpoint crash interval from - the immutable proposal plus canonical campaign and commit receipt. + receipt and mutation proof agree. Foundation, region, titled, and complexity + recovery all bind the immutable prepared candidate to the exact canonical + receipt; the presence of a plausible ID or checkpoint file is never treated + as proof that its content committed. - **Cut line:** whole-world scope is not overloaded onto the root compiler. The driver reuses existing destination compilation and kernel admission; there is no second region generator or event authority. @@ -1322,335 +1324,185 @@ outside this authority. Runs 64 and 65 stopped before that claim was established and remain failure evidence rather than accepted full-world runs. -#### Iterative latent-world complexity and elaborator continuity +#### Qualified latent-world complexity and elaborator continuity -- **Owner:** the strategic acceptance driver owns round orchestration, - deterministic titled-elaborator scheduling, checkpoint publication, and - bounded termination. `WorldScaleIntent` is consumer-authored; - `derive_world_elaboration_demand` deterministically owns the target, deficit, - realm shares, and per-round mutation pressure. Each titled elaborator owns - only its compact title-by-realm working frontier and mutation draft. The - acceptance driver owns deterministic parent-to-jurisdiction routing. After a - frozen parallel batch it also owns exact member-ID collision supersession by - retaining the lowest dispatch ordinal. - `WorldComplexityTool` owns construction of the complete frozen proposal from - the draft. `WorldKernel` remains the sole canonical mutation owner, and - Resolution remains the owner of the later active cover. -- **Inputs:** the admitted campaign after region and locality elaboration; its - active-cell entitlement; a target cover ratio; equal acceptance-driver realm - weights; a weighted titled-elaborator profile; bounded parallelism and round - limits; active, simulation-eligible Gestalt leaves without a currently - materialized member; the prior title-by-realm session checkpoint; and the - exact frozen parent state, members, profile, and local relations for each assigned - mutation. Scheduler title deterministically selects the proposal lane: - Hearth and Veil individuate one consequential member; the other six titles - fission the assigned population along their fixed agency axis. The model sees - the assigned parent projection and compacted session, but its action schema - admits only the lane-specific delta. A fission draft contains child IDs, - names, partition values, the residual child, and exact member/resource - assignments. An individuation draft is a compact - `WorldComplexityMemberDraft`: the model owns only the proposed local ID, - public name, capability and knowledge deltas, equipment, conditions, - obligations, exact-ID relationships, goals, and memories. - `elaboration::unique_containing_jurisdiction` is the single read-only owner - for deriving parent jurisdiction from canonical topology. For every exact - operating place in `AgencyProfile.location_ids`, it walks - `Location.container_id` ancestry until it reaches a consumer realm named by - `realm_subject_targets`. Exactly one - distinct realm target returns that jurisdiction. Zero targets or multiple - distinct targets return no jurisdiction; a containment cycle terminates that - ancestry walk without inventing one. Strategic candidate allocation and - title-by-realm session routing call this same resolver, and - `ModelWorldComplexityWorker` constructor admission calls it again against the - assigned jurisdiction before accepting the parent-to-jurisdiction map. -- **Outputs:** each round publishes an immutable frozen preview with demand, - wave and schedule receipts, exact parent bindings, proposals, and model - receipt hashes; then one immutable mutation checkpoint per sequentially - admitted fission or individuation; one compacted checkpoint for each title - that obtained an admitted mutation in that round; and a terminal round - checkpoint carrying the scheduler state, mutation paths, immutable - superseded-invocation checkpoint paths for same-batch duplicate member IDs, - the accumulated - title-by-realm session checkpoints keyed by stable - `:` session ID, and remeasured - actionable-subject count. Per-round session filenames contain the title plus - a stable digest suffix of that session ID; filenames are storage addresses, - while the map key and checkpoint's own `session_id` carry routing identity. - The final count and round reports are acceptance metadata, not canonical - world state. -- **Derived state:** actionable complexity is the count of canonical active, - simulation-eligible agency leaves. Dormant member rows, census texture, - model calls, dispatches, mutation budget, and compacted prose do not satisfy - the target. Inherited child capabilities, knowledge, goals, pressures, - evidence fields, campaign/revision binding, approval requirement, parent - version, partition axis, and individuation location are deterministic - projections of the frozen assignment, not model-authored state. An - individuated member's public name is presentation state attached to its stable local - member ID and canonical `member:` subject ID; name equality does - not merge subjects, resolve an address, or grant authority over another - Actor or population member. - checkpoint's title, jurisdiction, frontier summary, unresolved leads, - rejection findings, digest chain, and recent commit IDs are bounded steering - memory. Raw provider sessions, request history, and transcripts are neither - continuity nor world state; the checkpoint is the only carried elaborator - context, while the campaign and kernel commit receipts remain truth. - Resolution cover membership and Nemesis decision windows are downstream - derivations and do not feed back into complexity admission authority. - Same-batch individuation supersession is scheduling evidence, not canonical - member state: the earliest dispatch ordinal for one canonicalized local - member ID is retained, while later matching invocations contribute no commit - or session-memory journal. The following round derives fresh demand from the - admitted campaign rather than treating supersession as completed complexity. -- **Forbidden writers:** elaborators, session compaction, the scheduler, - demand derivation, preview files, round files, and acceptance metadata cannot - write campaign state, declare a target satisfied, allocate the Resolution - cover, or turn narrative memory into facts. A fission proposal cannot approve - or commit itself, invent evidence, promote raw roster rows, or rewrite a - parent. An individuation cannot invent a population, duplicate an existing - exact actor or member ID, materialize itself, or acquire relationships to - nonexistent subject IDs. Public names are not writers of identity or - authority: no lookup, relationship, action target, or admission decision may - select or merge a subject by display-name equality. The model cannot emit or - override the member schema, parent Gestalt ID, version, exact location, - materialization state, or relevance revisions because those deterministic - fields are absent from `WorldComplexityMemberDraft`. The model cannot restate - or alter inherited child - Persona constitutions, campaign identity, parent identity/version, location, - partition axis, evidence, or approval authority because those fields do not - exist in its mutation-draft schema. The model provider cannot select a realm, - merge title sessions, persist a transcript as memory, or redirect a - checkpoint to another title or jurisdiction. Neither the proposing model nor - commit order may choose the winner of a same-batch exact member-ID collision; - later duplicate invocations cannot write commits or compaction journals. - Sequential commits may update - only the optimistic revision of a frozen fission proposal after the exact - parent binding remains unchanged; individuation instead revalidates its exact - Gestalt version and location against current state. -- **Shared paths:** all round workers use the existing generic elaborator - dispatcher and weighted schedule. `WorldComplexityTool` first lowers a - compact draft into the existing full proposal algebra. For fission, it clones - the frozen parent Persona into every child, replaces only child identity, - name, version, and assigned resources, builds the child-partition map, - attaches the exact campaign/revision/parent/axis and empty - evidence/gap/canon fields, and fixes `requires_approval=true`. For - individuation, it lowers `WorldComplexityMemberDraft` into the canonical - `GestaltMemberDelta`: it fixes the member schema, parent Gestalt ID, version - zero, exact first parent-profile location, unmaterialized state, and zeroed - relevance revisions around the model-owned semantic member content. Every - constructed proposal then passes the existing fission or individuation - validator and the existing - `WorldCommand::FissionGestalt` or `WorldCommand::IndividuateGestaltMember` - mailbox. `resolution::validate_gestalt_individuation` is the single local - authority for exact member-ID uniqueness, parent version, exact location, - bounds, and exact relationship subject IDs; strategic action-bound selection - and direct kernel admission both reuse it. Canonical member addressing uses - `canonical_gestalt_member_local_id` and `gestalt_member_subject_id`, while - strategic actions and relationships carry exact subject IDs rather than - public names. It accumulates every failed deterministic clause in - one rejection so the generic agent loop can repair the exact bounded draft in - its next semantic step; the tool does not add a second judge or relax kernel - admission. Direct individuation then follows the ordinary - presence path: `apply_individuation` inserts the canonical member delta and - promotes it into an Actor, `commit_gestalt_presence` calls - `ensure_agency_profiles`, and the new active simulation-eligible actor profile - becomes visible to `canonical_actionable_subject_count`. Parallel workers - read one frozen campaign. Before serial admission, the round runner - canonicalizes every proposed local member ID, retains the invocation with the - earliest dispatch ordinal for each ID, and publishes one immutable - `ghostlight.complexity_superseded_invocation.v1` checkpoint for every later - duplicate, binding its dispatch, retained ordinal, canonical member ID, and - diagnostic. Only retained invocations enter session routing, kernel commit, - and successful compaction journals. IDs already present in the frozen - canonical campaign remain ordinary semantic failures owned by - `validate_gestalt_individuation`; supersession does not weaken that check. - Retained commits serialize through WorldKernel. Unrelated - earlier fissions may be rebased, - while any change to the assigned parent, its profile, or its members rejects - the later proposal instead of silently reconciling it. - Compaction runs only after successful mutations have produced admitted commit - journals routed by the invocation's stable title-by-realm session ID. The - resulting checkpoint is stored under that ID and supplied only when a later - dispatch has the same title and deterministically derived jurisdiction. - The existing `ElaboratorSessionCompactionTool` owns the model-boundary - compaction contract: its action schema requires a nonempty frontier summary - of at most 4,000 characters, at most 32 unresolved leads, and each lead to - contain 1 through 600 characters. Tool admission rechecks those same bounds - and returns the complete indexed mismatch set; checkpoint construction then - revalidates the identical shape before hashing and persistence. - The exact failure partition, frozen schedule, completed proposals, receipts, - and diagnostics remain in the resumable failure checkpoint; they are not - folded into successful session compaction. -- **Cut line:** this cut reuses Gestalt fission, strategic Gestalt - individuation, WorldKernel admission, model receipts, the generic scheduler, - and immutable acceptance checkpoints. It adds no second population or member - insertion path, campaign store, parallel commit owner, cover allocator, or - transcript-shaped session log. `WorldComplexityAction` is a model-boundary - draft, not a new canonical mutation or persistent state authority; the - existing `WorldComplexityProposal` remains the checked checkpoint and kernel - handoff. The prior partial-wave checkpoint remains recovery for one frozen - dispatch partition; it is not - elaborator memory. The title-by-realm checkpoint map replaces both the former - title-only global memory and any temptation to preserve provider transcripts; - it introduces no provider-session store or second jurisdiction allocator. - Compact checkpoints replace accumulated working history without replacing - canonical commits. Individuation admission requires unique exact IDs, not a - global public-name registry; the cut adds no alias table, fuzzy resolver, or - name-disambiguation authority. `WorldComplexityMemberDraft` is only the - model-boundary semantic payload; it is not a second member-state schema or - persistence owner, and deterministic lifecycle fields do not cross the model - boundary. The compaction repair adds no compactor, checkpoint type, session - store, or memory authority; it closes the item-bound seam between the - existing model schema, tool admission, and checkpoint validator. Parallel - member-ID supersession adds no namespace allocator, alias registry, - reconciliation pass, or alternate admission owner; it is a deterministic - filter over one frozen round's already-produced invocations. -- **Verification layer:** checkpoint shape and digest bind title, session, - campaign, target location, generation, world revision, prior checkpoint, and - bounded narrative fields. Frozen parent digests make conflicting parallel - mutations uncommittable. Existing fission validation proves lineage, child - partitioning, resource custody, and member transfer; shared individuation - validation proves active parent and location, exact version, unique bounded - member/Actor IDs, exact valid relationship targets, and non-materialized - member state. Equal public names on distinct exact IDs remain admissible and - do not change address resolution or authority. The focused tool regression - admits a new member whose name matches an established Actor, still rejects a - colliding canonical Actor ID and unsupported relationship subject, and - confirms name collision is absent from the rejection. WorldKernel - revalidates either lane before atomic mutation. The model schema exposes only - the assigned operation, bounds fission children to two through eight, - requires exactly the frozen parent resource and dormant-member IDs as - assignment-map keys while leaving child custody model-authored, and for - individuation exposes only semantic member fields and contains no `schema`, - `gestalt_id`, `version`, `last_location_id`, `materialized_actor_id`, - `last_relevant_revision`, or `relevance_lease_until_revision`; a focused - schema regression proves those deterministic fields are absent and the tool - still constructs an admissible canonical individuation. Tool-level - construction additionally rejects duplicate child IDs/partitions or - incomplete exact - member assignment before shared semantic validation. The shared fission - validator owns one complete deterministic mismatch set, including frozen - binding, child structure and inheritance, member assignments, resource - conservation and custody, approval, and parent profile eligibility; every - caller receives that set rather than a generic first-failure umbrella. - Worker construction requires exactly one jurisdiction for every assigned - parent, rejects extra routes, and reuses - `unique_containing_jurisdiction` to prove that the assigned jurisdiction - contains that parent's exact profile locations. Resume and - worker construction require every checkpoint - map key to equal both the embedded session ID and the session ID recomputed - from its embedded title and target location; checkpoint validation binds - those fields to its digest and current campaign. Runtime lookup - derives the expected ID from the current dispatch title and routed realm. - Routing tests prove that equal titles in different realms and different - titles in one realm produce different IDs. Each round must strictly - increase the canonical actionable count; completion requires a freshly - derived zero deficit rather than exhaustion of calls or rounds. Focused tests - cover scale arithmetic, checkpoint tampering and cross-title rejection, exact - compaction ancestry, assigned-parent validation, and safe revision rebasing. - A focused compaction regression proves the generated schema carries the - per-lead 1/600 character bounds and that empty and overlong leads in one - action produce separate index-addressed findings rather than one generic - combined diagnostic. - A focused batch regression proves that two parallel individuations with one - canonical member ID retain the lower dispatch ordinal, publish the later - invocation as superseded with the exact retained ordinal and ID, and leave - an unrelated member ID retained. -- **Failure and resume:** a provider failure publishes an immutable - round-failure generation with the exact completed and failed dispatches and - receipts, then stops that run. Explicit resume rehydrates the latest failure, - preserves completed invocations, and invokes only the failed original - dispatches through `resume_elaboration_wave`; repeated failures publish - numbered generations without changing the frozen schedule. A published - successful round preview may also be reused on explicit resume. Existing - mutation checkpoints suppress replay, title-by-realm compaction checkpoints - suppress recompaction, immutable superseded-invocation checkpoints must - exactly match recomputed collision ownership and suppress commit/journal work - for those later duplicates, and consecutive completed round checkpoints - restore scheduler fairness and session memory before the first unfinished - round. - After mutation admission, independent title-by-realm journals compact in - parallel under the same bounded complexity parallelism entitlement. Each - compactor reads the same committed campaign revision and only its own prior - session checkpoint and journal; the runner persists every successful model - receipt and immutable session checkpoint before returning any sibling - failure. Session checkpoints remain independent memory owners rather than a - shared round transcript, and canonical mutation order remains serial. - Tool-internal rejection findings are not currently copied into later - successful compaction. That is a bounded continuity limitation: the compacted - title-by-realm mind may lose a useful rejected path, but no live proposal is - orphaned because exact failed dispatches remain owned by the resumable failure - checkpoint. This limitation was not the Run 93 blocker. - Missing eligible parents, stale or malformed checkpoints, changed parent - bindings, zero-growth rounds, and a nonzero deficit at the configured round - ceiling are terminal failures. Canonical CultCache state, not the checkpoint - prose, decides whether already admitted mutations exist after interruption. - Parallel fission proposals may independently choose the same globally scoped - child identifier. Rebase treats that as syntactic contention rather than a - semantic contradiction: only an occupied child ID is deterministically moved - into the assigned parent's content-addressed namespace, with partition, - residual, member, and resource references rewritten together before the - ordinary complete fission validator runs. Names, partition meaning, custody, - and membership remain model-authored; canonical state still admits the result - only through WorldKernel. - -One boundary remains intentionally narrow in this acceptance-driver cut. -Fission and named-member individuation are live, but institution promotion is -not. Dormant member rows still do not count; the individuation lane earns its -increment by atomically promoting the admitted member into an Actor and deriving -that Actor's active agency profile through the shared presence-commit path. -Materializing one person does not retire their population as an elaboration -parent. A later fission transfers every exact member affiliation once. Dormant -members have their deltas rebased against the selected child baseline; -materialized people keep their Actor state byte-identical while their backing -member row moves to the selected child. The complete member-assignment map, -component membership transition, shared fission validator, and WorldKernel -commit remain the single path for both cases. -Architecture claims that complexity has no production caller, no durable -elaborator compaction owner, no partial-failure resume, or only a fission -proposal lane are stale. - -The strategic acceptance runner exercises this path as a deliberate hybrid. -The existing destination compiler first commits the complete civic foundation -for one canonical locality. A second revision-bound titled wave then adds one -Patina child place with exact reciprocal routes, preserves the next civic -manifest version through Charter, and admits title-specific facts and a -political relation before independent civic verification and a second kernel -commit. The acceptance profile allocates three Patina calls and two calls to -each other title per locality; a seventeen-slot pass therefore invokes workers -in exact configured proportion under bounded parallelism. This proves the -titled path against a real provider without claiming that the older civic -foundation compiler has already been replaced by the swarm. -The strategic acceptance harness persists compiler, foundation, titled-wave, -and completed strategic-wave checkpoints beside the CultCache campaign. On an -explicit resume it loads canonical world state from CultCache, rehydrates model -receipts by content hash, restores scheduler state from the consumed schedule, -and continues at the first incomplete authority boundary. A committed -foundation is checked against its preserved civic apparatus. A titled preview -is not completion authority: only a post-kernel completion checkpoint bound to -the before/after revisions, semantic verifier, model receipts, exact -`WorldCommitReceipt`, and the kernel-owned mutation authority, batch, and -receipt may skip that commit. New checkpoints also bind the admission digest; -legacy inference instead binds the finalized candidate through the persisted -mutation proof before minting its checkpoint. The batch's -`intended_effect_digest` binds the complete finalized candidate before -deterministic aggregate projection, so the -resume verifier does not duplicate the reducer's set and ordering rules. -Run-32's older committed Canopy pass can be admitted once through that persisted -mutation proof plus its complete civic candidate, then gains the same durable -completion checkpoint. A partial titled wave routes through -`resume_elaboration_wave`. Each later terminal partial result is published as a -new immutable generation by same-directory atomic rename, preserving the prior -valid generation. A committed strategic tick without its matching durable wave -checkpoint is refused rather than guessed or replayed. - -Both destination paths synthesize the smallest compatible branch-local routes, -geometry, people, supplies, procedures, capacity, responsibility, and doctrine -needed for play. Those choices remain visible as `branch_assumptions`; they may -vary between campaigns unless the Vault pins them. A `gap` is reserved for -contradictory canon baselines, an explicitly requested exact canon baseline that -cannot be anchored, or conflict with an approved capability. Ordinary source -silence is not a gap. Preview approval remains the only path from this proposal -into canonical world state. +- **Owner:** `elaboration::canonical_actionable_subject_ids` is the single + qualification derivation for world-scale complexity. It reads the canonical + campaign and returns the active subjects allowed to pay the scale target. + `derive_world_elaboration_demand` owns target and deficit arithmetic. + The strategic acceptance driver owns bounded round orchestration and selects + parents only from the same qualified-ID set. `WorldKernel` remains the sole + canonical mutation owner. +- **Inputs:** the admitted campaign; active, simulation-eligible agency profiles; + Gestalt lineages and residual IDs; each fission child's retained parent + semantics; the active-cell budget; the consumer-authored target cover basis + points; realm weights; topology-derived jurisdiction; realm pressure and + detail debt; bounded round and concurrency limits; model receipts; and the + compact title-by-jurisdiction elaborator checkpoint. +- **Qualification invariant:** compiler-admitted roots and individuated people + enter through their owning admission gates. A fission child qualifies only + when it is not any lineage residual, retains the exact parent baseline, and + has additions in at least two of capability, knowledge, goal, and pressure. + Residual populations preserve unresolved membership and lineage only. They + are non-simulation state, do not enter the public action namespace, do not pay + the complexity target, and cannot become recursive complexity parents. + Ordinary inherited-baseline fission children remain simulatable without + satisfying the qualified world-size bill. +- **Demand invariant:** the target actionable-subject count is + `ceil(active_cell_budget * 10_000 / target_cover_basis_points)`. An active + cell budget of 240 at a 20% cover target therefore requires 1,200 qualified + subjects. The demand is re-derived from accepted canonical state after every + round. The pre-wave phase boundary requires the qualified count to equal the + target exactly; a larger count is not accepted as a satisfied target. On + post-wave resume, the last fully validated complexity checkpoint owns that + historical pre-wave count because legitimate strategic individuation may + have increased the current campaign count. +- **Round dataflow:** frozen campaign -> qualified-ID derivation -> distinct + qualified parent selection by jurisdiction pressure and detail debt -> + deterministic titled dispatch -> parallel model proposals -> semantic + validation and same-batch supersession -> sequential kernel commits -> + qualified-ID re-derivation. Every accepted round must strictly increase the + qualified count. Missing parents, zero growth, or a nonzero deficit at the + round ceiling is terminal failure rather than completion telemetry. +- **Gain allocation:** `ModelWorldComplexityWorker` distributes the remaining + deficit over the assigned distinct parents, with an exact target gain of one + through six per dispatch. An elaborated fission emits target gain plus two + children because the retained parent retires and the residual child does not + qualify. The semantic validator checks the resulting + `children.len() - 2` against the qualification. +- **Proposal lanes:** Hearth, Tangle, and Veil individuate one consequential + member. Patina, Charter, Ledger, Ember, and Numen elaborate a population + fission. The model owns only the lane-specific draft; the tool reconstructs + campaign ID, revision, parent, axis, evidence and approval bindings around + that draft. +- **Fission draft and output:** each child draft contains an ID, public name, + partition value, home location, capability additions, knowledge additions, + goal additions, and pressure additions. The draft also names the residual + child and exact member and resource assignments. The tool clones the frozen + parent baseline, applies only those additions, and emits a + `WorldComplexityFissionQualification` binding title, jurisdiction, and exact + target actionable gain. +- **Fission semantic invariant:** every specific child has a normalized public + identity distinct from existing Gestalts and its siblings, remains inside its + assigned jurisdiction, and contributes at least two bounded causal + dimensions. Additions are bounded to two capabilities, two knowledge + statements, one goal, and one pressure, and must be semantically distinct + from the parent and siblings. Only Geography fission may move children away + from the parent home; when multiple admitted places exist it must use at + least two. The residual stays at the parent home with the exact parent + capability, knowledge, goal, and pressure baseline and the + `other/unknown` partition. +- **Title-specific fission shapes:** Patina requires a capability and goal. + Charter and Ledger require a goal, pressure, and either capability or + knowledge. Ember requires a pressure and either capability or knowledge. + Numen requires goal, pressure, and knowledge. +- **Individuation invariant:** every admitted member has a goal, a concrete + memory, and at least one additional identity-bearing dimension. Hearth also + requires a concrete care or belonging obligation. Tangle requires an exact + relationship to an existing canonical subject. Veil requires knowledge not + held by the parent population. The resolution validator rejects normalized + public-name collisions with existing actors and members; same-batch duplicate + canonical member IDs or public identities are superseded by the lowest + dispatch ordinal. Actor, institution, Gestalt, dormant member projection, + agency-profile key, profile ID, and profile subject ID share one canonical + subject-ID collision predicate at proposal validation, deterministic rebase, + and kernel application. Promotion alone may claim the exact member's own + reserved projection. +- **Typed mutation path:** `WorldCommand::ElaborateGestaltFission` carries the + preview, exact qualification, and model provenance. The kernel revalidates + both elaborated-fission structure and titled complexity semantics before + lowering. The lineage split first inherits the parent's component baseline. + Only this elaborated command may add the approved capability grants, + proposition admissions and knowledge acquisitions, goal commitments, and + pressures in the same component-world batch as identity, occupancy, custody, + membership, relationship, and civic rebindings. +- **Projection invariant:** component-batch application occurs against a cloned + overlay and validates the complete next world before aggregate projection. + Projection verifies exact capability entry IDs and descriptions, known + proposition statements, active goal descriptions, and unresolved pressure + labels against each approved child. It persists exactly the approved + preview-minus-parent knowledge additions as campaign facts through the + shared proposition resolver, copies the approved child semantics with typed + identity and occupancy, and derives resources from accepted custody. Campaign, + authority, batch, and receipt records persist only after projection succeeds. +- **Ordinary fission boundary:** `WorldCommand::FissionGestalt` retains the + strict inherited-baseline contract. Its lowering emits no semantic-delta + mutations. Elaborated semantic additions cannot enter through that path. +- **Relationship contribution:** the lineage reducer inherits every active + parent relationship to each child and aggregate projection retires the parent + relation. Fission has no free-form relationship-addition field. New + relationship texture enters qualified complexity through Tangle + individuation, while locality elaboration owns new civic relations. +- **Horizontal expansion boundary:** destination compilation and locality + elaboration own places, routes, population roots, institutions, civic + apparatus, and local political relations. Complexity fission may distribute + a Geography split across admitted places but cannot invent geography or a + parallel political apparatus. Horizontal civic foundations must therefore be + admitted before vertical complexity can elaborate them. Each admitted realm + must contain at least three mutually reachable internal places with resident + homes in at least three. At scale completion, each jurisdiction must place + qualified subjects in at least three locations and no one location may hold + more than four fifths of its qualified subjects. +- **Civic diversity context:** destination compilation and its semantic verifier + receive up to eight peer civic apparatus contexts. Candidate generation + requires materially executable means, preserves mixed, diasporic, and + cross-border constituencies, rejects ancestry-only civic partitions, and + admits locally coherent forms of redress. Verification separately requires + `political_form_distinct_or_deliberately_shared`, + `opposed_interests_have_distinct_means`, and + `candidate_text_intact`. Prompt excerpts are truncated only at word + boundaries. +- **Outputs:** canonical actors, Gestalts, members, agency profiles, lineages, + facts, component capabilities, knowledge, commitments, pressures, + relationships, memberships, occupancy, custody, civic bindings, and mutation + receipts. Qualified actionable IDs and the current deficit are always + derived from those canonical outputs; there is no independently writable + qualification flag. +- **Derived telemetry:** elaboration demand, round reports, previews, model + receipts, session compactions, supersession reports, and resume checkpoints + explain orchestration and recovery. They do not own world truth or + qualification. Session memory is bounded by title and jurisdiction and binds + only committed receipts; raw model sessions and transcripts are not + canonical state. +- **Recovery invariant:** every completed complexity round reopens its frozen + preview and validates every mutation, supersession, rejection, model receipt, + semantic verdict, canonical world receipt, and exact fission or individuation + mutation proof in one contiguous revision chain. An existing commit + checkpoint is accepted only after the same validator proves both historical + authority and the committed proposal effect. The cumulative session set must + equal prior memory plus every exact accepted/rejected invocation journal; + untouched sessions remain byte-equivalent and touched generations bind their + receipts, findings, prior digest, and round-end revision. Verifier transport + failure is a generational resumable checkpoint and never masquerades as + semantic rejection or causes the elaborator proposal to replay. +- **Forbidden writers:** model workers, schedulers, reports, checkpoints, + compaction text, cover construction, strategic cells, and renderers may not + create canonical subjects, mark a subject qualified, alter the target, or + repair semantic state after commit. Ordinary fission may not grant semantic + additions. Complexity fission may not create places, institutions, civic + systems, or arbitrary relationships. Residuals may not pay or recursively + service the target. +- **Shared paths:** initial runs and resumed runs derive demand and qualified + parents through the same functions. Rebase may update only the expected + revision when the frozen parent binding is unchanged; child-ID collisions are + renamed deterministically before revalidation. All fission writes, including + relationship inheritance and resource/member movement, pass through the same + typed batch and kernel commit boundary. +- **Cut line:** qualification is derived from canonical campaign semantics and + lineage, not stored beside them. The count, parent selector, progress check, + and completion check all consume that derivation. Any alternate active-leaf + counter, residual-inclusive quota path, or report-owned completion opinion is + outside the live authority model. +- **Verification layer:** focused tests prove 240 active cells at 20% require + 1,200 subjects; residual and ordinary relabel fission children do not pay the + qualified bill; exact target gain is enforced; title-specific additions are + bounded and distinct; public identities cannot collide; duplicate + individuations supersede deterministically; elaborated fission commits its + capability, proposition, knowledge, commitment, pressure, relationship, + custody, membership, and fact projection atomically; component re-snapshot + validates; ordinary lowering rejects a semantic preview; and the destination + verifier enforces political distinctness, materially opposed means, and + intact candidate text. Autonomous strategic waves run the same admitted agency state through parallel cell membranes under a provider concurrency gate. Action-bound individuation