diff --git a/crates/ghostlight-dungeon/src/persona.rs b/crates/ghostlight-dungeon/src/persona.rs index 454dc74..dca32ae 100644 --- a/crates/ghostlight-dungeon/src/persona.rs +++ b/crates/ghostlight-dungeon/src/persona.rs @@ -1470,13 +1470,6 @@ impl CellProjectionEngine { .map(|verification| verification.output.receipt), ); if attempt == 0 { - exclude_rejected_cell_effects( - request.output_schema.as_mut().ok_or_else(|| { - anyhow!("cell correction lost its output schema") - })?, - &appraisal.actions, - &rejected_action_indices, - )?; append_cell_correction( &mut request, &error, @@ -1531,30 +1524,6 @@ impl CellProjectionEngine { interpreted.receipt.local_validation_error = Some(error.to_string().chars().take(1_000).collect()); stage_receipts.push(interpreted.receipt); - if let Some(structured) = interpreted.structured - && let Ok(proposal) = - serde_json::from_value::(structured) - && let Ok(appraisal) = bind_cell_appraisal(&slice, proposal) - { - let rejected_action_indices = appraisal - .actions - .iter() - .enumerate() - .filter_map(|(index, action)| { - validate_cell_action(&slice, action) - .is_err() - .then_some(index) - }) - .collect::>(); - exclude_rejected_cell_effects( - request - .output_schema - .as_mut() - .ok_or_else(|| anyhow!("cell correction lost its output schema"))?, - &appraisal.actions, - &rejected_action_indices, - )?; - } append_cell_correction(&mut request, &error, &rejected_appraisal); } Err(error) => { @@ -1713,249 +1682,6 @@ fn rejected_action_diagnostic( .collect() } -fn exclude_rejected_cell_effects( - schema: &mut serde_json::Value, - actions: &[crate::domain::CellActionProposal], - rejected_action_indices: &[usize], -) -> Result<()> { - let candidates = schema - .pointer_mut("/properties/actions/items/anyOf") - .and_then(serde_json::Value::as_array_mut) - .ok_or_else(|| anyhow!("cell correction schema has no exact action alternatives"))?; - for index in rejected_action_indices { - let Some(action) = actions.get(*index) else { - continue; - }; - let candidate = candidates - .iter_mut() - .find(|candidate| { - candidate.pointer("/properties/subject_id/const") - == Some(&serde_json::Value::String(action.subject_id.clone())) - }) - .and_then(serde_json::Value::as_object_mut) - .ok_or_else(|| { - anyhow!( - "cell correction schema has no action alternative for subject {}", - action.subject_id - ) - })?; - let compact_effects = cell_effect_candidate_values(&action.effects); - let effect_properties = candidate - .get("properties") - .and_then(serde_json::Value::as_object) - .and_then(|properties| properties.get("effects")) - .and_then(|effects| effects.get("properties")) - .and_then(serde_json::Value::as_object) - .ok_or_else(|| anyhow!("cell correction action has no exact effect slots"))?; - let strict_effects = strict_cell_effect_candidate_values( - effect_properties, - compact_effects - .as_object() - .expect("cell effect candidate values are an object"), - )?; - candidate - .entry("allOf") - .or_insert_with(|| serde_json::json!([])) - .as_array_mut() - .ok_or_else(|| anyhow!("cell correction action constraints are not an array"))? - .push(serde_json::json!({ - "not":{ - "anyOf":[ - { - "type":"object", - "required":["effects"], - "properties":{"effects":{"const":compact_effects}} - }, - { - "type":"object", - "required":["effects"], - "properties":{"effects":{"const":strict_effects}} - } - ] - } - })); - } - Ok(()) -} - -fn strict_cell_effect_candidate_values( - effect_properties: &serde_json::Map, - compact_effects: &serde_json::Map, -) -> Result> { - let mut strict = serde_json::Map::new(); - for (lane, lane_schema) in effect_properties { - if lane.ends_with("_activities") - && let Some(activity_properties) = lane_schema - .get("properties") - .and_then(serde_json::Value::as_object) - { - strict.insert( - lane.clone(), - serde_json::Value::Object( - activity_properties - .keys() - .cloned() - .map(|activity| (activity, serde_json::Value::Null)) - .collect(), - ), - ); - } else { - strict.insert(lane.clone(), serde_json::Value::Null); - } - } - for (lane, value) in compact_effects { - if lane.ends_with("_activities") { - strict - .get_mut(lane) - .and_then(serde_json::Value::as_object_mut) - .ok_or_else(|| anyhow!("cell correction activity lane is unavailable"))? - .extend( - value - .as_object() - .ok_or_else(|| anyhow!("cell correction activity value is malformed"))? - .clone(), - ); - } else { - strict.insert(lane.clone(), value.clone()); - } - } - Ok(strict) -} - -fn cell_effect_candidate_values( - effects: &[crate::domain::StrategicCellEffect], -) -> serde_json::Value { - let mut values = serde_json::Map::new(); - for effect in effects { - let (lane, value) = cell_effect_candidate_value(effect); - if lane.ends_with("_activities") { - values - .entry(lane.to_owned()) - .or_insert_with(|| serde_json::json!({})) - .as_object_mut() - .expect("activity candidate lane is an object") - .insert( - strategic_activity_key( - strategic_effect_activity_kind(effect) - .expect("activity lane contains an activity effect"), - ) - .to_owned(), - value, - ); - } else { - values.insert(lane.to_owned(), value); - } - } - serde_json::Value::Object(values) -} - -fn cell_effect_candidate_value( - effect: &crate::domain::StrategicCellEffect, -) -> (&'static str, serde_json::Value) { - match effect { - crate::domain::StrategicCellEffect::Institution { - posture, - location_ids, - .. - } => ( - "institution", - serde_json::json!({ - "posture":posture, - "location_ids":location_ids, - }), - ), - crate::domain::StrategicCellEffect::Gestalt { - pressure_additions, - pressure_resolutions, - .. - } => ( - "gestalt_pressure", - serde_json::json!({ - "pressure_additions":pressure_additions, - "pressure_resolutions":pressure_resolutions, - }), - ), - crate::domain::StrategicCellEffect::GestaltActivity { - target_subject_ids, - location_ids, - .. - } => ( - "gestalt_activities", - serde_json::json!({ - "target_subject_ids":target_subject_ids, - "location_ids":location_ids, - }), - ), - crate::domain::StrategicCellEffect::GestaltMigration { - destination_gestalt_id, - } => ( - "gestalt_migration", - serde_json::json!({ - "destination_gestalt_id":destination_gestalt_id, - }), - ), - crate::domain::StrategicCellEffect::ActorMove { destination_id, .. } => ( - "actor_move", - serde_json::json!({ - "destination_id":destination_id, - }), - ), - crate::domain::StrategicCellEffect::ActorActivity { - target_subject_ids, - location_ids, - .. - } => ( - "actor_activities", - serde_json::json!({ - "target_subject_ids":target_subject_ids, - "location_ids":location_ids, - }), - ), - crate::domain::StrategicCellEffect::MemberActivity { - target_subject_ids, - location_ids, - .. - } => ( - "member_activities", - serde_json::json!({ - "target_subject_ids":target_subject_ids, - "location_ids":location_ids, - }), - ), - crate::domain::StrategicCellEffect::MemberMigration { - destination_gestalt_id, - } => ( - "member_migration", - serde_json::json!({ - "destination_gestalt_id":destination_gestalt_id, - }), - ), - } -} - -fn strategic_activity_key(activity: &crate::domain::StrategicActivityKind) -> &'static str { - match activity { - crate::domain::StrategicActivityKind::Prepare => "prepare", - crate::domain::StrategicActivityKind::Coordinate => "coordinate", - crate::domain::StrategicActivityKind::Investigate => "investigate", - crate::domain::StrategicActivityKind::Recruit => "recruit", - crate::domain::StrategicActivityKind::Obstruct => "obstruct", - crate::domain::StrategicActivityKind::Trade => "trade", - crate::domain::StrategicActivityKind::Communicate => "communicate", - } -} - -fn strategic_effect_activity_kind( - effect: &crate::domain::StrategicCellEffect, -) -> Option<&crate::domain::StrategicActivityKind> { - match effect { - crate::domain::StrategicCellEffect::GestaltActivity { activity, .. } - | crate::domain::StrategicCellEffect::ActorActivity { activity, .. } - | crate::domain::StrategicCellEffect::MemberActivity { activity, .. } => Some(activity), - _ => None, - } -} - fn cell_correction_guidance(error: &anyhow::Error) -> &'static str { if error .to_string() @@ -1965,7 +1691,7 @@ fn cell_correction_guidance(error: &anyhow::Error) -> &'static str { } else if error.to_string().contains("duplicate strategic actions") { "The named subject may own exactly one strategic choice in this horizon. Merge all faithful chosen means into one action and use its orthogonal effect slots together; remove any separate alternative, deliberation, or unreachable choice rather than emitting a second action." } else { - "A rejected action is forbidden unchanged: remove it or replace it with a different, faithful, permitted typed consequence. Do not repeat its subject, intended_effect, and typed effect together." + "Repair the semantic mismatch named by the verifier while preserving every unaffected faithful effect. A correction may retain exact unaffected values; do not alter text merely to make it bytewise different. Compress bounded text into complete clauses rather than truncating its meaning." } } @@ -3605,7 +3331,9 @@ mod tests { && request.lived_stream.contains("releases the reserve"), Ordering::SeqCst, ); - assert!(request.lived_stream.contains("forbidden unchanged")); + assert!(request + .lived_stream + .contains("do not alter text merely to make it bytewise different")); assert!(request .lived_stream .contains("no exact permitted destination")); @@ -3771,6 +3499,13 @@ mod tests { )); assert!(duplicate_action_guidance.contains("exactly one strategic choice")); assert!(duplicate_action_guidance.contains("orthogonal effect slots together")); + + let semantic_guidance = cell_correction_guidance(&anyhow!( + "effect verifier rejected action 0 because its bounded posture ended mid-clause" + )); + assert!(semantic_guidance.contains("preserving every unaffected faithful effect")); + assert!(semantic_guidance.contains("complete clauses")); + assert!(!semantic_guidance.contains("forbidden unchanged")); } #[test] @@ -4069,142 +3804,6 @@ mod tests { assert_eq!(rejected_action_diagnostic(&[], &[4]), "[]"); } - #[test] - fn semantic_retry_schema_forbids_the_exact_rejected_effect_but_not_a_faithful_lane() { - let mut slice = fixture_cell_slice(); - let actor_id = { - let actor = &mut slice.constituents[0]; - actor.subject_kind = AgencySubjectKind::Actor; - actor.current_posture = None; - actor.reachable_destinations = - BTreeMap::from([("encampment".into(), "Refugee Encampment".into())]); - actor.subject_id.clone() - }; - let active = BTreeSet::from([actor_id.clone()]); - let mut schema = serde_json::to_value(schema_for!(CellAppraisalProposal)).unwrap(); - constrain_cell_proposal_schema(&mut schema, &slice, &active).unwrap(); - let rejected = crate::domain::CellActionProposal { - subject_id: actor_id.clone(), - intent: "move the patients now".into(), - intended_effect: "lead the patients out".into(), - priority: 80, - state_references: vec!["institution:faction-06".into()], - public_channels: vec![], - effects: vec![StrategicCellEffect::ActorMove { - actor_id, - destination_id: "encampment".into(), - }], - }; - exclude_rejected_cell_effects(&mut schema, &[rejected], &[0]).unwrap(); - let validator = jsonschema::validator_for(&schema).unwrap(); - let appraisal = |effects| { - serde_json::json!({ - "actions":[{ - "subject_id":"faction-06", - "intent":"announce the departure plan", - "intended_effect":"tell the patients the plan", - "priority":80, - "state_references":["institution:faction-06"], - "public_channels":[], - "effects":effects, - }], - "inactions":[], - }) - }; - - assert!(!validator.is_valid(&appraisal(serde_json::json!({ - "actor_move":{"destination_id":"encampment"}, - })))); - assert!(validator.is_valid(&appraisal(serde_json::json!({ - "actor_activities":{"communicate":{ - "target_subject_ids":[], - "location_ids":["forum"] - }} - })))); - } - - #[test] - fn semantic_retry_schema_forbids_the_exact_strict_keyed_activity_set() { - let mut slice = fixture_cell_slice(); - let actor = &mut slice.constituents[0]; - actor.subject_kind = AgencySubjectKind::Actor; - actor.current_posture = None; - actor.activity_targets = BTreeMap::from([( - "inst_zhestokost".into(), - CellActivityTargetSlice { - name: "Zhestokost".into(), - locations: BTreeMap::from([("forum".into(), "Forum".into())]), - }, - )]); - let actor_id = actor.subject_id.clone(); - let active = BTreeSet::from([actor_id.clone()]); - let mut schema = serde_json::to_value(schema_for!(CellAppraisalProposal)).unwrap(); - constrain_cell_proposal_schema(&mut schema, &slice, &active).unwrap(); - let rejected = crate::domain::CellActionProposal { - subject_id: actor_id.clone(), - intent: "record and announce the inspection".into(), - intended_effect: "prepare the record and communicate the notice".into(), - priority: 80, - state_references: vec!["institution:faction-06".into()], - public_channels: vec![], - effects: vec![ - StrategicCellEffect::ActorActivity { - actor_id: actor_id.clone(), - activity: StrategicActivityKind::Prepare, - target_subject_ids: vec![], - location_ids: vec!["forum".into()], - }, - StrategicCellEffect::ActorActivity { - actor_id, - activity: StrategicActivityKind::Communicate, - target_subject_ids: vec![], - location_ids: vec!["forum".into()], - }, - ], - }; - exclude_rejected_cell_effects(&mut schema, &[rejected], &[0]).unwrap(); - let validator = jsonschema::validator_for(&schema).unwrap(); - let action = |effects| { - serde_json::json!({ - "actions":[{ - "subject_id":"faction-06", - "intent":"record and announce the inspection", - "intended_effect":"prepare the record and communicate the notice", - "priority":80, - "state_references":["institution:faction-06"], - "public_channels":[], - "effects":effects, - }], - "inactions":[], - }) - }; - let exact_strict_set = serde_json::json!({ - "institution":null, - "gestalt_pressure":null, - "gestalt_activities":null, - "gestalt_migration":null, - "actor_move":null, - "actor_activities":{ - "prepare":{"target_subject_ids":[],"location_ids":["forum"]}, - "coordinate":null, - "investigate":null, - "recruit":null, - "obstruct":null, - "trade":null, - "communicate":{"target_subject_ids":[],"location_ids":["forum"]} - }, - "member_activities":null, - "member_migration":null - }); - assert!(!validator.is_valid(&action(exact_strict_set.clone()))); - - let mut faithful_superset = exact_strict_set; - *faithful_superset - .pointer_mut("/actor_activities/investigate") - .unwrap() = serde_json::json!({"target_subject_ids":[],"location_ids":["forum"]}); - assert!(validator.is_valid(&action(faithful_superset))); - } - #[test] fn effect_verifier_separates_direct_contact_from_publication_authority() { assert!(CELL_EFFECT_VERIFIER_INSTRUCTIONS.contains( @@ -5120,40 +4719,6 @@ mod tests { assert!(exact_target_properties.contains_key("trade")); } - #[test] - fn activity_effects_project_as_one_keyed_slot_per_kind() { - let effects = vec![ - StrategicCellEffect::ActorActivity { - actor_id: "liaison".into(), - activity: StrategicActivityKind::Communicate, - target_subject_ids: vec!["clinic".into()], - location_ids: vec!["forum".into()], - }, - StrategicCellEffect::ActorActivity { - actor_id: "liaison".into(), - activity: StrategicActivityKind::Investigate, - target_subject_ids: vec![], - location_ids: vec!["forum".into()], - }, - ]; - - assert_eq!( - cell_effect_candidate_values(&effects), - serde_json::json!({ - "actor_activities":{ - "communicate":{ - "target_subject_ids":["clinic"], - "location_ids":["forum"] - }, - "investigate":{ - "target_subject_ids":[], - "location_ids":["forum"] - } - } - }) - ); - } - #[test] fn cohesive_gestalt_activity_can_coordinate_its_own_unnamed_members() { let schema = exact_activity_effects_schema(