Prove strategic emergence at hundreds-cell scale

This commit is contained in:
Meta Vens 2026-08-26 20:09:49 +02:00
commit f189fc855a
19 changed files with 855 additions and 49 deletions

View file

@ -204,6 +204,16 @@ pub struct GestaltIndividuation {
pub location_id: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct StrategicGestaltIndividuation {
pub schema: String,
/// Digest of the selected Gestalt-owned action whose pressure made this
/// individual strategically consequential.
pub action_digest: String,
pub rationale: String,
pub individuation: GestaltIndividuation,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct GestaltDemotion {
pub actor_id: String,
@ -585,6 +595,8 @@ pub struct ResolutionWaveCommit {
pub appraisals: Vec<CellAppraisal>,
#[serde(default)]
pub activity_outcomes: Vec<StrategicActivityOutcome>,
#[serde(default)]
pub strategic_individuations: Vec<StrategicGestaltIndividuation>,
pub model_receipt_hashes: Vec<String>,
}

View file

@ -1240,6 +1240,40 @@ fn execute(
));
}
}
if !wave.strategic_individuations.is_empty() {
let selected_actions = resolved_plan
.as_ref()
.map(|plan| plan.selected_actions.as_slice())
.unwrap_or_default();
let candidate_digests =
crate::scheduler::strategic_individuation_candidate_digests(
&campaign,
selected_actions,
);
for proposal in &wave.strategic_individuations {
let proposal_digest =
crate::scheduler::strategic_individuation_proposal_digest(proposal)
.map_err(|error| KernelError::Invalid(error.to_string()))?;
let binding = crate::scheduler::strategic_individuation_binding(
&campaign,
&candidate_digests,
Some(&proposal_digest),
);
if !stage_bindings
.contains(&("strategic_individuation_selector".into(), binding))
{
return Err(KernelError::Invalid(
"strategic individuation lacks a payload-bound selector receipt"
.into(),
));
}
}
}
}
if let Some(wave) = &resolution_wave {
for proposal in &wave.strategic_individuations {
apply_individuation(&mut campaign, &proposal.individuation)?;
}
}
let applied_tick = match resolved_plan.or(plan) {
Some(plan) => apply_strategic_tick_plan(&mut campaign, plan)?,
@ -1249,9 +1283,32 @@ fn execute(
}
};
let AppliedStrategicTickPlan {
events: tick_events,
events: mut tick_events,
mutation,
} = applied_tick;
if let Some(wave) = &resolution_wave {
for proposal in &wave.strategic_individuations {
let member = &proposal.individuation.member;
tick_events.push(crate::domain::Event {
id: format!(
"strategic:{}:individuation:{}",
campaign.strategic_tick_count.saturating_add(1),
crate::domain::canonical_gestalt_member_local_id(&member.id)
),
at: campaign.world_time,
kind: "gestalt_individuation".into(),
summary: format!(
"{} becomes a consequential named figure: {}",
member.name, proposal.rationale
),
actor_ids: vec![crate::domain::gestalt_member_subject_id(&member.id)],
institution_ids: vec![],
gestalt_ids: vec![proposal.individuation.gestalt_id.clone()],
location_ids: vec![proposal.individuation.location_id.clone()],
public_channels: vec![],
});
}
}
if let Some(wave) = &resolution_wave {
crate::resolution::advance_detail_debt(&mut campaign, &wave.cover);
campaign.resolution_cover = Some(wave.cover.clone());
@ -8068,6 +8125,7 @@ mod tests {
.collect(),
cover,
activity_outcomes: vec![],
strategic_individuations: vec![],
model_receipt_hashes: hashes,
}
}
@ -8617,6 +8675,181 @@ mod tests {
);
}
#[tokio::test]
async fn strategic_wave_atomically_individuates_and_materializes_an_action_bound_person() {
let dir = tempfile::tempdir().unwrap();
let store = CampaignStore::open(dir.path().join("campaign.cc")).unwrap();
let kernel = WorldKernel::start(store.clone());
let seed = hierarchical_refugee_campaign();
kernel
.command(WorldCommand::CreateCampaign {
campaign: seed.clone(),
evidence_receipts: vec![],
model_stage_receipts: vec![],
})
.await
.unwrap();
let persisted = store
.load::<Campaign>("campaign.v1", &seed.id.to_string())
.unwrap()
.unwrap()
.1;
let mut wave = inaction_wave(&persisted, &store);
let cell_id = wave
.cover
.cells
.iter()
.find(|cell| cell.subject_ids.contains("refugees-east"))
.unwrap()
.id
.clone();
let appraisal = wave
.appraisals
.iter_mut()
.find(|appraisal| appraisal.cell_id == cell_id)
.unwrap();
appraisal.considered_subject_ids = BTreeSet::from(["refugees-east".into()]);
let action = CellActionProposal {
subject_id: "refugees-east".into(),
intent: "appoint a named storm delegation broker".into(),
intended_effect: "make one person accountable for the camp negotiation".into(),
priority: 80,
state_references: vec!["subject:refugees-east".into(), "location:camp".into()],
public_channels: vec![],
effects: vec![StrategicCellEffect::Gestalt {
gestalt_id: "refugees-east".into(),
pressure_additions: vec!["the storm delegation needs an accountable broker".into()],
pressure_resolutions: vec![],
}],
};
let action_digest = crate::resolution::cell_action_digest(&action).unwrap();
appraisal.actions = vec![action.clone()];
appraisal.inactions.clear();
wave.strategic_individuations = vec![StrategicGestaltIndividuation {
schema: "ghostlight.strategic_gestalt_individuation.v1".into(),
action_digest: action_digest.clone(),
rationale: "The camp needs one accountable storm broker.".into(),
individuation: GestaltIndividuation {
gestalt_id: "refugees-east".into(),
expected_gestalt_version: 0,
location_id: "camp".into(),
member: GestaltMemberDelta {
schema: "ghostlight.gestalt_member_delta.v1".into(),
id: "veska-rill".into(),
gestalt_id: "refugees-east".into(),
version: 0,
name: "Veska Rill".into(),
capability_additions: BTreeSet::new(),
capability_removals: BTreeSet::new(),
knowledge_additions: BTreeSet::new(),
knowledge_removals: BTreeSet::new(),
equipment: BTreeSet::new(),
conditions: BTreeSet::new(),
obligations: BTreeSet::from(["answer to the camp wards".into()]),
relationships: BTreeMap::new(),
goals: vec!["secure the storm route".into()],
memories: vec!["the deep excavation broke the lower road".into()],
last_location_id: Some("camp".into()),
materialized_actor_id: None,
last_relevant_revision: persisted.revision,
relevance_lease_until_revision: persisted.revision + 4,
},
},
}];
let base_binding = format!(
"campaign:{}:revision:{}:resolution:{}:cell:{}",
persisted.id, persisted.revision, persisted.resolution_policy.resolution_epoch, cell_id
);
let mut verifier = resolution_stage(&cell_id, &persisted, "cell_effect_verifier", '7');
verifier.snapshot_binding = crate::persona::cell_effect_verification_binding(
&base_binding,
std::slice::from_ref(&action),
)
.unwrap();
let mut selector = resolution_stage(
&cell_id,
&persisted,
"strategic_individuation_selector",
'8',
);
selector.rebind_snapshot(crate::scheduler::strategic_individuation_binding(
&persisted,
std::slice::from_ref(&action_digest),
Some(
&crate::scheduler::strategic_individuation_proposal_digest(
&wave.strategic_individuations[0],
)
.unwrap(),
),
));
for receipt in [verifier, selector] {
store
.insert(
"persona_stage_receipt.v1",
"ghostlight.persona_stage_receipt.v1",
receipt.storage_key(),
&receipt,
)
.unwrap();
wave.model_receipt_hashes
.push(receipt.storage_key().to_owned());
}
let mut substituted = wave.clone();
substituted.strategic_individuations[0]
.individuation
.member
.name = "Substituted Caller Payload".into();
let error = kernel
.command(WorldCommand::AdvanceStrategicTick {
expected_revision: persisted.revision,
source: TickSource::Scheduler,
plan: None,
model_receipt_hash: Some(format!("sha256:{}", "c".repeat(64))),
resolution_wave: Some(substituted),
})
.await
.unwrap_err();
assert!(error.to_string().contains("payload-bound selector receipt"));
let unchanged = store
.load::<Campaign>("campaign.v1", &seed.id.to_string())
.unwrap()
.unwrap()
.1;
assert_eq!(unchanged.revision, persisted.revision);
assert!(!unchanged.gestalt_members.contains_key("veska-rill"));
let result = kernel
.command(WorldCommand::AdvanceStrategicTick {
expected_revision: persisted.revision,
source: TickSource::Scheduler,
plan: None,
model_receipt_hash: Some(format!("sha256:{}", "b".repeat(64))),
resolution_wave: Some(wave),
})
.await
.unwrap();
let CommandResult::Committed { campaign, .. } = result else {
panic!()
};
let member = &campaign.gestalt_members["veska-rill"];
assert_eq!(
member.materialized_actor_id.as_deref(),
Some("member:veska-rill")
);
assert_eq!(campaign.actors["member:veska-rill"].name, "Veska Rill");
assert!(campaign.agency_profiles["member:veska-rill"].simulation_eligible);
assert!(
campaign
.events
.iter()
.any(|event| event.kind == "gestalt_individuation")
);
let proposals = store
.load_all::<StrategicGestaltIndividuation>("strategic_gestalt_individuation.v1")
.unwrap();
assert_eq!(proposals.len(), 1);
assert_eq!(proposals[0].action_digest, action_digest);
}
#[tokio::test]
async fn stale_resolution_wave_leaves_time_clocks_and_campaign_unmutated() {
let dir = tempfile::tempdir().unwrap();

View file

@ -5546,8 +5546,13 @@ fn validate_player_http_command(command: &WorldCommand) -> Result<(), String> {
}
WorldCommand::SetResolutionBudget {
active_cell_budget, ..
} if !(1..=128).contains(active_cell_budget) => {
Err("active Persona-cell budget must be between 1 and 128".into())
} if !(1..=ghostlight_dungeon::resolution::MAX_ACTIVE_CELL_BUDGET)
.contains(active_cell_budget) =>
{
Err(format!(
"active Persona-cell budget must be between 1 and {}",
ghostlight_dungeon::resolution::MAX_ACTIVE_CELL_BUDGET
))
}
_ => Ok(()),
}

View file

@ -88,6 +88,29 @@ impl ModelStageReceipt {
}
}
/// Bind a structurally validated model output to a narrower local semantic
/// object without rerunning inference. The request and output hashes remain
/// the exact provider evidence; the content-addressed receipt changes to
/// cover the locally admitted binding.
pub fn rebind_snapshot(&mut self, snapshot_binding: String) {
self.snapshot_binding = snapshot_binding;
self.receipt_hash = format!(
"sha256:{:x}",
Sha256::digest(
format!(
"{}|{}|{}|{}|{}|{}",
self.provider,
self.model,
self.stage,
self.snapshot_binding,
self.request_hash,
self.output_hash
)
.as_bytes()
)
);
}
pub fn same_receipted_content(&self, other: &Self) -> bool {
self.storage_key() == other.storage_key()
&& self.schema == other.schema

View file

@ -943,6 +943,21 @@ impl CampaignStore {
outcome,
)?);
}
for individuation in &wave.strategic_individuations {
rows.push(envelope(
"strategic_gestalt_individuation.v1",
"ghostlight.strategic_gestalt_individuation.v1",
&format!(
"{}:{}:{}",
next.revision,
wave.resolution_epoch,
crate::domain::canonical_gestalt_member_local_id(
&individuation.individuation.member.id,
)
),
individuation,
)?);
}
}
self.append_external_proposals(&mut rows, external_proposals)?;
if let Some((authority, batch, mutation_receipt)) = mutation {

View file

@ -5,7 +5,10 @@ use std::cmp::{Ordering, Reverse};
use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, VecDeque};
pub const MIN_ACTIVE_CELL_BUDGET: u8 = 1;
pub const MAX_ACTIVE_CELL_BUDGET: u8 = 128;
/// A wave can cover a hundreds-class world while provider concurrency remains
/// independently bounded. Keep this below u8::MAX so malformed sentinel-like
/// values never become an operator setting.
pub const MAX_ACTIVE_CELL_BUDGET: u8 = 240;
pub const MAX_PROVIDER_PARALLELISM: u8 = 32;
pub(crate) fn information_channel_is_concrete(channel: &str) -> bool {
@ -2150,10 +2153,90 @@ pub fn validate_and_resolve_wave(
&selection.activity_proposals,
&wave.activity_outcomes,
)?;
validate_strategic_individuations(campaign, wave, &selection.plan.selected_actions)?;
selection.plan.activity_outcomes = wave.activity_outcomes.clone();
Ok(selection.plan)
}
fn validate_strategic_individuations(
campaign: &Campaign,
wave: &ResolutionWaveCommit,
selected_actions: &[CellActionProposal],
) -> Result<()> {
if wave.strategic_individuations.len() > 1 {
return Err(anyhow!(
"one strategic wave can admit at most one new person"
));
}
let selected = selected_actions
.iter()
.map(|action| Ok((cell_action_digest(action)?, action)))
.collect::<Result<BTreeMap<_, _>>>()?;
for proposal in &wave.strategic_individuations {
let action = selected
.get(&proposal.action_digest)
.ok_or_else(|| anyhow!("strategic individuation is not bound to a selected action"))?;
let individuation = &proposal.individuation;
let member = &individuation.member;
let profile = campaign
.agency_profiles
.get(&individuation.gestalt_id)
.ok_or_else(|| anyhow!("strategic individuation Gestalt has no agency profile"))?;
if proposal.schema != "ghostlight.strategic_gestalt_individuation.v1"
|| proposal.rationale.trim().is_empty()
|| proposal.rationale.chars().count() > 460
|| action.subject_id != individuation.gestalt_id
|| !campaign.gestalts.contains_key(&individuation.gestalt_id)
|| !profile.active_leaf
|| !profile.simulation_eligible
|| !profile.location_ids.contains(&individuation.location_id)
|| individuation.expected_gestalt_version
!= campaign.gestalts[&individuation.gestalt_id].version
|| member.schema != "ghostlight.gestalt_member_delta.v1"
|| member.gestalt_id != individuation.gestalt_id
|| member.version != 0
|| member.materialized_actor_id.is_some()
|| member.id.trim().is_empty()
|| member.id.chars().count() > 80
|| member.name.trim().is_empty()
|| member.name.chars().count() > 160
|| member.goals.len() > 8
|| member.memories.len() > 8
|| member.obligations.len() > 8
|| serde_json::to_vec(member).is_ok_and(|encoded| encoded.len() > 16_384)
|| campaign.gestalt_members.contains_key(
&crate::domain::canonical_gestalt_member_local_id(&member.id),
)
|| campaign
.actors
.contains_key(&crate::domain::gestalt_member_subject_id(&member.id))
|| campaign
.actors
.values()
.any(|actor| actor.name.trim().eq_ignore_ascii_case(member.name.trim()))
|| campaign.gestalt_members.values().any(|existing| {
existing
.name
.trim()
.eq_ignore_ascii_case(member.name.trim())
})
|| member.relationships.keys().any(|subject_id| {
!campaign.actors.contains_key(subject_id)
&& !campaign.institutions.contains_key(subject_id)
&& !campaign.gestalts.contains_key(subject_id)
&& subject_id
.strip_prefix("member:")
.is_none_or(|member_id| !campaign.gestalt_members.contains_key(member_id))
})
{
return Err(anyhow!(
"strategic individuation exceeds its exact Gestalt action authority"
));
}
}
Ok(())
}
fn validate_cell_proposal(
campaign: &Campaign,
cell: &SimulationCell,
@ -4074,6 +4157,21 @@ pub(crate) mod tests {
}
}
#[test]
fn partitions_one_thousand_subjects_into_a_two_hundred_cell_wave() {
let value = campaign(1_000, 200);
let cover = plan_cover(&value, default_demand(&value, "hundreds-class world")).unwrap();
assert_eq!(cover.configured_budget, 200);
assert_eq!(cover.effective_budget, 200);
assert_eq!(cover.cells.len(), 200);
let represented = cover
.cells
.iter()
.flat_map(|cell| cell.subject_ids.iter())
.collect::<BTreeSet<_>>();
assert_eq!(represented.len(), 1_000);
}
#[test]
fn arena_cannot_act_as_a_collective_or_borrow_a_constituents_secret() {
let mut value = campaign(2, 1);
@ -4127,6 +4225,7 @@ pub(crate) mod tests {
}],
cover: cover.clone(),
activity_outcomes: vec![],
strategic_individuations: vec![],
model_receipt_hashes: vec![],
}
};
@ -4332,6 +4431,7 @@ pub(crate) mod tests {
}],
cover: cover.clone(),
activity_outcomes: vec![],
strategic_individuations: vec![],
model_receipt_hashes: vec![],
}
};
@ -4384,6 +4484,7 @@ pub(crate) mod tests {
}],
cover: cover.clone(),
activity_outcomes: vec![],
strategic_individuations: vec![],
model_receipt_hashes: vec![],
};
assert!(select_resolution_wave(&value, &same_member_wave).is_err());
@ -4456,6 +4557,7 @@ pub(crate) mod tests {
}],
cover: cover.clone(),
activity_outcomes: vec![],
strategic_individuations: vec![],
model_receipt_hashes: vec![],
};
assert!(select_resolution_wave(&value, &same_subject_wave).is_err());
@ -4539,6 +4641,7 @@ pub(crate) mod tests {
}],
cover,
activity_outcomes: vec![],
strategic_individuations: vec![],
model_receipt_hashes: vec![],
};
let plan = validate_and_resolve_wave(&value, &wave).unwrap();

View file

@ -1,7 +1,7 @@
use crate::{
domain::{
AgencyAxis, AgencySubjectKind, Campaign, ResolutionDemand, ResolutionWaveCommit,
SimulationCell,
AgencyAxis, AgencySubjectKind, Campaign, GestaltIndividuation, GestaltMemberDelta,
ResolutionDemand, ResolutionWaveCommit, SimulationCell, StrategicGestaltIndividuation,
},
model::{MODEL_FAST, ModelPort, ModelStageOutput, ModelStageRequest, run_validated_stage},
outcome::{activity_outcome_binding, resolve_activity_outcomes},
@ -41,6 +41,24 @@ struct DemandAxisWeights {
information: f32,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
struct StrategicPersonDraft {
action_digest: String,
gestalt_id: String,
member_id: String,
name: String,
goals: Vec<String>,
obligations: BTreeSet<String>,
relationships: BTreeMap<String, String>,
memories: Vec<String>,
rationale: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
struct StrategicPersonSelection {
proposals: Vec<StrategicPersonDraft>,
}
impl DemandAxisWeights {
fn into_map(self) -> BTreeMap<AgencyAxis, f32> {
BTreeMap::from([
@ -172,6 +190,7 @@ pub async fn propose_resolution_wave_with_policy(
plan_receipt: receipt,
appraisals,
activity_outcomes: Vec::new(),
strategic_individuations: Vec::new(),
model_receipt_hashes: cell_model_receipt_hashes,
};
let selection = select_resolution_wave(campaign, &wave)?;
@ -212,6 +231,42 @@ pub async fn propose_resolution_wave_with_policy(
}
stages.extend(outcome_stages);
wave.activity_outcomes = activity_outcomes;
let individuation_candidate_digests =
strategic_individuation_candidate_digests(campaign, &selection.plan.selected_actions);
if !individuation_candidate_digests.is_empty() {
outcome_permit
.require(
"strategic-individuation",
&strategic_individuation_binding(campaign, &individuation_candidate_digests, None),
"strategic_individuation_selector",
)
.await?;
}
let (strategic_individuations, individuation_stages) = propose_strategic_individuation(
outcome_model.as_ref(),
campaign,
&selection.plan.selected_actions,
)
.await;
if !individuation_candidate_digests.is_empty() {
let proposal_digest = strategic_individuations
.first()
.map(strategic_individuation_proposal_digest)
.transpose()?;
outcome_permit
.require(
"strategic-individuation",
&strategic_individuation_binding(
campaign,
&individuation_candidate_digests,
proposal_digest.as_deref(),
),
"strategic_individuation_terminal",
)
.await?;
}
stages.extend(individuation_stages);
wave.strategic_individuations = strategic_individuations;
wave.model_receipt_hashes = distinct_model_receipt_hashes(&stages);
validate_and_resolve_wave(campaign, &wave)?;
let aggregate_receipt_hash = format!(
@ -226,6 +281,212 @@ pub async fn propose_resolution_wave_with_policy(
})
}
pub fn strategic_individuation_binding(
campaign: &Campaign,
action_digests: &[String],
proposal_digest: Option<&str>,
) -> String {
format!(
"campaign:{}:revision:{}:resolution:{}:strategic-individuation:{}:proposal:{}",
campaign.id,
campaign.revision,
campaign.resolution_policy.resolution_epoch,
action_digests.join(","),
proposal_digest.unwrap_or("none")
)
}
pub fn strategic_individuation_candidate_digests(
campaign: &Campaign,
selected_actions: &[crate::domain::CellActionProposal],
) -> Vec<String> {
selected_actions
.iter()
.filter(|action| {
campaign.gestalts.contains_key(&action.subject_id)
&& campaign
.agency_profiles
.get(&action.subject_id)
.is_some_and(|profile| {
profile.active_leaf
&& profile.simulation_eligible
&& profile.location_ids.len() == 1
})
})
.filter_map(|action| cell_action_digest(action).ok())
.collect()
}
pub fn strategic_individuation_proposal_digest(
proposal: &StrategicGestaltIndividuation,
) -> Result<String> {
Ok(format!(
"sha256:{:x}",
Sha256::digest(rmp_serde::to_vec_named(proposal)?)
))
}
async fn propose_strategic_individuation(
model: &dyn ModelPort,
campaign: &Campaign,
selected_actions: &[crate::domain::CellActionProposal],
) -> (Vec<StrategicGestaltIndividuation>, Vec<ModelStageOutput>) {
let candidates = selected_actions
.iter()
.filter_map(|action| {
let gestalt = campaign.gestalts.get(&action.subject_id)?;
let profile = campaign.agency_profiles.get(&action.subject_id)?;
(profile.active_leaf && profile.simulation_eligible && profile.location_ids.len() == 1)
.then(|| {
(
action,
gestalt,
profile.location_ids.iter().next().unwrap().clone(),
)
})
})
.filter_map(|(action, gestalt, location_id)| {
cell_action_digest(action)
.ok()
.map(|digest| (digest, action, gestalt, location_id))
})
.collect::<Vec<_>>();
if candidates.is_empty() {
return (Vec::new(), Vec::new());
}
let digests = candidates
.iter()
.map(|(digest, ..)| digest.clone())
.collect::<Vec<_>>();
let context = candidates
.iter()
.map(|(digest, action, gestalt, location_id)| {
serde_json::json!({
"action_digest":digest,
"gestalt_id":gestalt.id,
"gestalt_name":gestalt.name,
"location_id":location_id,
"goals":gestalt.goals,
"pressures":gestalt.pressures,
"selected_action":action,
})
})
.collect::<Vec<_>>();
let mut schema = match serde_json::to_value(schema_for!(StrategicPersonSelection)) {
Ok(schema) => schema,
Err(_) => return (Vec::new(), Vec::new()),
};
if let Some(value) = schema.pointer_mut("/$defs/StrategicPersonDraft/properties/action_digest")
{
*value = serde_json::json!({"type":"string","enum":digests});
}
if let Some(value) = schema.pointer_mut("/$defs/StrategicPersonDraft/properties/gestalt_id") {
*value = serde_json::json!({"type":"string","enum":candidates.iter().map(|(_, _, gestalt, _)| gestalt.id.clone()).collect::<Vec<_>>()});
}
if let Some(proposals) = schema
.pointer_mut("/properties/proposals")
.and_then(serde_json::Value::as_object_mut)
{
proposals.insert("maxItems".into(), 1.into());
}
let request = ModelStageRequest {
stage: "strategic_individuation_selector".into(),
model: MODEL_FAST.into(),
snapshot_binding: strategic_individuation_binding(campaign, &digests, None),
lived_stream: format!(
"OUTPUT JSON SCHEMA (follow exactly):\n{}\n\nReturn zero or one proposal. Choose one person only when a selected Gestalt action has created concrete political work that cannot remain anonymous: an envoy, organizer, claimant, conspirator, commander, broker, or dissident. Identity content is a proposal only. Use a short stable lowercase member_id without a member: prefix. Do not invent authority, location, or state beyond the supplied Gestalt. Return an empty proposals list when nobody needs to emerge.\nCANDIDATES:\n{}",
serde_json::to_string(&schema).unwrap_or_default(),
serde_json::to_string(&context).unwrap_or_default(),
),
output_schema: Some(schema),
source_receipt_ids: campaign.branch_origin.evidence_receipt_ids.clone(),
temperature: Some(0.2),
max_output_tokens: Some(768),
};
let Ok(mut output) = run_validated_stage(model, &request).await else {
return (Vec::new(), Vec::new());
};
let Some(mut selection) = output
.structured
.clone()
.and_then(|value| serde_json::from_value::<StrategicPersonSelection>(value).ok())
else {
return (Vec::new(), vec![output]);
};
if selection.proposals.is_empty() {
return (Vec::new(), vec![output]);
}
if selection.proposals.len() != 1 {
output.receipt.validation_result = "semantic_invalid".into();
output.receipt.local_validation_error =
Some("strategic selector exceeded its one-person budget".into());
return (Vec::new(), vec![output]);
}
let draft = selection.proposals.remove(0);
let Some((_, _, gestalt, location_id)) = candidates.iter().find(|(digest, _, gestalt, _)| {
digest == &draft.action_digest && gestalt.id == draft.gestalt_id
}) else {
output.receipt.validation_result = "semantic_invalid".into();
output.receipt.local_validation_error =
Some("proposed person crossed the selected Gestalt action boundary".into());
return (Vec::new(), vec![output]);
};
let member_id = crate::domain::canonical_gestalt_member_local_id(&draft.member_id);
if member_id.is_empty()
|| draft.name.trim().is_empty()
|| campaign.gestalt_members.contains_key(&member_id)
{
output.receipt.validation_result = "semantic_invalid".into();
output.receipt.local_validation_error =
Some("proposed person has an empty or occupied identity".into());
return (Vec::new(), vec![output]);
}
let member = GestaltMemberDelta {
schema: "ghostlight.gestalt_member_delta.v1".into(),
id: member_id,
gestalt_id: gestalt.id.clone(),
version: 0,
name: draft.name,
capability_additions: BTreeSet::new(),
capability_removals: BTreeSet::new(),
knowledge_additions: BTreeSet::new(),
knowledge_removals: BTreeSet::new(),
equipment: BTreeSet::new(),
conditions: BTreeSet::new(),
obligations: draft.obligations,
relationships: draft.relationships,
goals: draft.goals,
memories: draft.memories,
last_location_id: Some(location_id.clone()),
materialized_actor_id: None,
last_relevant_revision: campaign.revision,
relevance_lease_until_revision: campaign.revision.saturating_add(4),
};
let proposal = StrategicGestaltIndividuation {
schema: "ghostlight.strategic_gestalt_individuation.v1".into(),
action_digest: draft.action_digest,
rationale: draft.rationale,
individuation: GestaltIndividuation {
gestalt_id: gestalt.id.clone(),
expected_gestalt_version: gestalt.version,
member,
location_id: location_id.clone(),
},
};
let proposal_digest = match strategic_individuation_proposal_digest(&proposal) {
Ok(digest) => digest,
Err(_) => return (Vec::new(), vec![output]),
};
output
.receipt
.rebind_snapshot(strategic_individuation_binding(
campaign,
&digests,
Some(&proposal_digest),
));
(vec![proposal], vec![output])
}
fn distinct_model_receipt_hashes(stages: &[ModelStageOutput]) -> Vec<String> {
let mut seen = BTreeSet::new();
stages
@ -1044,6 +1305,8 @@ mod tests {
malformed_cell: bool,
}
struct PersonFixtureModel;
#[test]
fn gestalt_state_references_qualify_canonical_ids_exactly_once() {
assert_eq!(
@ -1195,6 +1458,44 @@ mod tests {
}
}
#[async_trait]
impl ModelPort for PersonFixtureModel {
async fn run(&self, request: &ModelStageRequest) -> Result<String> {
let digest = request
.output_schema
.as_ref()
.and_then(|schema| {
schema.pointer("/$defs/StrategicPersonDraft/properties/action_digest/enum/0")
})
.and_then(serde_json::Value::as_str)
.ok_or_else(|| anyhow!("person fixture lacks action digest"))?;
let gestalt_id = request
.output_schema
.as_ref()
.and_then(|schema| {
schema.pointer("/$defs/StrategicPersonDraft/properties/gestalt_id/enum/0")
})
.and_then(serde_json::Value::as_str)
.ok_or_else(|| anyhow!("person fixture lacks Gestalt"))?;
Ok(serde_json::json!({"proposals":[{
"action_digest":digest,
"gestalt_id":gestalt_id,
"member_id":"veska-rill",
"name":"Veska Rill",
"goals":["control the grain delegation"],
"obligations":["answer to the river wards"],
"relationships":{},
"memories":["the lower road vanished after the dwarven excavation"],
"rationale":"The delegation needs one accountable broker."
}]})
.to_string())
}
fn provider(&self) -> &'static str {
"fixture"
}
}
#[test]
fn away_budget_waits_and_caps_at_eight() {
let now = Utc::now();
@ -1718,6 +2019,105 @@ mod tests {
validate_and_resolve_wave(&campaign, &output.wave).unwrap();
}
#[tokio::test]
async fn two_hundred_cell_wave_dispatches_in_parallel_under_one_provider_gate() {
let mut campaign = crate::resolution::tests::campaign(1_000, 200);
campaign.resolution_policy.provider_parallelism = 7;
let model = Arc::new(CellFixtureModel {
active: AtomicUsize::new(0),
maximum: AtomicUsize::new(0),
malformed_cell: false,
});
let output = propose_resolution_wave(model.clone(), Arc::new(AllowAllPermit), &campaign)
.await
.unwrap();
assert_eq!(output.wave.cover.cells.len(), 200);
assert_eq!(output.wave.appraisals.len(), 200);
assert_eq!(output.stages.len(), 601);
let maximum = model.maximum.load(Ordering::SeqCst);
assert!(maximum > 1, "the wave never dispatched concurrently");
assert!(
maximum <= 7,
"provider concurrency escaped its gate: {maximum}"
);
validate_and_resolve_wave(&campaign, &output.wave).unwrap();
}
#[tokio::test]
async fn strategic_gestalt_pressure_can_propose_one_action_bound_named_person() {
use crate::domain::{AgencySubjectKind, GestaltPersonaState, StrategicCellEffect};
let mut campaign = crate::resolution::tests::campaign(1, 1);
let mut profile = campaign.agency_profiles["faction-0000"].clone();
profile.subject_id = "river-wards".into();
profile.subject_kind = AgencySubjectKind::Gestalt;
profile.location_ids = BTreeSet::from(["center".into()]);
let location_id = profile.location_ids.iter().next().unwrap().clone();
campaign
.agency_profiles
.insert(profile.subject_id.clone(), profile);
campaign.gestalts.insert(
"river-wards".into(),
GestaltPersonaState {
schema: "ghostlight.gestalt_persona_state.v1".into(),
id: "river-wards".into(),
name: "River Wards".into(),
version: 0,
home_location_id: location_id.clone(),
shared_capabilities: BTreeSet::new(),
shared_knowledge: BTreeSet::new(),
resources: BTreeSet::from(["grain barges".into()]),
goals: vec!["keep the river wards fed".into()],
pressures: vec!["dwarven excavation diverted the lower road".into()],
},
);
let action = crate::domain::CellActionProposal {
subject_id: "river-wards".into(),
intent: "Send a grain delegation around the broken lower road.".into(),
intended_effect: "Negotiate a politically accountable detour.".into(),
priority: 80,
state_references: vec![],
public_channels: vec![],
effects: vec![StrategicCellEffect::Gestalt {
gestalt_id: "river-wards".into(),
pressure_additions: vec!["the delegation needs an accountable broker".into()],
pressure_resolutions: vec![],
}],
};
let digest = cell_action_digest(&action).unwrap();
let institution_action = crate::domain::CellActionProposal {
subject_id: "faction-0000".into(),
intent: "Publish the existing ration posture.".into(),
intended_effect: "Keep the institution legible.".into(),
priority: 20,
state_references: vec![],
public_channels: vec![],
effects: vec![StrategicCellEffect::Institution {
institution_id: "faction-0000".into(),
posture: "publishing ration posture".into(),
location_ids: vec![],
}],
};
let selected_actions = vec![action, institution_action];
let (proposals, stages) =
propose_strategic_individuation(&PersonFixtureModel, &campaign, &selected_actions)
.await;
assert_eq!(proposals.len(), 1);
assert_eq!(stages.len(), 1);
assert_eq!(proposals[0].action_digest, digest);
assert_eq!(proposals[0].individuation.gestalt_id, "river-wards");
assert_eq!(proposals[0].individuation.location_id, location_id);
assert_eq!(proposals[0].individuation.member.name, "Veska Rill");
assert_eq!(proposals[0].individuation.expected_gestalt_version, 0);
let candidate_digests =
strategic_individuation_candidate_digests(&campaign, &selected_actions);
assert_eq!(candidate_digests, vec![digest]);
let proposal_digest = strategic_individuation_proposal_digest(&proposals[0]).unwrap();
assert_eq!(
stages[0].receipt.snapshot_binding,
strategic_individuation_binding(&campaign, &candidate_digests, Some(&proposal_digest),)
);
}
#[tokio::test]
async fn one_malformed_cell_aborts_the_whole_wave() {
let campaign = crate::resolution::tests::campaign(6, 2);

View file

@ -19,7 +19,7 @@ use uuid::Uuid;
pub const MAX_SESSION_ZERO_MEMBERS: usize = 8;
pub const FIXTURE_CELL_ALLOWANCE: u8 = 8;
pub const OPERATOR_CELL_CEILING: u8 = 128;
pub const OPERATOR_CELL_CEILING: u8 = crate::resolution::MAX_ACTIVE_CELL_BUDGET;
pub trait EntitlementPort: Send + Sync {
fn persona_cell_allowance(&self, account_hash: &str) -> u8;
@ -6254,7 +6254,7 @@ mod tests {
display_name: format!("P{index}"),
is_host: false,
active: true,
cell_allowance: 32,
cell_allowance: if index == 0 { 40 } else { 32 },
joined_at: Utc::now(),
},
);

View file

@ -203,7 +203,7 @@ pub fn player_surface_for_actor(campaign: &Campaign, viewer_actor_id: &str) -> V
json!({"expected_revision":campaign.revision}),
&["time_advance_minutes"],
),
json!({"id":"dungeon.cells.budget","kind":"control.input.number","props":{"label":"Active Persona-cell budget","value":campaign.resolution_policy.active_cell_budget,"min":1,"max":128},"stateBindings":[local_draft("active_cell_budget","number")],"children":[]}),
json!({"id":"dungeon.cells.budget","kind":"control.input.number","props":{"label":"Active Persona-cell budget","value":campaign.resolution_policy.active_cell_budget,"min":1,"max":crate::resolution::MAX_ACTIVE_CELL_BUDGET},"stateBindings":[local_draft("active_cell_budget","number")],"children":[]}),
command_control(
"dungeon.cells.propose",
"Propose cell budget",

View file

@ -84,7 +84,7 @@ initiative, or commit validation.
one deterministic initiative/conflict pass. Arrival order at HTTP or CultNet
transport is not fictional initiative.
- Each active member contributes the allowance returned by the entitlement
port. The campaign pool is capped by the operator ceiling of 128. Human
port. The campaign pool is capped by the operator ceiling of 240. Human
actors occupy mandatory cells without Persona inference; provider request
concurrency remains independent of the chosen cover.
- Reconnect and replay derive the player's view from committed receipts and

View file

@ -251,7 +251,7 @@ direct mutation. Disconnection grants no proxy authority.
Each active member contributes their entitlement-provided cell allowance. Test
accounts currently contribute eight. The campaign starts at the pooled total,
capped at 128; physical provider concurrency remains independent.
capped at 240; physical provider concurrency remains independent.
## Realtime and interface

View file

@ -211,9 +211,14 @@ gestalts, or fictional time.
## Budgeted connected cover
The campaign selects an active Persona-cell budget from 1 through 128, default
The campaign selects an active Persona-cell budget from 1 through 240, default
8, bounded by its pooled entitlement. Provider parallelism is a separate
control capped at 32 concurrent cell pipelines.
The scale fixture covers 1,000 canonical subjects with 200 unique cells, then
dispatches every cell membrane in one wave while a seven-permit semaphore
bounds physical provider work. Wave width and provider pressure are therefore
separate authorities; all terminals still join, sort, validate, and commit as
one atomic resolution wave.
The partitioner:
1. contracts valid `keep_together` pins;

View file

@ -37,14 +37,13 @@ d20 resolution, persistent locations and institutions, multiresolution Gestalt
agency, offscreen strategic activity, knowledge-filtered news, and actor-filtered
Eve/CultMesh surfaces.
The immediate gate is continued adversarial play through Ghostlight's native
CultMesh boundary. The client has completed Heimdall OAuth, persists only an
opaque Ghostlight app-session bearer, fetches the same actor-filtered
`ghostlight.play` surface, and submits the same canonical Eve invocations as the
browser. Server-side refresh preserves that grant between client launches. The
36-case agency corpus remains only a candidate seed against a 300-reviewed-case
target. Separate-account multiplayer privacy and unanimity proof also remains
unproven.
Hosted Dungeon adversarial play remains an active regression and
context-discovery lane through Ghostlight's native CultMesh boundary. The client
has completed Heimdall OAuth, persists only an opaque Ghostlight app-session
bearer, fetches the same actor-filtered `ghostlight.play` surface, and submits
the same canonical Eve invocations as the browser. The 36-case agency corpus
and separate-account multiplayer proof remain open regression work; neither is
the current consumer-fixture gate.
## Authority map
@ -430,31 +429,26 @@ institutions or Gestalts enter through revisioned WorldKernel snapshots; and str
actions aimed at them persist typed proposals for consumer acknowledgement over
the existing loopback CultNet RUDP operation server.
The immediate gate is now the scale and emergence pass.
The scale and emergence gate is implemented and locally proven.
Delvehold is a consumer of Ghostlight's generic authored-world API, not a
Ghostlight-owned organ or special runtime. Ghostlight owns the world beyond the
Greathold as a persistent multiresolution political simulation, while Delvehold
retains player sovereignty and quantitative economy. The first design cut must
close two missing faculties:
strategic/offscreen named-person emergence and proven hundreds-class cell waves.
Do not begin by multiplying prompts or raising a constant; map resolution
ownership, emergence triggers, causal propagation, folding, and provider-call
amortization first.
retains player sovereignty and quantitative economy. WorldKernel now admits at
most one action-bound strategic individuation per wave through the existing
individuation commit primitive. The selector proposes identity content; the
kernel retains revision, lineage, location, uniqueness, materialization, and
atomic commit authority. The active budget ceiling is 240, independent of the
32-call provider gate. A fixture proves 1,000 subjects across 200 cells and all
200 membrane pipelines under a seven-call gate.
1. Specify a WorldKernel-owned offscreen individuation path that lets causal
political pressure produce durable named figures without granting entity
creation authority to a cell, arena, or model.
2. Measure the existing parallel wave dispatcher at the current 128-cell ceiling
and design the hundreds-cell budget around bounded provider concurrency,
outcome resolution, atomic commit, fairness, and cost.
3. Have the game-side adapter lower its authored hierarchy into the published
1. Have the game-side adapter lower its authored hierarchy into the published
generic schemas; keep projection and economy translation consumer-owned.
4. Prove one consumer-owned effect crosses that public API, changes two foreign
political layers, and returns attributed news or intent without mutating
Delvehold-owned truth.
5. Leave the unpublished Mars seed and Kalsa refuge expansion unapproved unless
2. Prove one consumer-owned effect crosses that public API, changes two foreign
political layers, causes one durable named figure to emerge and later act,
and returns attributed news or intent without mutating Delvehold-owned truth.
3. Leave the unpublished Mars seed and Kalsa refuge expansion unapproved unless
the operator explicitly admits either; retain Kalsa as regression evidence.
6. Keep `gamecult-ops` synchronized with every executable deployment and model
4. Keep `gamecult-ops` synchronized with every executable deployment and model
transport change. Use Idunn on Yggdrasil for all builds and deployments.
Do not weaken actor custody, private projection, unanimous approval,

View file

@ -652,7 +652,7 @@ unchanged.
Population scale uses reversible individuation:
```text
gestalt baseline + existing member delta, or a first-relevance identity proposal
gestalt baseline + existing member delta, or an admitted foreground or system-owned identity proposal
-> WorldKernel validates gestalt/member/revision/location
-> atomic durable member delta + temporary ActorState
-> ordinary Persona appraisal and world commands
@ -682,6 +682,16 @@ turn. The generated schema closes the individuation lane for resolved attempts,
and `WorldKernel` independently rejects any such plan. Outcome prose and
narration are not entity-admission inputs.
A separate strategic selector may propose one first-relevance identity only for
an exact selected Gestalt-owned action in the current resolution wave. Its
receipt binds the eligible Gestalt action digest set, exact locally admitted
proposal digest, and world revision.
WorldKernel revalidates the action/Gestalt pair, active simulation authority,
location, version, lineage, bounded identity payload, and uniqueness, then calls
the same `apply_individuation` primitive used by the system-only direct command.
The proposal and strategic plan commit atomically; the selector cannot own
entity creation, location, revision, materialization, or commit.
Institution `already_committed_posture` is projected as durable state, distinct
from unresolved pressure and fresh choice. Maintaining it is inaction. A model
may propose only a materially different posture; repeated posture remains a

View file

@ -197,7 +197,7 @@ stake prose alone never impersonates world mutation.
- `ActorState`, `InstitutionState`, gestalt baselines, member deltas,
relationships, possessions, and knowledge remain canonical. A
`simulation_cell.v1` is a derived connected cover and may be discarded.
- The campaign chooses 1–128 active Persona cells, default 8, within its pooled
- The campaign chooses 1–240 active Persona cells, default 8, within its pooled
entitlement. Provider parallelism is a separate control capped at 32.
Foreground subjects,
active leases, initiative holders, explicit targets, and individual-detail

View file

@ -26,7 +26,7 @@ and run one private Ghostlight Persona membrane per cohesive or arena cell. The
WorldKernel admits the whole cell wave atomically and forbids player puppeting,
synthetic arena actors, borrowed secrets, invented IDs, unreachable movement,
and unbounded information or population-pressure edits. The laboratory exposes
a pooled 1–128 campaign cell budget supplied by a player entitlement port,
a pooled 1–240 campaign cell budget supplied by a player entitlement port,
persistent pins, approval-gated gestalt fission, graph and cover receipts, and
a separate operator provider-concurrency limit. The
specific authority map is

View file

@ -137,4 +137,7 @@
{"note": "Operator redirected Ghostlight's near-term objective from Kalsa adversarial play to Delvehold's full external-world simulation. Delvehold owns the Greathold, players, dungeons, civic state, and quantitative economy; Ghostlight owns foreign regions, institutions, populations, people, relationships, knowledge, politics, events, and news. The target requires a cell budget in the hundreds, durable offscreen named-person emergence, nested political hierarchies, and causal response to committed Greathold effects. Current runtime caps cells at 128, dispatches cell pipelines concurrently under independently bounded provider parallelism up to 32, and only admits new-person individuation from immediate player speech, so scale proof and offscreen emergence are explicit architecture gaps. Kalsa remains regression evidence.", "status": "ok", "ts": "2026-08-26T15:51:04+00:00", "type": "research"}
{"note": "Delvehold is a proving consumer of Ghostlight's generic world-simulation API, not a Ghostlight integration organ. Ghostlight may add only generic capabilities: one seed admission primitive shared by compiler and consumer producers, generic externally owned subject authority, consumer-supplied effect schemas, and typed proposals and receipts. The greathold ID, all delvehold.* bindings, and detailed projection/economy translation remain Delvehold-owned. Cells and WorldKernel never switch on consumer-specific names.", "status": "ok", "ts": "2026-08-26T15:55:05+00:00", "type": "architecture"}
{"note": "Supersedes the 2026-08-26T15:51 research note only where it called Delvehold the redirected primary play target: Delvehold is the first proving consumer of the generic world API, while adversarial single-player Dungeon play remains an active regression and context-discovery path. Implemented one compiler/consumer WorldSeed admission transaction, institution-scoped ExternalSubjectAuthority, WorldKernel-owned revisioned snapshots, atomic strategic-wave proposals, acknowledgement receipts, and typed loopback CultNet RUDP operations. Consumer-specific IDs and quantitative economy semantics remain outside Ghostlight.", "status": "ok", "ts": "2026-08-26T16:46:55+00:00", "type": "architecture"}
{"ts": "2026-08-26T17:12:55+00:00", "type": "architecture", "status": "ok", "note": "The generic external authority/snapshot contract admits both institution and Gestalt subjects. This closes the Delvehold sovereign-gestalt requirement without adding a Delvehold-specific type: one tagged ExternalSubjectProjection carries either canonical shape, WorldKernel validates exact authority kind and subject ID, refreshes derived agency facets, and keeps the subject non-simulation-eligible while still targetable by foreign proposals."}
{"note": "The generic external authority/snapshot contract admits both institution and Gestalt subjects. This closes the Delvehold sovereign-gestalt requirement without adding a Delvehold-specific type: one tagged ExternalSubjectProjection carries either canonical shape, WorldKernel validates exact authority kind and subject ID, refreshes derived agency facets, and keeps the subject non-simulation-eligible while still targetable by foreign proposals.", "status": "ok", "ts": "2026-08-26T17:12:55+00:00", "type": "architecture"}
{"note": "Strategic/offscreen named-person emergence now reuses WorldKernel apply_individuation: one selected Gestalt action may carry one action-bound model identity proposal, while the kernel revalidates revision, simulation authority, location, lineage, version, bounded payload, uniqueness, and atomically materializes the person with the strategic wave. This supersedes the earlier claim that all new-person individuation requires immediate player speech; that gate applies only to automatic foreground presence.", "status": "ok", "ts": "2026-08-26T17:53:55+00:00", "type": "implementation"}
{"note": "Hundreds-class scale proof: the active cell ceiling is 240; a 1,000-subject fixture produced complete unique 200-cell coverage and dispatched all 200 three-stage membrane pipelines in one wave under a seven-call provider semaphore. Wave width remains independent of the provider parallelism ceiling of 32.", "status": "ok", "ts": "2026-08-26T17:53:55+00:00", "type": "verification"}
{"ts": "2026-08-26T18:08:11+00:00", "type": "verification", "status": "ok", "note": "Modeling audit closed four emergence authority holes before commit: the selector now rechecks the execution permit before and after inference; scheduler and kernel share one eligible-Gestalt candidate-digest function for mixed waves; the locally validated selector receipt is content-addressed again against the exact proposal digest; and the exact StrategicGestaltIndividuation row persists in the same CultCache strategic CAS. A substituted caller payload is rejected while campaign revision and member state remain unchanged. Duplicate public names, materialized actor-ID collisions, and relationships to unknown canonical subjects are rejected."}

View file

@ -139,3 +139,6 @@
{"key": "2026-08-26T155505Z0000-0137-59ce136e84c9", "payload": "eyJub3RlIjogIkRlbHZlaG9sZCBpcyBhIHByb3ZpbmcgY29uc3VtZXIgb2YgR2hvc3RsaWdodCdzIGdlbmVyaWMgd29ybGQtc2ltdWxhdGlvbiBBUEksIG5vdCBhIEdob3N0bGlnaHQgaW50ZWdyYXRpb24gb3JnYW4uIEdob3N0bGlnaHQgbWF5IGFkZCBvbmx5IGdlbmVyaWMgY2FwYWJpbGl0aWVzOiBvbmUgc2VlZCBhZG1pc3Npb24gcHJpbWl0aXZlIHNoYXJlZCBieSBjb21waWxlciBhbmQgY29uc3VtZXIgcHJvZHVjZXJzLCBnZW5lcmljIGV4dGVybmFsbHkgb3duZWQgc3ViamVjdCBhdXRob3JpdHksIGNvbnN1bWVyLXN1cHBsaWVkIGVmZmVjdCBzY2hlbWFzLCBhbmQgdHlwZWQgcHJvcG9zYWxzIGFuZCByZWNlaXB0cy4gVGhlIGdyZWF0aG9sZCBJRCwgYWxsIGRlbHZlaG9sZC4qIGJpbmRpbmdzLCBhbmQgZGV0YWlsZWQgcHJvamVjdGlvbi9lY29ub215IHRyYW5zbGF0aW9uIHJlbWFpbiBEZWx2ZWhvbGQtb3duZWQuIENlbGxzIGFuZCBXb3JsZEtlcm5lbCBuZXZlciBzd2l0Y2ggb24gY29uc3VtZXItc3BlY2lmaWMgbmFtZXMuIiwgInN0YXR1cyI6ICJvayIsICJ0cyI6ICIyMDI2LTA4LTI2VDE1OjU1OjA1KzAwOjAwIiwgInR5cGUiOiAiYXJjaGl0ZWN0dXJlIn0=", "stored_at": "2026-08-26T15:55:05.625298+00:00", "type": "ghostlight.evidence_record.v0"}
{"key": "2026-08-26T164655Z0000-0138-849e5de32fc5", "payload": "eyJub3RlIjogIlN1cGVyc2VkZXMgdGhlIDIwMjYtMDgtMjZUMTU6NTEgcmVzZWFyY2ggbm90ZSBvbmx5IHdoZXJlIGl0IGNhbGxlZCBEZWx2ZWhvbGQgdGhlIHJlZGlyZWN0ZWQgcHJpbWFyeSBwbGF5IHRhcmdldDogRGVsdmVob2xkIGlzIHRoZSBmaXJzdCBwcm92aW5nIGNvbnN1bWVyIG9mIHRoZSBnZW5lcmljIHdvcmxkIEFQSSwgd2hpbGUgYWR2ZXJzYXJpYWwgc2luZ2xlLXBsYXllciBEdW5nZW9uIHBsYXkgcmVtYWlucyBhbiBhY3RpdmUgcmVncmVzc2lvbiBhbmQgY29udGV4dC1kaXNjb3ZlcnkgcGF0aC4gSW1wbGVtZW50ZWQgb25lIGNvbXBpbGVyL2NvbnN1bWVyIFdvcmxkU2VlZCBhZG1pc3Npb24gdHJhbnNhY3Rpb24sIGluc3RpdHV0aW9uLXNjb3BlZCBFeHRlcm5hbFN1YmplY3RBdXRob3JpdHksIFdvcmxkS2VybmVsLW93bmVkIHJldmlzaW9uZWQgc25hcHNob3RzLCBhdG9taWMgc3RyYXRlZ2ljLXdhdmUgcHJvcG9zYWxzLCBhY2tub3dsZWRnZW1lbnQgcmVjZWlwdHMsIGFuZCB0eXBlZCBsb29wYmFjayBDdWx0TmV0IFJVRFAgb3BlcmF0aW9ucy4gQ29uc3VtZXItc3BlY2lmaWMgSURzIGFuZCBxdWFudGl0YXRpdmUgZWNvbm9teSBzZW1hbnRpY3MgcmVtYWluIG91dHNpZGUgR2hvc3RsaWdodC4iLCAic3RhdHVzIjogIm9rIiwgInRzIjogIjIwMjYtMDgtMjZUMTY6NDY6NTUrMDA6MDAiLCAidHlwZSI6ICJhcmNoaXRlY3R1cmUifQ==", "stored_at": "2026-08-26T16:46:55.684556+00:00", "type": "ghostlight.evidence_record.v0"}
{"key": "2026-08-26T171255Z0000-0139-aa631fe6e30f", "payload": "eyJub3RlIjogIlRoZSBnZW5lcmljIGV4dGVybmFsIGF1dGhvcml0eS9zbmFwc2hvdCBjb250cmFjdCBhZG1pdHMgYm90aCBpbnN0aXR1dGlvbiBhbmQgR2VzdGFsdCBzdWJqZWN0cy4gVGhpcyBjbG9zZXMgdGhlIERlbHZlaG9sZCBzb3ZlcmVpZ24tZ2VzdGFsdCByZXF1aXJlbWVudCB3aXRob3V0IGFkZGluZyBhIERlbHZlaG9sZC1zcGVjaWZpYyB0eXBlOiBvbmUgdGFnZ2VkIEV4dGVybmFsU3ViamVjdFByb2plY3Rpb24gY2FycmllcyBlaXRoZXIgY2Fub25pY2FsIHNoYXBlLCBXb3JsZEtlcm5lbCB2YWxpZGF0ZXMgZXhhY3QgYXV0aG9yaXR5IGtpbmQgYW5kIHN1YmplY3QgSUQsIHJlZnJlc2hlcyBkZXJpdmVkIGFnZW5jeSBmYWNldHMsIGFuZCBrZWVwcyB0aGUgc3ViamVjdCBub24tc2ltdWxhdGlvbi1lbGlnaWJsZSB3aGlsZSBzdGlsbCB0YXJnZXRhYmxlIGJ5IGZvcmVpZ24gcHJvcG9zYWxzLiIsICJzdGF0dXMiOiAib2siLCAidHMiOiAiMjAyNi0wOC0yNlQxNzoxMjo1NSswMDowMCIsICJ0eXBlIjogImFyY2hpdGVjdHVyZSJ9", "stored_at": "2026-08-26T17:12:55.998149+00:00", "type": "ghostlight.evidence_record.v0"}
{"key": "2026-08-26T175355Z0000-0140-9d8d9efc8fb7", "payload": "eyJub3RlIjogIlN0cmF0ZWdpYy9vZmZzY3JlZW4gbmFtZWQtcGVyc29uIGVtZXJnZW5jZSBub3cgcmV1c2VzIFdvcmxkS2VybmVsIGFwcGx5X2luZGl2aWR1YXRpb246IG9uZSBzZWxlY3RlZCBHZXN0YWx0IGFjdGlvbiBtYXkgY2Fycnkgb25lIGFjdGlvbi1ib3VuZCBtb2RlbCBpZGVudGl0eSBwcm9wb3NhbCwgd2hpbGUgdGhlIGtlcm5lbCByZXZhbGlkYXRlcyByZXZpc2lvbiwgc2ltdWxhdGlvbiBhdXRob3JpdHksIGxvY2F0aW9uLCBsaW5lYWdlLCB2ZXJzaW9uLCBib3VuZGVkIHBheWxvYWQsIHVuaXF1ZW5lc3MsIGFuZCBhdG9taWNhbGx5IG1hdGVyaWFsaXplcyB0aGUgcGVyc29uIHdpdGggdGhlIHN0cmF0ZWdpYyB3YXZlLiBUaGlzIHN1cGVyc2VkZXMgdGhlIGVhcmxpZXIgY2xhaW0gdGhhdCBhbGwgbmV3LXBlcnNvbiBpbmRpdmlkdWF0aW9uIHJlcXVpcmVzIGltbWVkaWF0ZSBwbGF5ZXIgc3BlZWNoOyB0aGF0IGdhdGUgYXBwbGllcyBvbmx5IHRvIGF1dG9tYXRpYyBmb3JlZ3JvdW5kIHByZXNlbmNlLiIsICJzdGF0dXMiOiAib2siLCAidHMiOiAiMjAyNi0wOC0yNlQxNzo1Mzo1NSswMDowMCIsICJ0eXBlIjogImltcGxlbWVudGF0aW9uIn0=", "stored_at": "2026-08-26T17:53:55.470514+00:00", "type": "ghostlight.evidence_record.v0"}
{"key": "2026-08-26T175355Z0000-0141-5d422df0eb1c", "payload": "eyJub3RlIjogIkh1bmRyZWRzLWNsYXNzIHNjYWxlIHByb29mOiB0aGUgYWN0aXZlIGNlbGwgY2VpbGluZyBpcyAyNDA7IGEgMSwwMDAtc3ViamVjdCBmaXh0dXJlIHByb2R1Y2VkIGNvbXBsZXRlIHVuaXF1ZSAyMDAtY2VsbCBjb3ZlcmFnZSBhbmQgZGlzcGF0Y2hlZCBhbGwgMjAwIHRocmVlLXN0YWdlIG1lbWJyYW5lIHBpcGVsaW5lcyBpbiBvbmUgd2F2ZSB1bmRlciBhIHNldmVuLWNhbGwgcHJvdmlkZXIgc2VtYXBob3JlLiBXYXZlIHdpZHRoIHJlbWFpbnMgaW5kZXBlbmRlbnQgb2YgdGhlIHByb3ZpZGVyIHBhcmFsbGVsaXNtIGNlaWxpbmcgb2YgMzIuIiwgInN0YXR1cyI6ICJvayIsICJ0cyI6ICIyMDI2LTA4LTI2VDE3OjUzOjU1KzAwOjAwIiwgInR5cGUiOiAidmVyaWZpY2F0aW9uIn0=", "stored_at": "2026-08-26T17:53:55.641566+00:00", "type": "ghostlight.evidence_record.v0"}
{"key": "2026-08-26T180811Z0000-0142-4552563edb4f", "payload": "eyJub3RlIjogIk1vZGVsaW5nIGF1ZGl0IGNsb3NlZCBmb3VyIGVtZXJnZW5jZSBhdXRob3JpdHkgaG9sZXMgYmVmb3JlIGNvbW1pdDogdGhlIHNlbGVjdG9yIG5vdyByZWNoZWNrcyB0aGUgZXhlY3V0aW9uIHBlcm1pdCBiZWZvcmUgYW5kIGFmdGVyIGluZmVyZW5jZTsgc2NoZWR1bGVyIGFuZCBrZXJuZWwgc2hhcmUgb25lIGVsaWdpYmxlLUdlc3RhbHQgY2FuZGlkYXRlLWRpZ2VzdCBmdW5jdGlvbiBmb3IgbWl4ZWQgd2F2ZXM7IHRoZSBsb2NhbGx5IHZhbGlkYXRlZCBzZWxlY3RvciByZWNlaXB0IGlzIGNvbnRlbnQtYWRkcmVzc2VkIGFnYWluIGFnYWluc3QgdGhlIGV4YWN0IHByb3Bvc2FsIGRpZ2VzdDsgYW5kIHRoZSBleGFjdCBTdHJhdGVnaWNHZXN0YWx0SW5kaXZpZHVhdGlvbiByb3cgcGVyc2lzdHMgaW4gdGhlIHNhbWUgQ3VsdENhY2hlIHN0cmF0ZWdpYyBDQVMuIEEgc3Vic3RpdHV0ZWQgY2FsbGVyIHBheWxvYWQgaXMgcmVqZWN0ZWQgd2hpbGUgY2FtcGFpZ24gcmV2aXNpb24gYW5kIG1lbWJlciBzdGF0ZSByZW1haW4gdW5jaGFuZ2VkLiBEdXBsaWNhdGUgcHVibGljIG5hbWVzLCBtYXRlcmlhbGl6ZWQgYWN0b3ItSUQgY29sbGlzaW9ucywgYW5kIHJlbGF0aW9uc2hpcHMgdG8gdW5rbm93biBjYW5vbmljYWwgc3ViamVjdHMgYXJlIHJlamVjdGVkLiIsICJzdGF0dXMiOiAib2siLCAidHMiOiAiMjAyNi0wOC0yNlQxODowODoxMSswMDowMCIsICJ0eXBlIjogInZlcmlmaWNhdGlvbiJ9", "stored_at": "2026-08-26T18:08:11.091051+00:00", "type": "ghostlight.evidence_record.v0"}

View file

@ -88,8 +88,8 @@ live_architecture:
- Mutation-scope and action-assessment proposals are content-addressed by the exact scope, assessor, and verifier models, stable instructions, scoped action-specific schema, and permitted typed packet. Private CultCache cache rows are derived inference evidence only. Revision and expiry bind the consumable assessment and roll but do not invalidate an otherwise identical semantic proposal; cache hits revalidate it, emit an explicit zero-token receipt linked to the source scope, assessment, and verifier receipts, and issue a fresh revision-bound digest. Changed actor state, occupancy, facts, institutions, permissions, contract, boundaries, schema, or model contract changes the key. Eve projections itemize every signed modifier and exact reference.
- The hosted player story is the chronological lowering of exact committed NarrativeTurn rows. No narrator or verifier model rewrites those turns; historical narration projections are inert and never enter Eve or CultMesh player surfaces.
- Epiphany consumes the extracted Ghostlight projection crate at an exact Git revision while retaining her canonical Mind, effect admission, and consequence authority.
- Gestalt Personas own shared population state; durable member deltas preserve specific people between appearances. Current new-person individuation is admitted only from exact immediately committed player speech, at most one person per event; strategic and offscreen first-relevance individuation are not yet implemented.
- The multiresolution agency graph derives a connected cohesive/arena cell cover from canonical actors, institutions, and gestalt leaves under a campaign-selected 1–128 pooled entitlement budget; provider concurrency is separate.
- Gestalt Personas own shared population state; durable member deltas preserve specific people between appearances. WorldKernel owns one revision-bound individuation primitive that validates exact Gestalt lineage, location, version, and identity uniqueness before atomically materializing the member. Foreground presence may call it after exact player speech; strategic waves may call it for at most one selected Gestalt action through an action-bound selector receipt. The model proposes identity content only.
- The multiresolution agency graph derives a connected cohesive/arena cell cover from canonical actors, institutions, and gestalt leaves under a campaign-selected 1–240 pooled entitlement budget; provider concurrency is independently capped at 32. A 1,000-subject, 200-cell fixture proves complete unique coverage and parallel membrane dispatch under a seven-call provider gate.
- Model-driven strategic cell waves can change remote actors, institutions, gestalts, clocks, and exact-channel news while the absent player remains outside their write authority; the entire wave validates and commits atomically.
- Population activity effects represent exact attributed attempts to prepare, coordinate, investigate, recruit, obstruct, trade, or communicate; graph scope, locations, channels, and semantics validate before the kernel derives an event, and the effect never claims its own success.
- Population migration is a separate exact-leaf effect. WorldKernel atomically moves the Gestalt and its agency-profile location along an explicit reachable migration relation; destination populations remain distinct and named members never move by implication.
@ -194,8 +194,8 @@ live_architecture:
active_subgoals:
- Prove the admitted generic world-consumer contract with a game-side fixture: shared compiler/consumer WorldSeed admission, externally authoritative institution-or-Gestalt snapshots, and attributed proposal acknowledgement over typed CultNet, without consumer-specific IDs or schemas in Ghostlight.
- Specify strategic/offscreen named-person individuation, plotting, durable identity, and folding under WorldKernel authority; remote causal pressure must be able to make a person matter without granting a cell, arena, or model entity-creation authority.
- Prove the hundreds-class wave target using the existing parallel cell dispatcher and separately bounded provider concurrency. Measure the current 128-cell ceiling first; widen it only if the authored outside-world cover requires more, and only with an explicit scheduling, atomicity, and cost model.
- Exercise strategic/offscreen named-person individuation across sustained political waves so emerged people plot, acquire relationships, fold, and return without granting a cell, arena, or model commit authority.
- Pressure the proven 200-cell dispatcher with authored consumer hierarchies and measure provider cost; the active budget ceiling is 240 while provider concurrency remains separately capped at 32.
- Finish the semantic mutation migration: delete or demote remaining legacy model effect schemas and move aggregate campaign fields toward projection-only state. Seed publication and named-person presence now have explicit non-effect authority classifications.
- Keep the selectable Kalsa Vault's Public player-safe and Spoilers GM-only evidence lanes intact through Git/Obsidian sync, retrieval, exact receipts, and player projection. Do not import it into the read-only Aetheria recovery index or create a rival Ghostlight semantic index.
- Grow the 36-case candidate agency corpus toward 300 reviewed, behaviorally distinct attempts that either compose from existing primitives, produce an explicit bargain, or falsify the algebra.
@ -324,5 +324,5 @@ data_flow:
current_status:
summary: Yggdrasil serves immutable Ghostlight release fdb6352e0dfdd0eeb129020ea9dea0b6225eccf9 with Eve 672c0c1e9fc828b63752345385727b06c50491d0, daemon SHA-256 d4c57cc6df0e2743f2f444bbdf4c2c90961a41b74e2c92cf45fa52b4e63f5714, native-client SHA-256 0d54f75ce7fe66b876ae720cdf9d6314778972f2193410fe2979ec51b8d25aba, artifact sha256-feadb845bf999afb09e781f3e81a36fe5bc5b2d1d9eec06c028bbd81680a548a, and independent CodexConnector release 2f29b90eebd8fc1bbdc07cdd22ac4883f16849a5 using gpt-5.6-luna. Idunn passed 347 tests and left both services healthy with zero restarts. Kalsa campaign e99e8794-281f-4a82-8b2c-5e6954bd6b16 is at world revision 21, resolution epoch 3, and configured budget 1. Startup migration repaired Cal Rusk to exact canonical member:cal_rusk without advancing fiction; his learned knowledge and personal delta survived folding, travel, and rematerialisation. A budget-1 strategic wave advanced remote actors without puppeting Asha. Destination preview 9fa863a0-e1fc-4115-bb18-74d523a3c6de synthesized eight source-compatible branch assumptions and returned zero material gaps; it remains unapproved and the world is unchanged. The unpublished Mars preview also remains unapproved. Existing mutation, Gestalt, topology, knowledge, privacy, approval, no-puppeting, and atomicity invariants remain authoritative. Starfire remains retired as a writer and build host.
next_action: Keep Dungeon adversarial single-player play as regression pressure while the game-side adapter prepares its first generic WorldSeed. In Ghostlight, specify and prove WorldKernel-owned offscreen named-person emergence and benchmark the parallel dispatcher at the current 128-cell ceiling before widening toward hundreds. Then run one consumer fixture through seed admission, authoritative institution snapshot, two simulated political layers, durable named-person action, and attributed proposal acknowledgement without mutating consumer-owned truth.
next_action: Keep Dungeon adversarial single-player play as regression pressure while the game-side adapter prepares its first generic WorldSeed. Run one consumer fixture through seed admission, authoritative institution snapshot, two simulated political layers, strategic action-bound named-person emergence, a later action by that durable person, and attributed proposal acknowledgement without mutating consumer-owned truth.
gestalt_population: Gestalt Personas own shared population baselines; persistent member deltas own named individuality; WorldKernel materialization and approval-gated fission preserve lineage and identity. Materialisation/folding changes only representation and the exact member delta, advances resolution epoch, and retires the old cover; it has no Gestalt-wide effect authority. Selected actionable member exceptions are mandatory Projector perspective owners, so aggregation cannot silently erase their decision. 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.