Apply strategic waves from one snapshot

This commit is contained in:
Meta Vens 2026-08-18 05:23:59 +02:00
commit d0449516d8
5 changed files with 122 additions and 9 deletions

View file

@ -1270,6 +1270,11 @@ fn apply_strategic_tick_plan(
campaign: &mut Campaign,
plan: crate::domain::StrategicTickPlan,
) -> Result<Vec<crate::domain::Event>, KernelError> {
// Every action in a strategic wave was chosen against the same committed
// snapshot. Keep that snapshot immutable while applying to a private copy
// so action ordering cannot rewrite another action's permissions and an
// invalid late action cannot leave this primitive partially mutated.
let mut next = campaign.clone();
let revision = campaign.revision + 1;
let at = campaign.world_time;
let mut events = Vec::new();
@ -1295,7 +1300,7 @@ fn apply_strategic_tick_plan(
));
}
validate_public_channels(&action.public_channels)?;
let institution = campaign
let institution = next
.institutions
.get_mut(&action.institution_id)
.ok_or_else(|| KernelError::Invalid("strategic plan invented an institution".into()))?;
@ -1330,7 +1335,7 @@ fn apply_strategic_tick_plan(
));
}
validate_public_channels(&action.public_channels)?;
let gestalt = campaign
let gestalt = next
.gestalts
.get_mut(&action.gestalt_id)
.ok_or_else(|| KernelError::Invalid("strategic plan invented a gestalt".into()))?;
@ -1397,13 +1402,13 @@ fn apply_strategic_tick_plan(
let destination_name = campaign.gestalts[&action.destination_gestalt_id]
.name
.clone();
let gestalt = campaign
let gestalt = next
.gestalts
.get_mut(&action.gestalt_id)
.expect("gestalt migration source was validated");
gestalt.home_location_id = action.destination_location_id.clone();
gestalt.version = gestalt.version.saturating_add(1);
let profile = campaign
let profile = next
.agency_profiles
.get_mut(&action.gestalt_id)
.expect("gestalt migration profile was validated");
@ -1553,8 +1558,7 @@ fn apply_strategic_tick_plan(
}
let origin = actor.location_id.clone();
let actor_name = actor.name.clone();
campaign
.actors
next.actors
.get_mut(&action.actor_id)
.expect("actor was validated")
.location_id = action.destination_id.clone();
@ -1686,7 +1690,7 @@ fn apply_strategic_tick_plan(
.clone()
});
let member_name = campaign.gestalt_members[&action.member_id].name.clone();
rebase_member_migration(campaign, &action)?;
rebase_member_migration(&mut next, &action)?;
events.push(crate::domain::Event {
id: format!("strategic:{revision}:member:{}", action.member_id),
at,
@ -1702,6 +1706,7 @@ fn apply_strategic_tick_plan(
public_channels: action.public_channels,
});
}
*campaign = next;
Ok(events)
}
@ -2714,6 +2719,86 @@ mod tests {
);
}
#[test]
fn simultaneous_migration_preserves_snapshot_scoped_activity() {
let mut value = hierarchical_refugee_campaign();
value.gestalts.insert(
"camp-neighbors".into(),
GestaltPersonaState {
schema: "ghostlight.gestalt_persona_state.v1".into(),
id: "camp-neighbors".into(),
name: "Camp neighbors".into(),
version: 0,
home_location_id: "camp".into(),
shared_capabilities: BTreeSet::from(["raise signal fires".into()]),
shared_knowledge: BTreeSet::new(),
resources: BTreeSet::new(),
goals: vec!["keep departures visible".into()],
pressures: vec![],
},
);
crate::resolution::ensure_agency_profiles(&mut value);
let events = apply_strategic_tick_plan(
&mut value,
StrategicTickPlan {
gestalt_migrations: vec![StrategicGestaltMigration {
gestalt_id: "refugees-east".into(),
destination_gestalt_id: "dock-neighbors".into(),
destination_location_id: "docks".into(),
public_channels: vec![],
}],
gestalt_activities: vec![StrategicGestaltActivity {
gestalt_id: "camp-neighbors".into(),
activity: StrategicActivityKind::Communicate,
target_subject_ids: vec!["refugees-east".into()],
location_ids: vec!["camp".into()],
public_channels: vec![],
}],
..Default::default()
},
)
.unwrap();
assert_eq!(value.gestalts["refugees-east"].home_location_id, "docks");
assert_eq!(events.len(), 2);
assert_eq!(events[1].kind, "gestalt_activity");
assert_eq!(events[1].location_ids, vec!["camp"]);
assert_eq!(
events[1].summary,
"Camp neighbors sends a communication to Eastern transit refugees."
);
}
#[test]
fn invalid_late_strategic_action_cannot_partially_apply_an_earlier_action() {
let mut value = hierarchical_refugee_campaign();
let before = value.clone();
let error = apply_strategic_tick_plan(
&mut value,
StrategicTickPlan {
gestalt_migrations: vec![StrategicGestaltMigration {
gestalt_id: "refugees-east".into(),
destination_gestalt_id: "dock-neighbors".into(),
destination_location_id: "docks".into(),
public_channels: vec![],
}],
gestalt_activities: vec![StrategicGestaltActivity {
gestalt_id: "refugees-east".into(),
activity: StrategicActivityKind::Prepare,
target_subject_ids: vec![],
location_ids: vec!["camp".into()],
public_channels: vec![],
}],
..Default::default()
},
)
.unwrap_err();
assert!(error.to_string().contains("acts twice"));
assert_eq!(value, before);
}
#[test]
fn population_cannot_move_without_its_exact_migration_relation() {
let mut value = hierarchical_refugee_campaign();

View file

@ -454,6 +454,16 @@ population transition receives its exact current pressure set and the failed
transition reason. The correction model is not expected to reconstruct a
validator from a generic rejection.
The WorldKernel resolves every strategic batch against one immutable campaign
snapshot. It applies validated transitions to a private working copy and swaps
that copy into the campaign only after the whole batch succeeds. Earlier
actions in the batch therefore cannot rewrite the spatial, relationship, or
knowledge permissions used by later actions. This matters when, for example, a
camp population signals refugees during the same strategic horizon in which
those refugees depart: both choices are judged from the shared starting world,
while the committed result still contains the migration. A late invalid action
discards the working copy and leaves the campaign untouched.
An actionful cell has a fourth private stage after the Interpreter: the
`cell_effect_verifier`. This cheap structured reader compares the natural
Persona decision with each attributed typed effect. It rejects reversals,

View file

@ -332,6 +332,17 @@ or waits from the Persona turn, forbids absence-as-inaction, and requests
reasons within 160 characters. Rerun a 23-wave fairness-length budget-8 profile
at provider parallelism 8 from the same committed migration baseline.
The next exact-build profile passed the original Mira action/inaction conflict
after local validation began returning the exact failed condition instead of a
four-way generic error. It then exposed a deeper kernel seam: one camp Gestalt
communicated with `refugees-east` in the same wave that the refugees migrated.
The proposal was valid against the shared snapshot, but the apply loop moved
the refugees before rechecking the later communication. Strategic application
now reads the original snapshot, mutates a private working campaign, and swaps
it in only on total success. Regressions cover the simultaneous
migration/communication case and prove that an invalid late action cannot
partially apply an earlier migration.
Do not call setting-wide material agency finished yet. Most population output
is still an attributed `gestalt_activity` attempt stored as history. The long
run became repetitive around ferry loading and shelter requests because no

View file

@ -192,6 +192,13 @@ cannot duplicate, cross cell authority, or coexist with an action from the same
subject. They are non-mutating audit/fairness evidence. The former global
inaction string is no longer an owner or runtime path.
Strategic plan application reads authority and permissions from one immutable
command snapshot. All writes land in a private working campaign which replaces
the command-local campaign only after every action validates. Sequential loop
order is no longer a fictional owner: a migration earlier in the apply loop
cannot invalidate another subject's same-snapshot communication, and a late
invalid action cannot leave earlier in-memory mutations behind.
The active cell budget and provider concurrency limit are separate controls.
Budget and pins increment `resolution_epoch` without advancing world revision or
fictional time. Provider concurrency increments only

View file

@ -267,6 +267,6 @@ data_flow:
- If state grows faster than understanding, cut it back.
current_status:
summary: GhostlightDungeon production remains on the verified Starfire release. The 31-wave budget-8/provider-parallelism-4 live profile covered every subject, committed 194 consequences, recovered 15 rejected pulses without partial mutation, preserved player and rival/member agency, and returned the same Mira. Its trace exposed the missing attributed-inaction representation; the global inaction string has now been replaced by exact non-mutating per-subject inactions. Production state remains untouched.
next_action: Rerun a 23-wave budget-8/provider-parallelism-8 profile after forbidding invented inaction for unvoiced subjects, then compare retry rate, cache efficiency, latency, and exact cover behavior against the 6253f9f parallelism-4 baseline. Keep a future strategic outcome resolver separate from Interpreter authority.
summary: GhostlightDungeon production remains on the verified Starfire release. The 31-wave budget-8/provider-parallelism-4 live profile covered every subject, committed 194 consequences, recovered 15 rejected pulses without partial mutation, preserved player and rival/member agency, and returned the same Mira. Attributed inaction is now exact and non-mutating. The latest live profile also exposed and repaired sequential strategic application: every batch now validates from one immutable snapshot and swaps in a private working campaign only after total success. Production state remains untouched.
next_action: Rerun the 23-wave budget-8/provider-parallelism-8 profile from the committed migration baseline after the immutable-snapshot strategic apply cut, then compare retry rate, cache efficiency, latency, exact cover behavior, and earned callback against the 6253f9f parallelism-4 baseline. Keep a future strategic outcome resolver separate from Interpreter authority.
gestalt_population: Gestalt Personas own shared population baselines; persistent member deltas own named individuality; WorldKernel materialization and approval-gated fission preserve lineage and identity. Presence projection is limited to nearby active leaves and exact-location dormant members, includes reciprocal player relationship evidence, and may propose an earned cast without committing it. Simulation cells are derived covers only.