Separate agency knowledge from news channels
This commit is contained in:
parent
25f49af98e
commit
a830fe6699
3 changed files with 115 additions and 16 deletions
|
|
@ -1,8 +1,7 @@
|
|||
use anyhow::{Context, Result};
|
||||
use ghostlight_dungeon::{
|
||||
domain::{
|
||||
Campaign, CellAppraisal, ResolutionCover, StrategicActivityOutcome,
|
||||
StrategicTickReceipt,
|
||||
Campaign, CellAppraisal, ResolutionCover, StrategicActivityOutcome, StrategicTickReceipt,
|
||||
},
|
||||
model::ModelStageReceipt,
|
||||
persistence::CampaignStore,
|
||||
|
|
@ -34,9 +33,7 @@ fn main() -> Result<()> {
|
|||
(Some(revision), Some(epoch)) => store
|
||||
.load_all::<ResolutionCover>("resolution_cover.v1")?
|
||||
.into_iter()
|
||||
.find(|cover| {
|
||||
cover.world_revision == revision && cover.resolution_epoch == epoch
|
||||
}),
|
||||
.find(|cover| cover.world_revision == revision && cover.resolution_epoch == epoch),
|
||||
_ => None,
|
||||
};
|
||||
let appraisals = match (snapshot_revision, resolution_epoch) {
|
||||
|
|
@ -55,10 +52,8 @@ fn main() -> Result<()> {
|
|||
let mut outcomes = Vec::new();
|
||||
for key in store.keys("strategic_activity_outcome.v1")? {
|
||||
if key.starts_with(&prefix)
|
||||
&& let Some((_, outcome)) = store.load::<StrategicActivityOutcome>(
|
||||
"strategic_activity_outcome.v1",
|
||||
&key,
|
||||
)?
|
||||
&& let Some((_, outcome)) = store
|
||||
.load::<StrategicActivityOutcome>("strategic_activity_outcome.v1", &key)?
|
||||
{
|
||||
outcomes.push(outcome);
|
||||
}
|
||||
|
|
@ -99,7 +94,10 @@ fn main() -> Result<()> {
|
|||
|
||||
let mut subjects = BTreeMap::new();
|
||||
for actor in campaign.actors.values() {
|
||||
subjects.insert(actor.id.clone(), json!({ "kind": "actor", "name": actor.name }));
|
||||
subjects.insert(
|
||||
actor.id.clone(),
|
||||
json!({ "kind": "actor", "name": actor.name }),
|
||||
);
|
||||
}
|
||||
for institution in campaign.institutions.values() {
|
||||
subjects.insert(
|
||||
|
|
|
|||
|
|
@ -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. 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 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.",
|
||||
serde_json::to_string(&subject_briefs)?
|
||||
);
|
||||
let agency_schema = serde_json::to_value(schema_for!(CompiledAgencySkeleton))?;
|
||||
|
|
@ -3016,16 +3016,42 @@ fn apply_compiled_agency_skeleton(
|
|||
.filter(|id| !campaign.locations.contains_key(*id))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let knowledge_channel_overlap = match input.subject_kind {
|
||||
AgencySubjectKind::Actor => campaign
|
||||
.actors
|
||||
.get(&input.subject_id)
|
||||
.map(|actor| {
|
||||
input
|
||||
.information_channels
|
||||
.intersection(&actor.knowledge)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
AgencySubjectKind::Gestalt => campaign
|
||||
.gestalts
|
||||
.get(&input.subject_id)
|
||||
.map(|gestalt| {
|
||||
input
|
||||
.information_channels
|
||||
.intersection(&gestalt.shared_knowledge)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
AgencySubjectKind::Institution => Vec::new(),
|
||||
};
|
||||
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()
|
||||
{
|
||||
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={:?}",
|
||||
"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:?}",
|
||||
input.subject_id,
|
||||
profile.subject_kind,
|
||||
input.subject_kind,
|
||||
|
|
@ -3244,6 +3270,43 @@ fn validate_opening_playability(campaign: &Campaign) -> Result<()> {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn agency_compiler_cannot_disguise_private_knowledge_as_a_channel() {
|
||||
let mut campaign = crate::resolution::tests::campaign(0, 1);
|
||||
campaign
|
||||
.actors
|
||||
.get_mut("player")
|
||||
.unwrap()
|
||||
.knowledge
|
||||
.insert("convoy vulnerabilities".into());
|
||||
let facets = BTreeMap::from([
|
||||
(AgencyAxis::Geography, BTreeSet::from(["center".into()])),
|
||||
(AgencyAxis::Ideology, BTreeSet::from(["unknown".into()])),
|
||||
(AgencyAxis::Authority, BTreeSet::from(["unknown".into()])),
|
||||
(AgencyAxis::EconomyRole, BTreeSet::from(["unknown".into()])),
|
||||
(AgencyAxis::SpeciesBody, BTreeSet::from(["unknown".into()])),
|
||||
(
|
||||
AgencyAxis::Information,
|
||||
BTreeSet::from(["convoy vulnerabilities".into()]),
|
||||
),
|
||||
]);
|
||||
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(["convoy vulnerabilities".into()]),
|
||||
}],
|
||||
vec![],
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(error.to_string().contains("knowledge_channel_overlap"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_text_admission_rejects_empty_oversized_and_binary_control_input() {
|
||||
assert!(validate_user_text("field", "", 8).is_err());
|
||||
|
|
@ -3661,7 +3724,7 @@ mod tests {
|
|||
}).to_string()
|
||||
}
|
||||
"agency_compile" => serde_json::json!({
|
||||
"agency_profiles":[{"subject_id":"yard-workers","subject_kind":"gestalt","collective_authority_id":"yard-workers","facets":{"geography":["yard"],"ideology":["mutual aid"],"authority":["yard-workers"],"economy_role":["maintenance"],"species_body":["human"],"information":["yard routines"]},"location_ids":["yard"],"information_channels":["yard routines"]}],
|
||||
"agency_profiles":[{"subject_id":"yard-workers","subject_kind":"gestalt","collective_authority_id":"yard-workers","facets":{"geography":["yard"],"ideology":["mutual aid"],"authority":["yard-workers"],"economy_role":["maintenance"],"species_body":["human"],"information":["yard routines"]},"location_ids":["yard"],"information_channels":["yard bulletin"]}],
|
||||
"agency_relations":[]
|
||||
}).to_string(),
|
||||
_ => return Err(anyhow!("unexpected stage")),
|
||||
|
|
|
|||
|
|
@ -84,7 +84,14 @@ pub fn ensure_agency_profiles(campaign: &mut Campaign) {
|
|||
profile.location_ids = BTreeSet::from([actor.location_id.clone()]);
|
||||
profile
|
||||
.information_channels
|
||||
.extend(actor.knowledge.iter().cloned());
|
||||
.retain(|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));
|
||||
}
|
||||
}
|
||||
let live: BTreeSet<_> = campaign
|
||||
|
|
@ -143,7 +150,6 @@ fn profile_for_actor(actor: &ActorState, evidence: &[String]) -> AgencyProfile {
|
|||
AgencyAxis::Information,
|
||||
actor.knowledge.iter().cloned().collect(),
|
||||
);
|
||||
profile.information_channels = actor.knowledge.iter().cloned().collect();
|
||||
profile
|
||||
}
|
||||
|
||||
|
|
@ -185,7 +191,6 @@ fn profile_for_gestalt(value: &GestaltPersonaState, evidence: &[String]) -> Agen
|
|||
AgencyAxis::Information,
|
||||
value.shared_knowledge.iter().cloned().collect(),
|
||||
);
|
||||
profile.information_channels = value.shared_knowledge.iter().cloned().collect();
|
||||
profile
|
||||
}
|
||||
|
||||
|
|
@ -2922,6 +2927,39 @@ pub(crate) mod tests {
|
|||
value
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_maintenance_never_promotes_knowledge_into_public_channels() {
|
||||
let mut value = campaign(1, 1);
|
||||
value
|
||||
.actors
|
||||
.get_mut("player")
|
||||
.unwrap()
|
||||
.knowledge
|
||||
.insert("private convoy vulnerability".into());
|
||||
|
||||
value.agency_profiles.remove("player");
|
||||
ensure_agency_profiles(&mut value);
|
||||
assert!(
|
||||
value.agency_profiles["player"]
|
||||
.information_channels
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
value
|
||||
.agency_profiles
|
||||
.get_mut("player")
|
||||
.unwrap()
|
||||
.information_channels = BTreeSet::from([
|
||||
"private convoy vulnerability".into(),
|
||||
"licensed courier wire".into(),
|
||||
]);
|
||||
ensure_agency_profiles(&mut value);
|
||||
assert_eq!(
|
||||
value.agency_profiles["player"].information_channels,
|
||||
BTreeSet::from(["licensed courier wire".into()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aetheria_scale_cover_is_complete_unique_and_budgeted() {
|
||||
for budget in [1, 4, 8, 32] {
|
||||
|
|
|
|||
Loading…
Reference in a new issue