Preserve actor continuity in strategic cells
This commit is contained in:
parent
a830fe6699
commit
122aa1fb3e
6 changed files with 94 additions and 13 deletions
|
|
@ -96,7 +96,14 @@ fn main() -> Result<()> {
|
|||
for actor in campaign.actors.values() {
|
||||
subjects.insert(
|
||||
actor.id.clone(),
|
||||
json!({ "kind": "actor", "name": actor.name }),
|
||||
json!({
|
||||
"kind": "actor",
|
||||
"name": actor.name,
|
||||
"goals": actor.goals,
|
||||
"obligations": actor.obligations,
|
||||
"relationships": actor.relationships,
|
||||
"memories": actor.memories,
|
||||
}),
|
||||
);
|
||||
}
|
||||
for institution in campaign.institutions.values() {
|
||||
|
|
@ -108,13 +115,26 @@ fn main() -> Result<()> {
|
|||
for gestalt in campaign.gestalts.values() {
|
||||
subjects.insert(
|
||||
gestalt.id.clone(),
|
||||
json!({ "kind": "gestalt", "name": gestalt.name }),
|
||||
json!({
|
||||
"kind": "gestalt",
|
||||
"name": gestalt.name,
|
||||
"goals": gestalt.goals,
|
||||
"pressures": gestalt.pressures,
|
||||
}),
|
||||
);
|
||||
}
|
||||
for member in campaign.gestalt_members.values() {
|
||||
subjects.insert(
|
||||
member.id.clone(),
|
||||
json!({ "kind": "gestalt_member", "name": member.name, "gestaltId": member.gestalt_id }),
|
||||
json!({
|
||||
"kind": "gestalt_member",
|
||||
"name": member.name,
|
||||
"gestaltId": member.gestalt_id,
|
||||
"goals": member.goals,
|
||||
"obligations": member.obligations,
|
||||
"relationships": member.relationships,
|
||||
"memories": member.memories,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -598,7 +598,7 @@ impl WorldCompiler {
|
|||
.map(|brief| brief.subject_id.clone())
|
||||
.collect::<BTreeSet<_>>();
|
||||
let agency_prompt = format!(
|
||||
"MULTIRESOLUTION AGENCY SKELETON\nCompile only this exact, already validated subject roster:\n{}\n\nReturn exactly one agency profile for every supplied subject and no other subject. Copy every subject_id, subject_kind, and location_ids exactly. Every profile must contain exactly the six facet axes geography, ideology, authority, economy_role, species_body, and information. Derive facets only from the supplied roster fields; use an explicit unknown value when they do not support a sharper claim. information_channels are routes through which the subject can publish or receive reports (for example a courier wire, bulletin, newspaper, radio net, or word of mouth), never facts the subject knows; do not copy knowledge_or_posture values into information_channels. collective_authority_id must be null or one supplied subject ID; it denotes real shared authority, never mere alliance or proximity. Relations may use only supplied subject IDs and strength must be an integer from 1 through 100. Cross-faction relations never imply shared speech, knowledge, or authority. Preserve geographic, ideological, institutional, economic, biological, and information boundaries that predict different behavior under pressure.",
|
||||
"MULTIRESOLUTION AGENCY SKELETON\nCompile only this exact, already validated subject roster:\n{}\n\nReturn exactly one agency profile for every supplied subject and no other subject. Copy every subject_id, subject_kind, and location_ids exactly. Every profile must contain exactly the six facet axes geography, ideology, authority, economy_role, species_body, and information. Derive facets only from the supplied roster fields; use an explicit unknown value when they do not support a sharper facet claim. information_channels are concrete routes through which the subject can publish or receive reports (for example a courier wire, bulletin, newspaper, radio net, or word of mouth), never facts the subject knows; do not copy knowledge_or_posture values into information_channels, and use an empty set rather than an unknown placeholder when no route is supported. collective_authority_id must be null or one supplied subject ID; it denotes real shared authority, never mere alliance or proximity. Relations may use only supplied subject IDs and strength must be an integer from 1 through 100. Cross-faction relations never imply shared speech, knowledge, or authority. Preserve geographic, ideological, institutional, economic, biological, and information boundaries that predict different behavior under pressure.",
|
||||
serde_json::to_string(&subject_briefs)?
|
||||
);
|
||||
let agency_schema = serde_json::to_value(schema_for!(CompiledAgencySkeleton))?;
|
||||
|
|
@ -3041,17 +3041,24 @@ fn apply_compiled_agency_skeleton(
|
|||
.unwrap_or_default(),
|
||||
AgencySubjectKind::Institution => Vec::new(),
|
||||
};
|
||||
let invalid_information_channels = input
|
||||
.information_channels
|
||||
.iter()
|
||||
.filter(|channel| !crate::resolution::information_channel_is_concrete(channel))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
if profile.subject_kind != input.subject_kind
|
||||
|| input_axes != axes
|
||||
|| input.location_ids != profile.location_ids
|
||||
|| !unknown_locations.is_empty()
|
||||
|| !authority_known
|
||||
|| !knowledge_channel_overlap.is_empty()
|
||||
|| !invalid_information_channels.is_empty()
|
||||
{
|
||||
let missing_axes = axes.difference(&input_axes).cloned().collect::<Vec<_>>();
|
||||
let unexpected_axes = input_axes.difference(&axes).cloned().collect::<Vec<_>>();
|
||||
return Err(anyhow!(
|
||||
"agency profile {} malformed: expected_kind={:?}; supplied_kind={:?}; expected_location_ids={:?}; supplied_location_ids={:?}; missing_axes={missing_axes:?}; unexpected_axes={unexpected_axes:?}; unknown_locations={unknown_locations:?}; unknown_collective_authority={:?}; knowledge_channel_overlap={knowledge_channel_overlap:?}",
|
||||
"agency profile {} malformed: expected_kind={:?}; supplied_kind={:?}; expected_location_ids={:?}; supplied_location_ids={:?}; missing_axes={missing_axes:?}; unexpected_axes={unexpected_axes:?}; unknown_locations={unknown_locations:?}; unknown_collective_authority={:?}; knowledge_channel_overlap={knowledge_channel_overlap:?}; invalid_information_channels={invalid_information_channels:?}",
|
||||
input.subject_id,
|
||||
profile.subject_kind,
|
||||
input.subject_kind,
|
||||
|
|
@ -3297,7 +3304,7 @@ mod tests {
|
|||
subject_id: "player".into(),
|
||||
subject_kind: AgencySubjectKind::Actor,
|
||||
collective_authority_id: None,
|
||||
facets,
|
||||
facets: facets.clone(),
|
||||
location_ids: BTreeSet::from(["center".into()]),
|
||||
information_channels: BTreeSet::from(["convoy vulnerabilities".into()]),
|
||||
}],
|
||||
|
|
@ -3305,6 +3312,23 @@ mod tests {
|
|||
)
|
||||
.unwrap_err();
|
||||
assert!(error.to_string().contains("knowledge_channel_overlap"));
|
||||
|
||||
campaign.actors.get_mut("player").unwrap().knowledge.clear();
|
||||
let error = apply_compiled_agency_skeleton(
|
||||
&mut campaign,
|
||||
&BTreeSet::from(["player".into()]),
|
||||
vec![CompiledAgencyProfile {
|
||||
subject_id: "player".into(),
|
||||
subject_kind: AgencySubjectKind::Actor,
|
||||
collective_authority_id: None,
|
||||
facets,
|
||||
location_ids: BTreeSet::from(["center".into()]),
|
||||
information_channels: BTreeSet::from(["unknown".into()]),
|
||||
}],
|
||||
vec![],
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(error.to_string().contains("invalid_information_channels"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -2705,7 +2705,7 @@ fn validate_public_channels(channels: &[String]) -> Result<(), KernelError> {
|
|||
if channels.len() > 8
|
||||
|| channels
|
||||
.iter()
|
||||
.any(|c| c.trim().is_empty() || c.len() > 160)
|
||||
.any(|channel| !crate::resolution::information_channel_is_concrete(channel))
|
||||
{
|
||||
return Err(KernelError::Invalid(
|
||||
"strategic action has invalid information channels".into(),
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@ pub struct CellConstituentSlice {
|
|||
pub migration_destinations: BTreeMap<String, String>,
|
||||
pub activity_target_ids: BTreeSet<String>,
|
||||
pub goals: Vec<String>,
|
||||
pub relationships: BTreeMap<String, String>,
|
||||
pub memories: Vec<String>,
|
||||
pub current_posture: Option<String>,
|
||||
pub pressures: Vec<String>,
|
||||
}
|
||||
|
|
@ -450,6 +452,8 @@ fn cell_projector_context(slice: &PermittedCellSlice) -> serde_json::Value {
|
|||
"migration_destinations": subject.migration_destinations,
|
||||
"activity_target_ids": subject.activity_target_ids,
|
||||
"goals": subject.goals,
|
||||
"relationships": subject.relationships,
|
||||
"memories": subject.memories,
|
||||
"already_committed_posture": subject.current_posture,
|
||||
"pressures": subject.pressures,
|
||||
})).collect::<Vec<_>>(),
|
||||
|
|
@ -1993,6 +1997,8 @@ mod tests {
|
|||
migration_destinations: BTreeMap::new(),
|
||||
activity_target_ids: BTreeSet::new(),
|
||||
goals: vec!["publish a position".into()],
|
||||
relationships: BTreeMap::new(),
|
||||
memories: vec![],
|
||||
current_posture: Some("weighing whether to publish a position".into()),
|
||||
pressures: vec!["the vote is near".into()],
|
||||
}],
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@ pub const MIN_ACTIVE_CELL_BUDGET: u8 = 1;
|
|||
pub const MAX_ACTIVE_CELL_BUDGET: u8 = 128;
|
||||
pub const MAX_PROVIDER_PARALLELISM: u8 = 32;
|
||||
|
||||
pub(crate) fn information_channel_is_concrete(channel: &str) -> bool {
|
||||
let channel = channel.trim();
|
||||
!channel.is_empty() && channel.len() <= 160 && !channel.eq_ignore_ascii_case("unknown")
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Candidate {
|
||||
left: String,
|
||||
|
|
@ -82,16 +87,17 @@ pub fn ensure_agency_profiles(campaign: &mut Campaign) {
|
|||
for actor in campaign.actors.values() {
|
||||
if let Some(profile) = campaign.agency_profiles.get_mut(&actor.id) {
|
||||
profile.location_ids = BTreeSet::from([actor.location_id.clone()]);
|
||||
profile
|
||||
.information_channels
|
||||
.retain(|channel| !actor.knowledge.contains(channel));
|
||||
profile.information_channels.retain(|channel| {
|
||||
information_channel_is_concrete(channel) && !actor.knowledge.contains(channel)
|
||||
});
|
||||
}
|
||||
}
|
||||
for gestalt in campaign.gestalts.values() {
|
||||
if let Some(profile) = campaign.agency_profiles.get_mut(&gestalt.id) {
|
||||
profile
|
||||
.information_channels
|
||||
.retain(|channel| !gestalt.shared_knowledge.contains(channel));
|
||||
profile.information_channels.retain(|channel| {
|
||||
information_channel_is_concrete(channel)
|
||||
&& !gestalt.shared_knowledge.contains(channel)
|
||||
});
|
||||
}
|
||||
}
|
||||
let live: BTreeSet<_> = campaign
|
||||
|
|
@ -2952,6 +2958,7 @@ pub(crate) mod tests {
|
|||
.information_channels = BTreeSet::from([
|
||||
"private convoy vulnerability".into(),
|
||||
"licensed courier wire".into(),
|
||||
"unknown".into(),
|
||||
]);
|
||||
ensure_agency_profiles(&mut value);
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -730,6 +730,8 @@ fn constituent_slice(campaign: &Campaign, id: &str) -> Result<CellConstituentSli
|
|||
migration_destinations: BTreeMap::new(),
|
||||
activity_target_ids: crate::resolution::strategic_activity_targets(campaign, id),
|
||||
goals: vec![],
|
||||
relationships: BTreeMap::new(),
|
||||
memories: vec![],
|
||||
current_posture: None,
|
||||
pressures: vec![],
|
||||
};
|
||||
|
|
@ -741,6 +743,8 @@ fn constituent_slice(campaign: &Campaign, id: &str) -> Result<CellConstituentSli
|
|||
value.capabilities = actor.capabilities.clone();
|
||||
value.resources = actor.equipment.clone();
|
||||
value.goals = actor.goals.clone();
|
||||
value.relationships = actor.relationships.clone();
|
||||
value.memories = actor.memories.clone();
|
||||
value.pressures = actor
|
||||
.conditions
|
||||
.iter()
|
||||
|
|
@ -983,6 +987,26 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_actor_cell_slice_preserves_memory_and_relationship_context() {
|
||||
let mut campaign = crate::resolution::tests::campaign(0, 1);
|
||||
let expected_goals = vec!["keep the twelve patients together".into()];
|
||||
let expected_memories = vec!["Promised Ash to stay with the twelve.".into()];
|
||||
let expected_relationships =
|
||||
BTreeMap::from([("ash".into(), "trusted with a responsibility".into())]);
|
||||
{
|
||||
let actor = campaign.actors.get_mut("player").unwrap();
|
||||
actor.goals = expected_goals.clone();
|
||||
actor.memories = expected_memories.clone();
|
||||
actor.relationships = expected_relationships.clone();
|
||||
}
|
||||
|
||||
let slice = constituent_slice(&campaign, "player").unwrap();
|
||||
assert_eq!(slice.goals, expected_goals);
|
||||
assert_eq!(slice.memories, expected_memories);
|
||||
assert_eq!(slice.relationships, expected_relationships);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_slice_projects_only_actionable_salient_member_exceptions() {
|
||||
use crate::domain::*;
|
||||
|
|
|
|||
Loading…
Reference in a new issue