Constrain strategic effects by exact subject authority
This commit is contained in:
parent
3607771f33
commit
3f1c7a5cc4
1 changed files with 162 additions and 12 deletions
|
|
@ -496,7 +496,7 @@ fn cell_interpreter_context(
|
|||
"exact_permissions": slice.constituents.iter().filter(|subject| active_subject_ids.contains(&subject.subject_id)).map(|subject| serde_json::json!({
|
||||
"subject_id": subject.subject_id,
|
||||
"subject_kind": subject.subject_kind,
|
||||
"allowed_effect_type": allowed_effect_type(&subject.subject_kind),
|
||||
"allowed_effect_types": allowed_constituent_effect_types(subject),
|
||||
"collective_authority_id": subject.collective_authority_id,
|
||||
"location_ids": subject.location_ids,
|
||||
"allowed_public_channels": subject.information_channels,
|
||||
|
|
@ -510,7 +510,7 @@ fn cell_interpreter_context(
|
|||
"member_permissions": slice.member_exceptions.iter().filter(|member| active_subject_ids.contains(&member.subject_id)).map(|member| serde_json::json!({
|
||||
"subject_id": member.subject_id,
|
||||
"member_id": member.member_id,
|
||||
"allowed_effect_type": "member_migration_or_member_activity",
|
||||
"allowed_effect_types": allowed_member_effect_types(member),
|
||||
"source_gestalt_id": member.source_gestalt_id,
|
||||
"source_location_id": member.source_location_id,
|
||||
"allowed_public_channels": member.information_channels,
|
||||
|
|
@ -736,16 +736,34 @@ fn required_projection_subject_ids(slice: &PermittedCellSlice) -> BTreeSet<Strin
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn allowed_effect_type(kind: &crate::domain::AgencySubjectKind) -> &'static str {
|
||||
match kind {
|
||||
crate::domain::AgencySubjectKind::Actor => "actor_move_or_actor_activity",
|
||||
crate::domain::AgencySubjectKind::Institution => "institution",
|
||||
fn allowed_constituent_effect_types(subject: &CellConstituentSlice) -> Vec<&'static str> {
|
||||
match subject.subject_kind {
|
||||
crate::domain::AgencySubjectKind::Actor => {
|
||||
let mut types = vec!["actor_activity"];
|
||||
if !subject.reachable_destination_ids.is_empty() {
|
||||
types.push("actor_move");
|
||||
}
|
||||
types
|
||||
}
|
||||
crate::domain::AgencySubjectKind::Institution => vec!["institution"],
|
||||
crate::domain::AgencySubjectKind::Gestalt => {
|
||||
"gestalt_pressure_or_gestalt_activity_or_gestalt_migration"
|
||||
let mut types = vec!["gestalt", "gestalt_activity"];
|
||||
if !subject.migration_destinations.is_empty() {
|
||||
types.push("gestalt_migration");
|
||||
}
|
||||
types
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn allowed_member_effect_types(member: &CellMemberSlice) -> Vec<&'static str> {
|
||||
let mut types = vec!["member_activity"];
|
||||
if !member.migration_destinations.is_empty() {
|
||||
types.push("member_migration");
|
||||
}
|
||||
types
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CellProjectionEngine {
|
||||
pub model: Arc<dyn ModelPort>,
|
||||
|
|
@ -884,7 +902,7 @@ impl CellProjectionEngine {
|
|||
let permission_guidance = format!(
|
||||
concat!(
|
||||
"Emit at most {} exact constituent- or named-member-attributed attempts. Priority is an urgency score from 0 to 100 where higher numbers resolve first. ",
|
||||
"Copy each subject's allowed_effect_type: institution -> institution, gestalt -> gestalt pressure transition, gestalt_activity, or gestalt_migration, actor -> actor_move, named member -> member_migration or member_activity. ",
|
||||
"Copy each subject's exact allowed_effect_types. A lane absent from that list is structurally unavailable: do not emit it and do not invent a destination. ",
|
||||
"Use gestalt_activity or member_activity for a concrete attempt that does not itself change pressure. Map attempts narrowly: communicate means speak, send, offer, ask, or notify; coordinate means arrange a joint attempt; prepare means the subject's own concrete work; investigate means seek information; recruit means invite; trade means offer an exchange; obstruct means attempt interference. ",
|
||||
"target_subject_ids and location_ids must come from that exact subject's permissions. Every activity has at most four unique target_subject_ids; choose the four most causally relevant when more permitted subjects are involved. A member_activity uses exactly the member's source_location_id. Internal work is prepare with no targets. A local investigate may have no target and use the exact current location to seek information from the environment or an unnamed ordinary role; asking an unnamed clerk or dock master for facts maps here and records only the inquiry, never a reply or discovery. A local communicate may likewise have no target at the exact current location when the Persona speaks, sends, offers, asks permission, or notifies an unnamed ordinary role; it records only the source's outgoing attempt, never a listener, reply, acceptance, or outcome. Communication with a canonical subject requires that exact target ID. Never substitute a containing population, related institution, or merely permitted ID for an unnamed role. ",
|
||||
"Write intended_effect as the attempted act, never its hoped-for outcome or target response. Merely waiting, watching, staying, holding position, or remaining ready is attributed inaction, not prepare. prepare requires concrete work on a bounded arrangement, repair, resource, or capability-backed readiness change. Institution posture must be a specific materially new commitment or withholding of at most 240 characters. already_committed_posture is state already in force: maintaining, continuing, or restating it is inaction and must not emit an institution action. Gestalt pressure_resolutions copy exact current_pressures; additions are new unresolved constraints, never completed actions. Use only permitted state references and public channels. ",
|
||||
|
|
@ -1806,9 +1824,83 @@ fn constrain_cell_proposal_schema(
|
|||
"subject_id".into(),
|
||||
serde_json::json!({"type":"string","enum":subject_ids}),
|
||||
);
|
||||
let exact_subject_conditions = slice
|
||||
.constituents
|
||||
.iter()
|
||||
.filter(|subject| active_subject_ids.contains(&subject.subject_id))
|
||||
.map(|subject| {
|
||||
exact_cell_action_condition(
|
||||
&subject.subject_id,
|
||||
allowed_constituent_effect_types(subject),
|
||||
&subject.permitted_state_references,
|
||||
&subject.information_channels,
|
||||
)
|
||||
})
|
||||
.chain(
|
||||
slice
|
||||
.member_exceptions
|
||||
.iter()
|
||||
.filter(|member| active_subject_ids.contains(&member.subject_id))
|
||||
.map(|member| {
|
||||
exact_cell_action_condition(
|
||||
&member.subject_id,
|
||||
allowed_member_effect_types(member),
|
||||
&member.permitted_state_references,
|
||||
&member.information_channels,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
let action_candidate = schema
|
||||
.pointer_mut("/$defs/CellActionCandidate")
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
.ok_or_else(|| anyhow!("cell appraisal schema has no action candidate"))?;
|
||||
action_candidate.insert(
|
||||
"allOf".into(),
|
||||
serde_json::Value::Array(exact_subject_conditions),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exact_cell_action_condition(
|
||||
subject_id: &str,
|
||||
allowed_effect_types: Vec<&'static str>,
|
||||
permitted_state_references: &BTreeSet<String>,
|
||||
information_channels: &BTreeSet<String>,
|
||||
) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"if":{
|
||||
"properties":{"subject_id":{"const":subject_id}},
|
||||
"required":["subject_id"]
|
||||
},
|
||||
"then":{
|
||||
"properties":{
|
||||
"state_references":exact_string_array_schema(permitted_state_references, 16),
|
||||
"public_channels":exact_string_array_schema(information_channels, 8),
|
||||
"effect":{
|
||||
"properties":{
|
||||
"type":{"type":"string","enum":allowed_effect_types}
|
||||
},
|
||||
"required":["type"]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn exact_string_array_schema(values: &BTreeSet<String>, max_items: usize) -> serde_json::Value {
|
||||
if values.is_empty() {
|
||||
serde_json::json!({"type":"array","maxItems":0})
|
||||
} else {
|
||||
serde_json::json!({
|
||||
"type":"array",
|
||||
"uniqueItems":true,
|
||||
"maxItems":max_items,
|
||||
"items":{"type":"string","enum":values}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn constrain_interpreter_schema(
|
||||
schema: &mut serde_json::Value,
|
||||
slice: &PermittedActorSlice,
|
||||
|
|
@ -1913,12 +2005,12 @@ mod tests {
|
|||
return Ok(serde_json::json!({
|
||||
"actions":[{
|
||||
"subject_id":"faction-06",
|
||||
"intent":"publish a position",
|
||||
"intended_effect":"move a person instead",
|
||||
"intent":"continue weighing the position",
|
||||
"intended_effect":"retain the posture already in force",
|
||||
"priority":5,
|
||||
"state_references":["institution:faction-06"],
|
||||
"public_channels":["public bulletin"],
|
||||
"effect":{"type":"actor_move","destination_id":"forum"}
|
||||
"effect":{"type":"institution","posture":"weighing whether to publish a position","location_ids":["forum"]}
|
||||
}],
|
||||
"inactions":[]
|
||||
})
|
||||
|
|
@ -1926,7 +2018,9 @@ mod tests {
|
|||
}
|
||||
self.saw_rejected_appraisal.store(
|
||||
request.lived_stream.contains("PREVIOUS_REJECTED_APPRAISAL")
|
||||
&& request.lived_stream.contains("actor_move")
|
||||
&& request
|
||||
.lived_stream
|
||||
.contains("weighing whether to publish a position")
|
||||
&& request.lived_stream.contains("faction-06"),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
|
@ -2237,6 +2331,62 @@ mod tests {
|
|||
assert!(!CELL_APPRAISAL_OUTPUT_CONTRACT.contains("\"member_id\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_schema_removes_effect_lanes_the_exact_actor_cannot_use() {
|
||||
let mut slice = fixture_cell_slice();
|
||||
let actor_id = "relationship-anchor:reed".to_owned();
|
||||
{
|
||||
let actor = &mut slice.constituents[0];
|
||||
actor.subject_id = actor_id.clone();
|
||||
actor.subject_kind = AgencySubjectKind::Actor;
|
||||
actor.name = "Reed".into();
|
||||
actor.permitted_state_references =
|
||||
BTreeSet::from(["subject:relationship-anchor:reed".into()]);
|
||||
actor.information_channels.clear();
|
||||
actor.current_posture = None;
|
||||
actor.reachable_destination_ids.clear();
|
||||
actor.migration_destinations.clear();
|
||||
}
|
||||
slice.detail_focus_subject_id = Some(actor_id.clone());
|
||||
let active = BTreeSet::from([actor_id]);
|
||||
let mut schema = serde_json::to_value(schema_for!(CellAppraisalProposal)).unwrap();
|
||||
constrain_cell_proposal_schema(&mut schema, &slice, &active).unwrap();
|
||||
let validator = jsonschema::validator_for(&schema).unwrap();
|
||||
let action = |effect| {
|
||||
serde_json::json!({
|
||||
"actions":[{
|
||||
"subject_id":"relationship-anchor:reed",
|
||||
"intent":"keep the patients together",
|
||||
"intended_effect":"make one bounded local attempt",
|
||||
"priority":80,
|
||||
"state_references":["subject:relationship-anchor:reed"],
|
||||
"public_channels":[],
|
||||
"effect":effect
|
||||
}],
|
||||
"inactions":[]
|
||||
})
|
||||
};
|
||||
|
||||
assert!(validator.is_valid(&action(serde_json::json!({
|
||||
"type":"actor_activity",
|
||||
"activity":"prepare",
|
||||
"target_subject_ids":[],
|
||||
"location_ids":["forum"]
|
||||
}))));
|
||||
assert!(!validator.is_valid(&action(serde_json::json!({
|
||||
"type":"actor_move",
|
||||
"destination_id":"forum"
|
||||
}))));
|
||||
assert!(!validator.is_valid(&action(serde_json::json!({
|
||||
"type":"gestalt_migration",
|
||||
"destination_gestalt_id":"loc_water_ice_rail_corridor"
|
||||
}))));
|
||||
assert_eq!(
|
||||
allowed_constituent_effect_types(&slice.constituents[0]),
|
||||
vec!["actor_activity"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effect_verifier_binding_changes_with_the_exact_action_bundle() {
|
||||
let first = crate::domain::CellActionProposal {
|
||||
|
|
|
|||
Loading…
Reference in a new issue