Apply strategic waves from one snapshot
This commit is contained in:
parent
e0bf1c2116
commit
d0449516d8
5 changed files with 122 additions and 9 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue