Route population fission through mutation algebra
This commit is contained in:
parent
6fbe47ad88
commit
c7463c0506
10 changed files with 706 additions and 94 deletions
|
|
@ -285,6 +285,7 @@ struct CompiledFissionSeed {
|
|||
child_partition_values: BTreeMap<String, String>,
|
||||
#[serde(default)]
|
||||
member_child_assignments: BTreeMap<String, String>,
|
||||
resource_child_assignments: BTreeMap<String, String>,
|
||||
gaps: Vec<String>,
|
||||
}
|
||||
|
||||
|
|
@ -971,7 +972,7 @@ impl WorldCompiler {
|
|||
.retrieve_all(&queries, &campaign.branch_origin.canon_cutoff, 12)
|
||||
.await?;
|
||||
let base_prompt = format!(
|
||||
"Refine one canonical leaf gestalt along exactly the requested facet. Produce one child per requested value plus one mandatory child whose value is exactly 'other/unknown'. Children inherit the parent baseline and contain only justified refinements; every child starts at version 0 and uses an existing campaign location. Do not erase or rewrite member deltas. Assign a member only when evidence or durable existing delta supports the cut; unassigned members will remain in other/unknown. List every material lore gap. This is an approval preview, not a commit. SUBJECT:\n{}\nEVIDENCE:\n{}",
|
||||
"Refine one canonical leaf gestalt along exactly the requested facet. Produce one child per requested value plus one mandatory child whose value is exactly 'other/unknown'. Every child starts at version 0 and uses an existing campaign location. Each child must copy the parent's shared capabilities, shared knowledge, goals, and pressures exactly; the partition facet belongs to the agency profile and does not silently rewrite the population baseline. Exact scarce resources are not inheritable traits: assign every parent resource to exactly one child in resource_child_assignments, and make each child's resources set equal exactly the resources assigned to that child. Do not create, duplicate, omit, or rename a resource. Do not erase or rewrite member deltas. Assign a member only when evidence or durable existing delta supports the cut; unassigned members will remain in other/unknown. List every material lore gap. This is an approval preview, not a commit. SUBJECT:\n{}\nEVIDENCE:\n{}",
|
||||
serde_json::to_string(&subject)?,
|
||||
evidence_text(&receipts),
|
||||
);
|
||||
|
|
@ -1044,6 +1045,7 @@ impl WorldCompiler {
|
|||
child_partition_values: compiled.child_partition_values,
|
||||
residual_child_id: residual_child_id.unwrap_or_default(),
|
||||
member_child_assignments: compiled.member_child_assignments,
|
||||
resource_child_assignments: compiled.resource_child_assignments,
|
||||
evidence_receipt_ids,
|
||||
gaps,
|
||||
canon_candidates,
|
||||
|
|
|
|||
|
|
@ -615,6 +615,11 @@ pub struct GestaltFissionPreview {
|
|||
pub residual_child_id: String,
|
||||
#[serde(default)]
|
||||
pub member_child_assignments: BTreeMap<String, String>,
|
||||
/// Exact scarce-resource custody after the split. Every resource owned by
|
||||
/// the parent appears once and names one child; it is never inherited by
|
||||
/// every child as population baseline.
|
||||
#[serde(default)]
|
||||
pub resource_child_assignments: BTreeMap<String, String>,
|
||||
pub evidence_receipt_ids: Vec<String>,
|
||||
pub gaps: Vec<String>,
|
||||
#[serde(default)]
|
||||
|
|
|
|||
|
|
@ -870,8 +870,19 @@ fn execute(
|
|||
"fission preview evidence receipts were not supplied".into(),
|
||||
));
|
||||
}
|
||||
crate::resolution::apply_fission(&mut campaign, &preview)
|
||||
.map_err(|error| KernelError::Invalid(error.to_string()))?;
|
||||
let transition = crate::legacy_transition::lower_fission(
|
||||
&campaign,
|
||||
&preview,
|
||||
Utc::now() + Duration::minutes(5),
|
||||
)
|
||||
.map_err(|error| KernelError::Invalid(error.to_string()))?;
|
||||
let mutation_receipt = crate::legacy_transition::apply_lowered_fission(
|
||||
&mut campaign,
|
||||
&preview,
|
||||
&transition,
|
||||
Utc::now(),
|
||||
)
|
||||
.map_err(|error| KernelError::Invalid(error.to_string()))?;
|
||||
for candidate in &preview.canon_candidates {
|
||||
campaign
|
||||
.canon_candidates
|
||||
|
|
@ -885,6 +896,7 @@ fn execute(
|
|||
evidence_receipts,
|
||||
preview.canon_candidates,
|
||||
model_stage_receipts,
|
||||
Some((transition, mutation_receipt)),
|
||||
)
|
||||
}
|
||||
WorldCommand::AdvanceStrategicTick {
|
||||
|
|
@ -1097,6 +1109,7 @@ fn execute(
|
|||
evidence_receipts,
|
||||
canon_candidates,
|
||||
model_stage_receipts,
|
||||
None,
|
||||
)
|
||||
}
|
||||
WorldCommand::MaterializeGestaltMember {
|
||||
|
|
@ -3098,17 +3111,32 @@ fn commit_with_records(
|
|||
evidence: Vec<VaultEvidenceReceipt>,
|
||||
candidates: Vec<CanonCandidate>,
|
||||
model_receipts: Vec<crate::model::ModelStageReceipt>,
|
||||
mutation: Option<(
|
||||
crate::legacy_transition::LoweredLegacyTransition,
|
||||
crate::transition::WorldMutationReceipt,
|
||||
)>,
|
||||
) -> Result<CommandResult, KernelError> {
|
||||
crate::resolution::ensure_agency_profiles(&mut campaign);
|
||||
let previous_revision = campaign.revision;
|
||||
campaign.revision += 1;
|
||||
if let Some((_, mutation_receipt)) = &mutation
|
||||
&& (mutation_receipt.previous_world_revision != previous_revision
|
||||
|| mutation_receipt.world_revision != campaign.revision)
|
||||
{
|
||||
return Err(KernelError::Invalid(
|
||||
"mutation receipt does not bind the recorded world commit".into(),
|
||||
));
|
||||
}
|
||||
let receipt = WorldCommitReceipt {
|
||||
schema: "ghostlight.world_commit_receipt.v1".into(),
|
||||
campaign_id: campaign.id,
|
||||
previous_revision,
|
||||
revision: campaign.revision,
|
||||
command_kind: kind.into(),
|
||||
committed_at: Utc::now(),
|
||||
committed_at: mutation
|
||||
.as_ref()
|
||||
.map(|(_, receipt)| receipt.committed_at)
|
||||
.unwrap_or_else(Utc::now),
|
||||
roll: None,
|
||||
};
|
||||
store
|
||||
|
|
@ -3120,6 +3148,9 @@ fn commit_with_records(
|
|||
&evidence,
|
||||
&candidates,
|
||||
&model_receipts,
|
||||
mutation
|
||||
.as_ref()
|
||||
.map(|(transition, receipt)| (&transition.authority, &transition.batch, receipt)),
|
||||
)
|
||||
.map_err(persist)?;
|
||||
Ok(CommandResult::Committed { campaign, receipt })
|
||||
|
|
@ -3513,6 +3544,103 @@ mod tests {
|
|||
value
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn approved_fission_commits_one_typed_lineage_batch_and_no_parallel_writer() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = CampaignStore::open(dir.path().join("campaign.cc")).unwrap();
|
||||
let seed = hierarchical_refugee_campaign();
|
||||
let parent = seed.gestalts["refugees-east"].clone();
|
||||
let child = |id: &str, name: &str| GestaltPersonaState {
|
||||
schema: "ghostlight.gestalt_persona_state.v1".into(),
|
||||
id: id.into(),
|
||||
name: name.into(),
|
||||
version: 0,
|
||||
home_location_id: parent.home_location_id.clone(),
|
||||
shared_capabilities: parent.shared_capabilities.clone(),
|
||||
shared_knowledge: parent.shared_knowledge.clone(),
|
||||
resources: BTreeSet::new(),
|
||||
goals: parent.goals.clone(),
|
||||
pressures: parent.pressures.clone(),
|
||||
};
|
||||
let preview = GestaltFissionPreview {
|
||||
schema: "ghostlight.gestalt_fission_preview.v1".into(),
|
||||
campaign_id: seed.id,
|
||||
expected_world_revision: seed.revision,
|
||||
parent_gestalt_id: parent.id.clone(),
|
||||
partition_axis: AgencyAxis::Ideology,
|
||||
children: vec![
|
||||
child("refugees-returning", "Refugees planning to return"),
|
||||
child("refugees-other", "Other eastern transit refugees"),
|
||||
],
|
||||
child_partition_values: BTreeMap::from([
|
||||
("refugees-returning".into(), "returning".into()),
|
||||
("refugees-other".into(), "other/unknown".into()),
|
||||
]),
|
||||
residual_child_id: "refugees-other".into(),
|
||||
member_child_assignments: BTreeMap::from([(
|
||||
"mira".into(),
|
||||
"refugees-returning".into(),
|
||||
)]),
|
||||
resource_child_assignments: BTreeMap::new(),
|
||||
evidence_receipt_ids: vec![],
|
||||
gaps: vec![],
|
||||
canon_candidates: vec![],
|
||||
requires_approval: true,
|
||||
};
|
||||
let kernel = WorldKernel::start(store.clone());
|
||||
kernel
|
||||
.command(WorldCommand::CreateCampaign {
|
||||
campaign: seed.clone(),
|
||||
evidence_receipts: vec![],
|
||||
model_stage_receipts: vec![],
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let result = kernel
|
||||
.command(WorldCommand::FissionGestalt {
|
||||
expected_revision: seed.revision,
|
||||
preview,
|
||||
evidence_receipts: vec![],
|
||||
model_stage_receipts: vec![],
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let CommandResult::Committed { campaign, receipt } = result else {
|
||||
panic!("fission did not commit")
|
||||
};
|
||||
assert_eq!(receipt.command_kind, "fission_gestalt");
|
||||
assert_eq!(campaign.revision, seed.revision + 1);
|
||||
assert_eq!(campaign.gestalt_members["mira"].id, "mira");
|
||||
assert_eq!(
|
||||
campaign.gestalt_members["mira"].gestalt_id,
|
||||
"refugees-returning"
|
||||
);
|
||||
assert!(!campaign.agency_profiles["refugees-east"].active_leaf);
|
||||
assert!(campaign.agency_profiles["refugees-returning"].active_leaf);
|
||||
assert_eq!(
|
||||
store.keys("mutation_authority_envelope.v1").unwrap().len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(store.keys("world_mutation_batch.v1").unwrap().len(), 1);
|
||||
assert_eq!(store.keys("world_mutation_receipt.v1").unwrap().len(), 1);
|
||||
let batch = store
|
||||
.load_all::<crate::transition::WorldMutationBatch>("world_mutation_batch.v1")
|
||||
.unwrap()
|
||||
.pop()
|
||||
.unwrap();
|
||||
let operations = batch
|
||||
.mutations
|
||||
.iter()
|
||||
.map(|mutation| mutation.mutation.operation())
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert!(operations.contains(&crate::transition::WorldMutationOperation::AdmitEntity));
|
||||
assert!(operations.contains(&crate::transition::WorldMutationOperation::PopulationSplit));
|
||||
assert!(
|
||||
operations.contains(&crate::transition::WorldMutationOperation::PopulationTransfer)
|
||||
);
|
||||
assert!(!operations.contains(&crate::transition::WorldMutationOperation::ResourceCreate));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn member_migration_rebases_across_unrelated_hierarchy_without_changing_the_person() {
|
||||
let mut value = hierarchical_refugee_campaign();
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@
|
|||
|
||||
use crate::{
|
||||
domain::{
|
||||
ActorReaction, Campaign, OutcomeBand, StrategicActivityOutcome, StrategicOutcomeEffect,
|
||||
StrategicTickPlan, WorldEffectDelta,
|
||||
ActorReaction, Campaign, GestaltFissionPreview, GestaltLineage, GestaltPersonaState,
|
||||
OutcomeBand, StrategicActivityOutcome, StrategicOutcomeEffect, StrategicTickPlan,
|
||||
WorldEffectDelta,
|
||||
},
|
||||
transition::*,
|
||||
};
|
||||
|
|
@ -142,6 +143,253 @@ pub fn lower_group_travel(
|
|||
)
|
||||
}
|
||||
|
||||
pub fn lower_fission(
|
||||
campaign: &Campaign,
|
||||
preview: &GestaltFissionPreview,
|
||||
expires_at: DateTime<Utc>,
|
||||
) -> Result<LoweredLegacyTransition> {
|
||||
crate::resolution::validate_fission(campaign, preview)?;
|
||||
let preview_digest = digest_serializable(preview)?;
|
||||
let source_receipt_id = format!(
|
||||
"fission-preview:{}",
|
||||
preview_digest.trim_start_matches("sha256:")
|
||||
);
|
||||
let parent = population_subject(&preview.parent_gestalt_id);
|
||||
let mut mutations = Vec::new();
|
||||
for child in &preview.children {
|
||||
let subject = population_subject(&child.id);
|
||||
mutations.push(WorldMutation::AdmitEntity {
|
||||
subject: subject.clone(),
|
||||
initial_components: BTreeSet::from([
|
||||
WorldComponentKind::Identity,
|
||||
WorldComponentKind::Occupancy,
|
||||
WorldComponentKind::Capability,
|
||||
WorldComponentKind::Knowledge,
|
||||
WorldComponentKind::Commitment,
|
||||
WorldComponentKind::Pressure,
|
||||
WorldComponentKind::PopulationLineage,
|
||||
]),
|
||||
initial_place: Some(place_subject(&child.home_location_id)),
|
||||
admission_receipt_id: source_receipt_id.clone(),
|
||||
});
|
||||
mutations.push(WorldMutation::ChangeIdentity {
|
||||
subject,
|
||||
operation: IdentityMutationOperation::Adopt,
|
||||
handle_id: format!("identity:canonical:{}", child.id),
|
||||
handle_value: Some(child.name.clone()),
|
||||
audience: Vec::new(),
|
||||
});
|
||||
}
|
||||
mutations.push(WorldMutation::ChangePopulationLineage {
|
||||
operation: PopulationLineageOperation::Split,
|
||||
parent_populations: vec![parent.clone()],
|
||||
child_populations: preview
|
||||
.children
|
||||
.iter()
|
||||
.map(|child| population_subject(&child.id))
|
||||
.collect(),
|
||||
remainder_population: Some(population_subject(&preview.residual_child_id)),
|
||||
});
|
||||
for (resource, child_id) in &preview.resource_child_assignments {
|
||||
mutations.push(WorldMutation::TransferCustody {
|
||||
resource: resource_subject(campaign, resource),
|
||||
from_custodian: parent.clone(),
|
||||
to_custodian: population_subject(child_id),
|
||||
});
|
||||
}
|
||||
for member in campaign
|
||||
.gestalt_members
|
||||
.values()
|
||||
.filter(|member| member.gestalt_id == preview.parent_gestalt_id)
|
||||
{
|
||||
let destination = preview
|
||||
.member_child_assignments
|
||||
.get(&member.id)
|
||||
.unwrap_or(&preview.residual_child_id);
|
||||
mutations.push(WorldMutation::ChangePopulationMembership {
|
||||
actor: actor_subject(&format!("member:{}", member.id)),
|
||||
operation: PopulationMembershipOperation::Transfer,
|
||||
source_population: Some(parent.clone()),
|
||||
destination_population: Some(population_subject(destination)),
|
||||
});
|
||||
}
|
||||
lower_exact_mutations(
|
||||
campaign,
|
||||
mutations,
|
||||
MutationProcedure::CompilerAdmission,
|
||||
None,
|
||||
MutationOutcomeBinding::Deterministic,
|
||||
Some(campaign.resolution_policy.resolution_epoch),
|
||||
"Admit the approved child populations, preserve their inherited baseline, partition each exact resource once, transfer each named member once, and record one population lineage split.",
|
||||
&source_receipt_id,
|
||||
Some(digest_serializable(&(
|
||||
"fission",
|
||||
&preview.parent_gestalt_id,
|
||||
&preview.partition_axis,
|
||||
))?),
|
||||
Some(preview_digest),
|
||||
expires_at,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn apply_lowered_fission(
|
||||
campaign: &mut Campaign,
|
||||
preview: &GestaltFissionPreview,
|
||||
transition: &LoweredLegacyTransition,
|
||||
now: DateTime<Utc>,
|
||||
) -> Result<WorldMutationReceipt> {
|
||||
crate::resolution::validate_fission(campaign, preview)?;
|
||||
let snapshot = component_snapshot(campaign)?;
|
||||
let application =
|
||||
apply_component_world_batch(&snapshot, &transition.authority, &transition.batch, now)?;
|
||||
project_accepted_fission(campaign, preview, &application.state)?;
|
||||
Ok(application.receipt)
|
||||
}
|
||||
|
||||
fn project_accepted_fission(
|
||||
campaign: &mut Campaign,
|
||||
preview: &GestaltFissionPreview,
|
||||
next: &ComponentWorldState,
|
||||
) -> Result<()> {
|
||||
let parent = campaign
|
||||
.gestalts
|
||||
.get(&preview.parent_gestalt_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow!("accepted fission parent vanished"))?;
|
||||
let inherited_relations = campaign
|
||||
.agency_relations
|
||||
.values()
|
||||
.filter(|relation| {
|
||||
relation.active
|
||||
&& (relation.from_subject_id == preview.parent_gestalt_id
|
||||
|| relation.to_subject_id == preview.parent_gestalt_id)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for approved in &preview.children {
|
||||
let subject = population_subject(&approved.id);
|
||||
let place = next
|
||||
.occupancy
|
||||
.get(&subject)
|
||||
.filter(|place| place.kind == SubjectKind::Place)
|
||||
.ok_or_else(|| anyhow!("accepted fission child lacks exact occupancy"))?;
|
||||
let identity = next
|
||||
.identities
|
||||
.get(&format!("identity:canonical:{}", approved.id))
|
||||
.filter(|identity| identity.subject == subject && identity.active)
|
||||
.ok_or_else(|| anyhow!("accepted fission child lacks exact identity"))?;
|
||||
campaign.gestalts.insert(
|
||||
approved.id.clone(),
|
||||
GestaltPersonaState {
|
||||
schema: "ghostlight.gestalt_persona_state.v1".into(),
|
||||
id: approved.id.clone(),
|
||||
name: identity.value.clone(),
|
||||
version: 0,
|
||||
home_location_id: place.id.clone(),
|
||||
shared_capabilities: parent.shared_capabilities.clone(),
|
||||
shared_knowledge: parent.shared_knowledge.clone(),
|
||||
resources: BTreeSet::new(),
|
||||
goals: parent.goals.clone(),
|
||||
pressures: parent.pressures.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let member_ids = campaign
|
||||
.gestalt_members
|
||||
.values()
|
||||
.filter(|member| member.gestalt_id == preview.parent_gestalt_id)
|
||||
.map(|member| member.id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
for member_id in member_ids {
|
||||
let expected_destination = preview
|
||||
.member_child_assignments
|
||||
.get(&member_id)
|
||||
.unwrap_or(&preview.residual_child_id);
|
||||
let actor = actor_subject(&format!("member:{member_id}"));
|
||||
let active_destinations = next
|
||||
.memberships
|
||||
.iter()
|
||||
.filter(|(key, value)| key.actor == actor && value.active)
|
||||
.map(|(key, _)| key.population.id.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
if active_destinations != [expected_destination.as_str()] {
|
||||
return Err(anyhow!(
|
||||
"accepted fission member does not have one exact destination"
|
||||
));
|
||||
}
|
||||
project_member_population_transfer(
|
||||
campaign,
|
||||
&member_id,
|
||||
&preview.parent_gestalt_id,
|
||||
expected_destination,
|
||||
)?;
|
||||
let member = campaign
|
||||
.gestalt_members
|
||||
.get_mut(&member_id)
|
||||
.expect("member was projected");
|
||||
member.version = member.version.saturating_add(1);
|
||||
}
|
||||
|
||||
project_all_resources(campaign, next)?;
|
||||
for child in &preview.children {
|
||||
if campaign.gestalts[&child.id].resources != child.resources {
|
||||
return Err(anyhow!(
|
||||
"accepted fission resource custody does not match the approved partition"
|
||||
));
|
||||
}
|
||||
}
|
||||
campaign
|
||||
.gestalts
|
||||
.get_mut(&preview.parent_gestalt_id)
|
||||
.expect("parent existence checked")
|
||||
.version = parent.version.saturating_add(1);
|
||||
|
||||
for relation in inherited_relations {
|
||||
for child in &preview.children {
|
||||
let id = format!("{}:fission:{}", relation.id, child.id);
|
||||
let component = next
|
||||
.relationships
|
||||
.get(&id)
|
||||
.ok_or_else(|| anyhow!("accepted fission lost inherited agency relation"))?;
|
||||
let mut inherited = relation.clone();
|
||||
inherited.id = id.clone();
|
||||
if inherited.from_subject_id == preview.parent_gestalt_id {
|
||||
inherited.from_subject_id = child.id.clone();
|
||||
}
|
||||
if inherited.to_subject_id == preview.parent_gestalt_id {
|
||||
inherited.to_subject_id = child.id.clone();
|
||||
}
|
||||
if component.source.id != inherited.from_subject_id
|
||||
|| component.target.id != inherited.to_subject_id
|
||||
|| component.strength != Some(i64::from(inherited.strength))
|
||||
{
|
||||
return Err(anyhow!("accepted fission agency relation was rewritten"));
|
||||
}
|
||||
campaign.agency_relations.insert(id, inherited);
|
||||
}
|
||||
}
|
||||
campaign.gestalt_lineages.insert(
|
||||
preview.parent_gestalt_id.clone(),
|
||||
GestaltLineage {
|
||||
schema: "ghostlight.gestalt_lineage.v1".into(),
|
||||
parent_gestalt_id: preview.parent_gestalt_id.clone(),
|
||||
child_gestalt_ids: preview
|
||||
.children
|
||||
.iter()
|
||||
.map(|child| child.id.clone())
|
||||
.collect(),
|
||||
partition_axis: preview.partition_axis.clone(),
|
||||
partition_values: preview.child_partition_values.clone(),
|
||||
residual_child_id: preview.residual_child_id.clone(),
|
||||
source_revision: campaign.revision,
|
||||
},
|
||||
);
|
||||
crate::resolution::project_fission_resolution(campaign, preview)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn lower_exact_mutations(
|
||||
campaign: &Campaign,
|
||||
|
|
|
|||
|
|
@ -646,15 +646,20 @@ impl CampaignStore {
|
|||
Ok(next_campaign_row)
|
||||
}
|
||||
|
||||
pub fn append_world_commit<T: Serialize, R: Serialize>(
|
||||
pub fn append_world_commit<T: Serialize>(
|
||||
&self,
|
||||
expected: &CultCacheEnvelope,
|
||||
next: &T,
|
||||
receipt_key: &str,
|
||||
receipt: &R,
|
||||
receipt: &WorldCommitReceipt,
|
||||
evidence: &[VaultEvidenceReceipt],
|
||||
candidates: &[crate::domain::CanonCandidate],
|
||||
model_receipts: &[crate::model::ModelStageReceipt],
|
||||
mutation: Option<(
|
||||
&MutationAuthorityEnvelope,
|
||||
&WorldMutationBatch,
|
||||
&WorldMutationReceipt,
|
||||
)>,
|
||||
) -> Result<CultCacheEnvelope> {
|
||||
let next_row = envelope(
|
||||
&expected.r#type,
|
||||
|
|
@ -704,6 +709,9 @@ impl CampaignStore {
|
|||
item,
|
||||
)?);
|
||||
}
|
||||
if let Some(mutation) = mutation {
|
||||
append_mutation_proof(&mut rows, receipt, mutation)?;
|
||||
}
|
||||
let mut expected_rows = vec![expected.clone()];
|
||||
if let Some((row, _)) = existing_manifest {
|
||||
expected_rows.push(row);
|
||||
|
|
|
|||
|
|
@ -2694,13 +2694,13 @@ fn is_sha256(value: &str) -> bool {
|
|||
.is_some_and(|hex| hex.len() == 64 && hex.bytes().all(|byte| byte.is_ascii_hexdigit()))
|
||||
}
|
||||
|
||||
pub fn apply_fission(campaign: &mut Campaign, preview: &GestaltFissionPreview) -> Result<()> {
|
||||
validate_fission(campaign, preview)?;
|
||||
let parent = campaign
|
||||
.gestalts
|
||||
.get_mut(&preview.parent_gestalt_id)
|
||||
.ok_or_else(|| anyhow!("fission parent vanished"))?;
|
||||
parent.version = parent.version.saturating_add(1);
|
||||
/// Project the resolution-only consequences of an already accepted population
|
||||
/// split. Canonical populations, memberships, lineage, and resource custody
|
||||
/// must already have been committed by the world mutation reducer.
|
||||
pub(crate) fn project_fission_resolution(
|
||||
campaign: &mut Campaign,
|
||||
preview: &GestaltFissionPreview,
|
||||
) -> Result<()> {
|
||||
let parent_profile = campaign
|
||||
.agency_profiles
|
||||
.get_mut(&preview.parent_gestalt_id)
|
||||
|
|
@ -2732,59 +2732,6 @@ pub fn apply_fission(campaign: &mut Campaign, preview: &GestaltFissionPreview) -
|
|||
}
|
||||
campaign.agency_profiles.insert(child.id.clone(), profile);
|
||||
}
|
||||
for member in campaign
|
||||
.gestalt_members
|
||||
.values_mut()
|
||||
.filter(|member| member.gestalt_id == preview.parent_gestalt_id)
|
||||
{
|
||||
member.gestalt_id = preview
|
||||
.member_child_assignments
|
||||
.get(&member.id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| preview.residual_child_id.clone());
|
||||
member.version = member.version.saturating_add(1);
|
||||
}
|
||||
let inherited_relations: Vec<_> = campaign
|
||||
.agency_relations
|
||||
.values()
|
||||
.filter(|relation| {
|
||||
relation.active
|
||||
&& (relation.from_subject_id == preview.parent_gestalt_id
|
||||
|| relation.to_subject_id == preview.parent_gestalt_id)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
for relation in inherited_relations {
|
||||
for child in &preview.children {
|
||||
let mut inherited_relation = relation.clone();
|
||||
inherited_relation.id = format!("{}:fission:{}", relation.id, child.id);
|
||||
if inherited_relation.from_subject_id == preview.parent_gestalt_id {
|
||||
inherited_relation.from_subject_id = child.id.clone();
|
||||
}
|
||||
if inherited_relation.to_subject_id == preview.parent_gestalt_id {
|
||||
inherited_relation.to_subject_id = child.id.clone();
|
||||
}
|
||||
campaign
|
||||
.agency_relations
|
||||
.insert(inherited_relation.id.clone(), inherited_relation);
|
||||
}
|
||||
}
|
||||
campaign.gestalt_lineages.insert(
|
||||
preview.parent_gestalt_id.clone(),
|
||||
GestaltLineage {
|
||||
schema: "ghostlight.gestalt_lineage.v1".into(),
|
||||
parent_gestalt_id: preview.parent_gestalt_id.clone(),
|
||||
child_gestalt_ids: preview
|
||||
.children
|
||||
.iter()
|
||||
.map(|child| child.id.clone())
|
||||
.collect(),
|
||||
partition_axis: preview.partition_axis.clone(),
|
||||
partition_values: preview.child_partition_values.clone(),
|
||||
residual_child_id: preview.residual_child_id.clone(),
|
||||
source_revision: campaign.revision,
|
||||
},
|
||||
);
|
||||
campaign.resolution_cover = None;
|
||||
campaign.resolution_policy.resolution_epoch = campaign
|
||||
.resolution_policy
|
||||
|
|
@ -2812,6 +2759,26 @@ pub fn validate_fission(campaign: &Campaign, preview: &GestaltFissionPreview) ->
|
|||
.child_partition_values
|
||||
.get(&preview.residual_child_id)
|
||||
.map(|value| value.trim().to_ascii_lowercase());
|
||||
let assigned_resources = preview
|
||||
.resource_child_assignments
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect::<BTreeSet<_>>();
|
||||
let child_resources = preview
|
||||
.children
|
||||
.iter()
|
||||
.flat_map(|child| {
|
||||
child
|
||||
.resources
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(move |resource| (resource, child.id.clone()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let unique_child_resources = child_resources
|
||||
.iter()
|
||||
.map(|(resource, _)| resource.clone())
|
||||
.collect::<BTreeSet<_>>();
|
||||
if preview.campaign_id != campaign.id
|
||||
|| preview.expected_world_revision != campaign.revision
|
||||
|| !preview.requires_approval
|
||||
|
|
@ -2828,6 +2795,10 @@ pub fn validate_fission(campaign: &Campaign, preview: &GestaltFissionPreview) ->
|
|||
|| !campaign.locations.contains_key(&child.home_location_id)
|
||||
|| (preview.partition_axis != AgencyAxis::Geography
|
||||
&& child.home_location_id != parent.home_location_id)
|
||||
|| child.shared_capabilities != parent.shared_capabilities
|
||||
|| child.shared_knowledge != parent.shared_knowledge
|
||||
|| child.goals != parent.goals
|
||||
|| child.pressures != parent.pressures
|
||||
|| !preview.child_partition_values.contains_key(&child.id)
|
||||
})
|
||||
|| preview
|
||||
|
|
@ -2840,6 +2811,22 @@ pub fn validate_fission(campaign: &Campaign, preview: &GestaltFissionPreview) ->
|
|||
.is_none_or(|value| value.gestalt_id != parent.id)
|
||||
|| !child_ids.contains(child.as_str())
|
||||
})
|
||||
|| campaign
|
||||
.gestalt_members
|
||||
.values()
|
||||
.any(|member| member.gestalt_id == parent.id && member.materialized_actor_id.is_some())
|
||||
|| assigned_resources != parent.resources
|
||||
|| unique_child_resources != parent.resources
|
||||
|| child_resources.len() != unique_child_resources.len()
|
||||
|| preview
|
||||
.resource_child_assignments
|
||||
.iter()
|
||||
.any(|(resource, child)| {
|
||||
!child_ids.contains(child.as_str())
|
||||
|| !preview.children.iter().any(|candidate| {
|
||||
candidate.id == *child && candidate.resources.contains(resource)
|
||||
})
|
||||
})
|
||||
|| parent_profile.subject_kind != AgencySubjectKind::Gestalt
|
||||
{
|
||||
return Err(anyhow!("gestalt fission preview is stale or malformed"));
|
||||
|
|
@ -3777,36 +3764,58 @@ pub(crate) mod tests {
|
|||
home_location_id: "center".into(),
|
||||
shared_capabilities: BTreeSet::from(["farm".into()]),
|
||||
shared_knowledge: BTreeSet::from(["local roads".into()]),
|
||||
resources: BTreeSet::from(["granary".into()]),
|
||||
resources: BTreeSet::new(),
|
||||
goals: vec!["survive winter".into()],
|
||||
pressures: vec![],
|
||||
};
|
||||
let mut other = child("other", "Other villagers");
|
||||
other.resources.insert("granary".into());
|
||||
let preview = GestaltFissionPreview {
|
||||
schema: "ghostlight.gestalt_fission_preview.v1".into(),
|
||||
campaign_id: value.id,
|
||||
expected_world_revision: value.revision,
|
||||
parent_gestalt_id: "villagers".into(),
|
||||
partition_axis: AgencyAxis::Ideology,
|
||||
children: vec![
|
||||
child("traditionalists", "Traditionalists"),
|
||||
child("other", "Other villagers"),
|
||||
],
|
||||
children: vec![child("traditionalists", "Traditionalists"), other],
|
||||
child_partition_values: BTreeMap::from([
|
||||
("traditionalists".into(), "traditionalist".into()),
|
||||
("other".into(), "other/unknown".into()),
|
||||
]),
|
||||
residual_child_id: "other".into(),
|
||||
member_child_assignments: BTreeMap::new(),
|
||||
resource_child_assignments: BTreeMap::from([("granary".into(), "other".into())]),
|
||||
evidence_receipt_ids: vec![],
|
||||
gaps: vec![],
|
||||
canon_candidates: vec![],
|
||||
requires_approval: true,
|
||||
};
|
||||
apply_fission(&mut value, &preview).unwrap();
|
||||
let mut duplicated = preview.clone();
|
||||
duplicated.children[0].resources.insert("granary".into());
|
||||
assert!(validate_fission(&value, &duplicated).is_err());
|
||||
let transition = crate::legacy_transition::lower_fission(
|
||||
&value,
|
||||
&preview,
|
||||
Utc::now() + chrono::Duration::minutes(5),
|
||||
)
|
||||
.unwrap();
|
||||
crate::legacy_transition::apply_lowered_fission(
|
||||
&mut value,
|
||||
&preview,
|
||||
&transition,
|
||||
Utc::now(),
|
||||
)
|
||||
.unwrap();
|
||||
value.revision += 1;
|
||||
assert!(value.gestalts.contains_key("villagers"));
|
||||
assert!(!value.agency_profiles["villagers"].active_leaf);
|
||||
assert!(value.agency_profiles["traditionalists"].active_leaf);
|
||||
assert!(value.agency_profiles["other"].active_leaf);
|
||||
assert!(value.gestalts["villagers"].resources.is_empty());
|
||||
assert!(value.gestalts["traditionalists"].resources.is_empty());
|
||||
assert_eq!(
|
||||
value.gestalts["other"].resources,
|
||||
BTreeSet::from(["granary".into()])
|
||||
);
|
||||
assert_eq!(value.gestalt_members["john"].gestalt_id, "other");
|
||||
assert_eq!(value.gestalt_members["john"].version, 4);
|
||||
assert!(
|
||||
|
|
@ -3819,6 +3828,8 @@ pub(crate) mod tests {
|
|||
"other"
|
||||
);
|
||||
|
||||
let mut nested_residual = child("other-unknown", "Other unclassified villagers");
|
||||
nested_residual.resources.insert("granary".into());
|
||||
let nested = GestaltFissionPreview {
|
||||
schema: "ghostlight.gestalt_fission_preview.v1".into(),
|
||||
campaign_id: value.id,
|
||||
|
|
@ -3827,7 +3838,7 @@ pub(crate) mod tests {
|
|||
partition_axis: AgencyAxis::EconomyRole,
|
||||
children: vec![
|
||||
child("other-smiths", "Smiths among the other villagers"),
|
||||
child("other-unknown", "Other unclassified villagers"),
|
||||
nested_residual,
|
||||
],
|
||||
child_partition_values: BTreeMap::from([
|
||||
("other-smiths".into(), "smith".into()),
|
||||
|
|
@ -3835,14 +3846,36 @@ pub(crate) mod tests {
|
|||
]),
|
||||
residual_child_id: "other-unknown".into(),
|
||||
member_child_assignments: BTreeMap::from([("john".into(), "other-smiths".into())]),
|
||||
resource_child_assignments: BTreeMap::from([(
|
||||
"granary".into(),
|
||||
"other-unknown".into(),
|
||||
)]),
|
||||
evidence_receipt_ids: vec![],
|
||||
gaps: vec![],
|
||||
canon_candidates: vec![],
|
||||
requires_approval: true,
|
||||
};
|
||||
apply_fission(&mut value, &nested).unwrap();
|
||||
let transition = crate::legacy_transition::lower_fission(
|
||||
&value,
|
||||
&nested,
|
||||
Utc::now() + chrono::Duration::minutes(5),
|
||||
)
|
||||
.unwrap();
|
||||
crate::legacy_transition::apply_lowered_fission(
|
||||
&mut value,
|
||||
&nested,
|
||||
&transition,
|
||||
Utc::now(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(!value.agency_profiles["other"].active_leaf);
|
||||
assert!(value.agency_profiles["other-smiths"].active_leaf);
|
||||
assert!(value.gestalts["other"].resources.is_empty());
|
||||
assert!(value.gestalts["other-smiths"].resources.is_empty());
|
||||
assert_eq!(
|
||||
value.gestalts["other-unknown"].resources,
|
||||
BTreeSet::from(["granary".into()])
|
||||
);
|
||||
assert_eq!(value.gestalt_members["john"].gestalt_id, "other-smiths");
|
||||
assert_eq!(value.gestalt_members["john"].version, 5);
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -216,6 +216,7 @@ pub enum MutationSubjectRole {
|
|||
TopologyOrigin,
|
||||
TopologyDestination,
|
||||
Entity,
|
||||
InitialPlace,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
|
@ -548,6 +549,8 @@ pub enum WorldMutation {
|
|||
AdmitEntity {
|
||||
subject: SubjectRef,
|
||||
initial_components: BTreeSet<WorldComponentKind>,
|
||||
#[serde(default)]
|
||||
initial_place: Option<SubjectRef>,
|
||||
admission_receipt_id: String,
|
||||
},
|
||||
RetireEntity {
|
||||
|
|
@ -1246,6 +1249,7 @@ fn apply_component_mutation(
|
|||
AdmitEntity {
|
||||
subject,
|
||||
initial_components,
|
||||
initial_place,
|
||||
..
|
||||
} => {
|
||||
if subject.kind == SubjectKind::Resource {
|
||||
|
|
@ -1264,6 +1268,11 @@ fn apply_component_mutation(
|
|||
version: next_component_version(state),
|
||||
},
|
||||
);
|
||||
if let Some(place) = initial_place {
|
||||
active_subject(state, place)?;
|
||||
require_kind(place, SubjectKind::Place)?;
|
||||
state.occupancy.insert(subject.clone(), place.clone());
|
||||
}
|
||||
}
|
||||
RetireEntity { subject, .. } => retire_entity(state, subject)?,
|
||||
AdvanceWorldTime { campaign, minutes } => {
|
||||
|
|
@ -2086,6 +2095,7 @@ fn apply_lineage_mutation(
|
|||
if !child_set.contains(remainder) {
|
||||
return Err(anyhow!("population split remainder is not a child"));
|
||||
}
|
||||
inherit_population_baseline(state, &parents[0], children)?;
|
||||
}
|
||||
PopulationLineageOperation::Merge => {
|
||||
if parents.len() < 2 || children.len() != 1 || remainder.is_some() {
|
||||
|
|
@ -2112,6 +2122,122 @@ fn apply_lineage_mutation(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn inherit_population_baseline(
|
||||
state: &mut ComponentWorldState,
|
||||
parent: &SubjectRef,
|
||||
children: &[SubjectRef],
|
||||
) -> Result<()> {
|
||||
let version = next_component_version(state);
|
||||
let capabilities = state
|
||||
.capabilities
|
||||
.iter()
|
||||
.filter(|(key, _)| key.subject == *parent)
|
||||
.map(|(key, value)| (key.entry_id.clone(), value.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
let commitments = state
|
||||
.commitments
|
||||
.iter()
|
||||
.filter(|(key, _)| key.subject == *parent)
|
||||
.map(|(key, value)| (key.entry_id.clone(), value.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
let knowledge = state
|
||||
.knowledge
|
||||
.iter()
|
||||
.filter(|(key, _)| key.knower == *parent)
|
||||
.map(|(key, value)| (key.proposition.clone(), value.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
let pressures = state
|
||||
.pressures
|
||||
.iter()
|
||||
.filter(|(_, value)| value.owner == *parent)
|
||||
.map(|(subject, value)| (subject.clone(), value.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
let relationships = state
|
||||
.relationships
|
||||
.iter()
|
||||
.filter(|(_, value)| value.source == *parent || value.target == *parent)
|
||||
.map(|(id, value)| (id.clone(), value.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for child in children {
|
||||
for (entry_id, mut value) in capabilities.clone() {
|
||||
value.version = version;
|
||||
state.capabilities.insert(
|
||||
SubjectEntryKey {
|
||||
subject: child.clone(),
|
||||
entry_id,
|
||||
},
|
||||
value,
|
||||
);
|
||||
}
|
||||
for (entry_id, mut value) in commitments.clone() {
|
||||
value.version = version;
|
||||
state.commitments.insert(
|
||||
SubjectEntryKey {
|
||||
subject: child.clone(),
|
||||
entry_id,
|
||||
},
|
||||
value,
|
||||
);
|
||||
}
|
||||
for (proposition, mut value) in knowledge.clone() {
|
||||
value.version = version;
|
||||
state.knowledge.insert(
|
||||
KnowledgeKey {
|
||||
knower: child.clone(),
|
||||
proposition,
|
||||
},
|
||||
value,
|
||||
);
|
||||
}
|
||||
for (_source_pressure, mut value) in pressures.clone() {
|
||||
let pressure = SubjectRef {
|
||||
kind: SubjectKind::Pressure,
|
||||
id: format!("gestalt-pressure:{}:{}", child.id, short_hash(&value.label)),
|
||||
};
|
||||
if state.subjects.contains_key(&pressure) || state.pressures.contains_key(&pressure) {
|
||||
return Err(anyhow!("population split pressure identity collides"));
|
||||
}
|
||||
state.subjects.insert(
|
||||
pressure.clone(),
|
||||
TypedSubject {
|
||||
schema: "ghostlight.typed_subject.v1".into(),
|
||||
subject: pressure.clone(),
|
||||
lifecycle: LifecycleStatus::Admitted,
|
||||
admitted_components: BTreeSet::from([WorldComponentKind::Pressure]),
|
||||
version,
|
||||
},
|
||||
);
|
||||
value.pressure = pressure.clone();
|
||||
value.owner = child.clone();
|
||||
value.version = version;
|
||||
state.pressures.insert(pressure, value);
|
||||
}
|
||||
for (source_id, mut value) in relationships.clone() {
|
||||
let id = format!("{source_id}:fission:{}", child.id);
|
||||
if state.relationships.contains_key(&id) {
|
||||
return Err(anyhow!("population split relationship identity collides"));
|
||||
}
|
||||
if value.source == *parent {
|
||||
value.source = child.clone();
|
||||
}
|
||||
if value.target == *parent {
|
||||
value.target = child.clone();
|
||||
}
|
||||
value.version = version;
|
||||
state.relationships.insert(id, value);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn short_hash(value: &str) -> String {
|
||||
format!("{:x}", Sha256::digest(value.as_bytes()))
|
||||
.chars()
|
||||
.take(16)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn apply_identity_mutation(
|
||||
state: &mut ComponentWorldState,
|
||||
subject: &SubjectRef,
|
||||
|
|
@ -2517,9 +2643,18 @@ fn mutation_component_refs(
|
|||
Component::Topology,
|
||||
Some(edge_id.clone()),
|
||||
)],
|
||||
AdmitEntity { subject, .. } | RetireEntity { subject, .. } => {
|
||||
vec![(subject.clone(), Component::Lifecycle, None)]
|
||||
}
|
||||
AdmitEntity {
|
||||
subject,
|
||||
initial_place,
|
||||
..
|
||||
} => std::iter::once((subject.clone(), Component::Lifecycle, None))
|
||||
.chain(
|
||||
initial_place
|
||||
.iter()
|
||||
.map(|_| (subject.clone(), Component::Occupancy, None)),
|
||||
)
|
||||
.collect(),
|
||||
RetireEntity { subject, .. } => vec![(subject.clone(), Component::Lifecycle, None)],
|
||||
AdvanceWorldTime { campaign, .. } => vec![(campaign.clone(), Component::WorldTime, None)],
|
||||
}
|
||||
}
|
||||
|
|
@ -2842,7 +2977,11 @@ fn subject_schema_fields(
|
|||
(TopologyOrigin, "from_place", Required),
|
||||
(TopologyDestination, "to_place", Required),
|
||||
],
|
||||
AdmitEntity | RetireEntity => vec![(Entity, "subject", Required)],
|
||||
AdmitEntity => vec![
|
||||
(Entity, "subject", Required),
|
||||
(InitialPlace, "initial_place", Optional),
|
||||
],
|
||||
RetireEntity => vec![(Entity, "subject", Required)],
|
||||
AdvanceWorldTime => vec![(Subject, "campaign", Required)],
|
||||
}
|
||||
}
|
||||
|
|
@ -3310,9 +3449,17 @@ impl WorldMutation {
|
|||
add(MutationSubjectRole::TopologyOrigin, from_place);
|
||||
add(MutationSubjectRole::TopologyDestination, to_place);
|
||||
}
|
||||
AdmitEntity { subject, .. } | RetireEntity { subject, .. } => {
|
||||
add(MutationSubjectRole::Entity, subject)
|
||||
AdmitEntity {
|
||||
subject,
|
||||
initial_place,
|
||||
..
|
||||
} => {
|
||||
add(MutationSubjectRole::Entity, subject);
|
||||
if let Some(place) = initial_place {
|
||||
add(MutationSubjectRole::InitialPlace, place);
|
||||
}
|
||||
}
|
||||
RetireEntity { subject, .. } => add(MutationSubjectRole::Entity, subject),
|
||||
AdvanceWorldTime { campaign, .. } => add(MutationSubjectRole::Subject, campaign),
|
||||
}
|
||||
roles
|
||||
|
|
@ -3692,12 +3839,23 @@ impl WorldMutation {
|
|||
}
|
||||
AdmitEntity {
|
||||
initial_components,
|
||||
initial_place,
|
||||
admission_receipt_id,
|
||||
..
|
||||
} => {
|
||||
if initial_components.is_empty() {
|
||||
return Err(anyhow!("entity admission requires initial components"));
|
||||
}
|
||||
if initial_components.contains(&WorldComponentKind::Occupancy)
|
||||
!= initial_place.is_some()
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"entity admission occupancy requires one exact initial place"
|
||||
));
|
||||
}
|
||||
if let Some(place) = initial_place {
|
||||
require_kind(place, SubjectKind::Place)?;
|
||||
}
|
||||
nonempty("admission receipt id", admission_receipt_id)?;
|
||||
}
|
||||
RetireEntity { reason, .. } => nonempty("retirement reason", reason)?,
|
||||
|
|
|
|||
|
|
@ -47,11 +47,20 @@ leaves. A material split uses approval-gated `FissionGestalt`:
|
|||
2. every requested enumerated facet gets a child and one `other/unknown`
|
||||
remainder is mandatory;
|
||||
3. children inherit the parent baseline and own only later deltas;
|
||||
4. member deltas are assigned to one child without rewriting identity;
|
||||
5. the parent remains as inactive lineage rather than being destroyed;
|
||||
6. the kernel validates the preview, evidence, versions, assignments, and
|
||||
4. every exact scarce resource is assigned to one child rather than copied as
|
||||
an inheritable trait;
|
||||
5. member deltas are assigned to one child without rewriting identity;
|
||||
6. the parent remains as inactive lineage rather than being destroyed;
|
||||
7. the kernel validates the preview, evidence, versions, assignments, and
|
||||
residual child before one atomic commit.
|
||||
|
||||
The commit is a canonical `WorldMutationBatch`: child admission establishes
|
||||
identity and occupancy, the lineage mutation inherits non-scarce baseline
|
||||
components, custody transfers partition resources, and membership transfers
|
||||
move exact named people. Agency profiles, inherited partition facets, and the
|
||||
discarded cover are resolution projections of that accepted transition; they
|
||||
cannot create a second result.
|
||||
|
||||
Reversible simulation aggregation never invokes fission and never deletes or
|
||||
reverses this lineage.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
# Ghostlight World Transition Algebra
|
||||
|
||||
Status: adopted architecture; production migration is incomplete until the
|
||||
legacy effect writers named below are deleted.
|
||||
Status: adopted architecture; foreground, reaction, strategic, time, travel,
|
||||
and approval-gated fission writes use the mutation reducer. Region expansion,
|
||||
initial compiler publication, and aggregate-storage removal remain migration
|
||||
work.
|
||||
|
||||
## Objective
|
||||
|
||||
|
|
@ -97,9 +99,12 @@ The migration is not complete while any of these can decide canonical state:
|
|||
- model output, browser payloads, narration, news generation, retrieval, or a
|
||||
derived simulation cell.
|
||||
|
||||
Administrative world creation, compiler publication, and approval-gated
|
||||
population fission must ultimately lower to the same batch primitive. They may
|
||||
possess broader authority envelopes, but not another state-writing mechanism.
|
||||
Administrative world creation, compiler publication, and region expansion must
|
||||
ultimately lower to the same batch primitive. They may possess broader
|
||||
authority envelopes, but not another state-writing mechanism. Approval-gated
|
||||
population fission already lowers to entity admission, lineage, custody, and
|
||||
membership mutations; the fission projector owns only the derived resolution
|
||||
profiles and cover epoch.
|
||||
|
||||
### Shared paths
|
||||
|
||||
|
|
@ -217,8 +222,12 @@ Complex effects are batches, not new verbs. Giving half a medicine lot to a
|
|||
clinic composes resource split, custody transfer, and perhaps an obligation.
|
||||
Evacuating a named refugee composes relocation, population-membership transfer,
|
||||
and accessible knowledge. Population fission composes entity admission,
|
||||
lineage, and exact member transfers while preserving an `other/unknown`
|
||||
remainder.
|
||||
lineage, exact member transfers, and one custody transfer per scarce resource
|
||||
while preserving an `other/unknown` remainder. Capabilities, shared knowledge,
|
||||
goals, and active pressures inherit through the lineage reducer. Resources do
|
||||
not: every parent resource must be assigned to exactly one child in the
|
||||
approved preview, so aggregation cannot mint a granary for every ideological
|
||||
subgroup.
|
||||
|
||||
## Means, intended effect, and committed mutation
|
||||
|
||||
|
|
@ -385,7 +394,8 @@ Migration order:
|
|||
5. Normalize named Gestalt members into stable actor subjects and resource,
|
||||
membership, identity, and occupancy components. Remove materialized actor as
|
||||
a second identity owner.
|
||||
6. Lower travel, waits, fission, expansion, and compiler publication.
|
||||
6. Lower travel, waits, fission, expansion, and compiler publication. Travel,
|
||||
waits, and fission are complete; expansion and initial publication remain.
|
||||
7. Migrate persisted campaign rows transactionally, quarantine ambiguous legacy
|
||||
resources or identities, and make the old campaign fields projection-only.
|
||||
8. Remove legacy schemas from model boundaries. Retain schema readers solely
|
||||
|
|
@ -478,4 +488,3 @@ The algebra cut is complete only when:
|
|||
- the agency corpus meets its coverage and stability gates;
|
||||
- adversarial play through the typed Eve surface still proves the homepage
|
||||
promises after migration.
|
||||
|
||||
|
|
|
|||
|
|
@ -110,6 +110,18 @@ responses are spoiler-safe projections rather than serialized campaigns. The
|
|||
browser renders these projections as labeled, escaped human-readable controls,
|
||||
and narration receives only the latest causal turn rather than a route dump.
|
||||
|
||||
World transition authority is now a closed semantic mutation algebra rather
|
||||
than four empirical effect universes. Foreground attempts, NPC initiative,
|
||||
reaction appraisal, strategic actor/institution/Gestalt outcomes, waits,
|
||||
unanimous group travel, and approval-gated population fission lower through one
|
||||
revision-bound batch reducer and persist its authority, batch, and receipt with
|
||||
the aggregate campaign projection. Fission inherits non-scarce population
|
||||
baseline through typed lineage while partitioning every exact resource and
|
||||
named member once; the resolution organ only changes profiles and cover epoch.
|
||||
Region expansion, initial compiler publication, removal of legacy model effect
|
||||
schemas, and the 300-case reviewed agency corpus remain the explicit migration
|
||||
frontier. See `docs/architecture/ghostlight-transition-algebra.md`.
|
||||
|
||||
Live local evidence now covers grounded VoidBot compilation, a four-actor wave,
|
||||
impossible and receipted d20 actions, grounded narration, strategic cells, and a
|
||||
24-faction budget-4 wave. The first scale baseline used 37,327 prompt tokens and
|
||||
|
|
|
|||
Loading…
Reference in a new issue