Complete Persona context and gestalt individuation

This commit is contained in:
Meta Vens 2026-08-16 23:03:30 +02:00
commit 22281891cb
14 changed files with 341 additions and 92 deletions

View file

@ -149,6 +149,14 @@ pub struct GestaltPromotion {
pub location_id: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct GestaltIndividuation {
pub gestalt_id: String,
pub expected_gestalt_version: u64,
pub member: GestaltMemberDelta,
pub location_id: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct GestaltDemotion {
pub actor_id: String,
@ -158,6 +166,8 @@ pub struct GestaltDemotion {
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
pub struct GestaltPresencePlan {
#[serde(default)]
pub individuations: Vec<GestaltIndividuation>,
pub promotions: Vec<GestaltPromotion>,
pub demotions: Vec<GestaltDemotion>,
}
@ -422,6 +432,10 @@ pub enum WorldCommand {
expected_member_version: u64,
location_id: String,
},
IndividuateGestaltMember {
expected_revision: u64,
individuation: GestaltIndividuation,
},
DematerializeGestaltMember {
expected_revision: u64,
actor_id: String,

View file

@ -34,7 +34,7 @@ impl GestaltPresencePlanner {
model: self.model_name.clone(),
snapshot_binding: format!("campaign:{}:revision:{}", campaign.id, campaign.revision),
lived_stream: format!(
"Choose reversible Persona population presence after this event. Promote a member only when individualized interaction, perception, custody, promise, injury, possession, or relationship now matters. Demote a materialized member when they are no longer scene-relevant. Use only the supplied IDs and exact versions. Never place a promoted member outside the player location. Aggregate deltas must remain empty; population learning requires separate review. Emit the exact JSON schema.\nSCHEMA:\n{}\nCANDIDATES:\n{}\nEVENT:\n{}",
"Choose reversible Persona population presence after this event. Promote an existing member when they become individually relevant. If the event makes an anonymous population member individually relevant and no supplied member fits, individuate exactly one durable member delta from the gestalt baseline; use a new stable lowercase id, version 0, the exact gestalt id/version, no materialized actor id, and record only personal departures from the shared baseline. Demote a materialized member when they are no longer scene-relevant. Never place a promoted or individuated member outside the player location. Aggregate deltas must remain empty; population learning requires separate review. Emit the exact JSON schema.\nSCHEMA:\n{}\nCANDIDATES:\n{}\nEVENT:\n{}",
serde_json::to_string_pretty(&schema)?, candidates, event_summary
),
output_schema: Some(schema),
@ -58,6 +58,27 @@ fn validate_plan(
player_location: &str,
) -> Result<()> {
let mut members = BTreeSet::new();
for individuation in &plan.individuations {
let member = &individuation.member;
let gestalt = campaign
.gestalts
.get(&individuation.gestalt_id)
.ok_or_else(|| anyhow!("presence plan invented a gestalt"))?;
if individuation.expected_gestalt_version != gestalt.version
|| individuation.location_id != player_location
|| member.gestalt_id != individuation.gestalt_id
|| member.version != 0
|| member.materialized_actor_id.is_some()
|| member.id.trim().is_empty()
|| member.name.trim().is_empty()
|| campaign.gestalt_members.contains_key(&member.id)
|| !members.insert(member.id.clone())
{
return Err(anyhow!(
"presence individuation does not match its snapshot"
));
}
}
for promotion in &plan.promotions {
if !members.insert(promotion.member_id.clone()) {
return Err(anyhow!("presence plan promotes one member twice"));

View file

@ -348,6 +348,22 @@ fn execute(
"direct materialization command",
)
}
WorldCommand::IndividuateGestaltMember {
expected_revision,
individuation,
} => {
require_revision(&campaign, expected_revision)?;
let before = campaign.clone();
apply_individuation(&mut campaign, &individuation)?;
commit_gestalt_presence(
store,
row,
before,
campaign,
"individuate_gestalt_member",
"direct individuation command",
)
}
WorldCommand::DematerializeGestaltMember {
expected_revision,
actor_id,
@ -385,6 +401,9 @@ fn execute(
for demotion in &plan.demotions {
apply_demotion(&mut candidate, demotion)?;
}
for individuation in &plan.individuations {
apply_individuation(&mut candidate, individuation)?;
}
for promotion in &plan.promotions {
apply_promotion(&mut candidate, promotion)?;
}
@ -505,6 +524,42 @@ fn execute(
}
}
fn apply_individuation(
campaign: &mut Campaign,
individuation: &GestaltIndividuation,
) -> Result<(), KernelError> {
let member = &individuation.member;
let gestalt = campaign
.gestalts
.get(&individuation.gestalt_id)
.ok_or_else(|| KernelError::Invalid("gestalt is unknown".into()))?;
if gestalt.version != individuation.expected_gestalt_version
|| member.gestalt_id != individuation.gestalt_id
|| member.version != 0
|| member.materialized_actor_id.is_some()
|| member.id.trim().is_empty()
|| member.name.trim().is_empty()
|| campaign.gestalt_members.contains_key(&member.id)
{
return Err(KernelError::Invalid(
"gestalt individuation is stale or malformed".into(),
));
}
campaign
.gestalt_members
.insert(member.id.clone(), member.clone());
apply_promotion(
campaign,
&GestaltPromotion {
gestalt_id: individuation.gestalt_id.clone(),
expected_gestalt_version: individuation.expected_gestalt_version,
member_id: member.id.clone(),
expected_member_version: 0,
location_id: individuation.location_id.clone(),
},
)
}
fn apply_promotion(
campaign: &mut Campaign,
promotion: &GestaltPromotion,
@ -1664,30 +1719,27 @@ mod tests {
pressures: vec![],
},
);
seed.gestalt_members.insert(
"john".into(),
GestaltMemberDelta {
schema: "ghostlight.gestalt_member_delta.v1".into(),
id: "john".into(),
gestalt_id: "village".into(),
version: 0,
name: "John".into(),
capability_additions: BTreeSet::from(["master blacksmith".into()]),
capability_removals: BTreeSet::new(),
knowledge_additions: BTreeSet::new(),
knowledge_removals: BTreeSet::new(),
equipment: BTreeSet::from(["John's hammer".into()]),
conditions: BTreeSet::new(),
obligations: BTreeSet::new(),
relationships: BTreeMap::from([("player".into(), "new acquaintance".into())]),
goals: vec![],
memories: vec!["met the player".into()],
last_location_id: None,
materialized_actor_id: None,
last_relevant_revision: 0,
relevance_lease_until_revision: 0,
},
);
let john = GestaltMemberDelta {
schema: "ghostlight.gestalt_member_delta.v1".into(),
id: "john".into(),
gestalt_id: "village".into(),
version: 0,
name: "John".into(),
capability_additions: BTreeSet::from(["master blacksmith".into()]),
capability_removals: BTreeSet::new(),
knowledge_additions: BTreeSet::new(),
knowledge_removals: BTreeSet::new(),
equipment: BTreeSet::from(["John's hammer".into()]),
conditions: BTreeSet::new(),
obligations: BTreeSet::new(),
relationships: BTreeMap::from([("player".into(), "new acquaintance".into())]),
goals: vec![],
memories: vec!["met the player".into()],
last_location_id: None,
materialized_actor_id: None,
last_relevant_revision: 0,
relevance_lease_until_revision: 0,
};
kernel
.command(WorldCommand::CreateCampaign {
campaign: seed,
@ -1697,13 +1749,14 @@ mod tests {
.await
.unwrap();
let first = kernel
.command(WorldCommand::MaterializeGestaltMember {
.command(WorldCommand::IndividuateGestaltMember {
expected_revision: 0,
gestalt_id: "village".into(),
expected_gestalt_version: 0,
member_id: "john".into(),
expected_member_version: 0,
location_id: "room".into(),
individuation: GestaltIndividuation {
gestalt_id: "village".into(),
expected_gestalt_version: 0,
member: john,
location_id: "room".into(),
},
})
.await
.unwrap();
@ -1788,6 +1841,7 @@ mod tests {
);
let bad_plan = GestaltPresencePlan {
individuations: vec![],
demotions: vec![GestaltDemotion {
actor_id: "member:john".into(),
aggregate_delta: GestaltAggregateDelta::default(),

View file

@ -16,7 +16,7 @@ use ghostlight_dungeon::{
},
gestalt::GestaltPresencePlanner,
kernel::CommandResult,
mesh::MeshPublisher,
mesh::{CampaignMeshSnapshot, MeshPublisher},
model::{DeepSeekPort, ModelPort, ModelStageRequest, run_validated_stage},
narrator::Narrator,
persistence::CampaignStore,
@ -1052,6 +1052,7 @@ fn player_http_command_allowed(command: &WorldCommand, player_actor_id: &str) ->
| WorldCommand::ExpandRegion { .. }
| WorldCommand::MaterializeGestaltMember { .. }
| WorldCommand::DematerializeGestaltMember { .. }
| WorldCommand::IndividuateGestaltMember { .. }
| WorldCommand::ReconcileGestaltPresence { .. }
| WorldCommand::ResolveReactionWave { .. }
| WorldCommand::BeginNpcAction { .. } => false,
@ -1112,7 +1113,18 @@ async fn refresh_mesh(state: &AppState) -> anyhow::Result<serde_json::Value> {
.filter(|value| value.campaign_id == campaign.id)
.collect::<Vec<_>>();
narrations.sort_by_key(|value| value.source_revision);
snapshots.push((campaign, narrations));
snapshots.push(CampaignMeshSnapshot {
campaign,
narrations,
evidence: runtime.store.load_all("vault_evidence_receipt.v1")?,
commits: runtime.store.load_all("world_commit_receipt.v1")?,
stages: runtime.store.load_all("persona_stage_receipt.v1")?,
strategic_ticks: runtime.store.load_all("strategic_tick.v1")?,
gestalt_receipts: runtime
.store
.load_all("gestalt_materialization_receipt.v1")?,
rejected: runtime.store.load_all("rejected_proposal_receipt.v1")?,
});
}
let publisher = state.mesh.clone();
let deepseek = state.deepseek_status.clone();
@ -1341,33 +1353,10 @@ async fn operator_inspector(headers: HeaderMap, State(state): State<AppState>) -
return (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response();
}
};
let evidence = runtime
.store
.load_all::<ghostlight_dungeon::domain::VaultEvidenceReceipt>("vault_evidence_receipt.v1")
.unwrap_or_default();
let commits = runtime
.store
.load_all::<ghostlight_dungeon::domain::WorldCommitReceipt>("world_commit_receipt.v1")
.unwrap_or_default();
let stages = runtime
.store
.load_all::<ghostlight_dungeon::model::ModelStageReceipt>("persona_stage_receipt.v1")
.unwrap_or_default();
let rejected = runtime
.store
.load_all::<RejectedProposalReceipt>("rejected_proposal_receipt.v1")
.unwrap_or_default();
let strategic_ticks = runtime
.store
.load_all::<ghostlight_dungeon::domain::StrategicTickReceipt>("strategic_tick.v1")
.unwrap_or_default();
let gestalt_receipts = runtime
.store
.load_all::<ghostlight_dungeon::domain::GestaltMaterializationReceipt>(
"gestalt_materialization_receipt.v1",
)
.unwrap_or_default();
Json(serde_json::json!({"schema":"ghostlight.operator_inspector.v1","campaign":campaign,"evidence":evidence,"commit_receipts":commits,"model_stage_receipts":stages,"strategic_ticks":strategic_ticks,"gestalt_materialization_receipts":gestalt_receipts,"rejected_proposals":rejected,"scheduler":{"live_turn_pressure":state.live_turns.load(Ordering::SeqCst)}})).into_response()
match state.mesh.operator_surface(campaign.id) {
Ok(surface) => Json(surface).into_response(),
Err(error) => (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response(),
}
}
async fn process_due_ticks(

View file

@ -8,7 +8,7 @@ use crate::{
WorldActionProposal, WorldClock, WorldCommitReceipt, WorldCompilePreview, WorldFact,
},
model::ModelStageReceipt,
surface::player_surface,
surface::{operator_surface, player_surface},
};
use anyhow::{Context, Result};
use chrono::Utc;
@ -27,6 +27,18 @@ pub const PROVIDER_ID: &str = "gamecult.ghostlight.dungeon";
pub const HEALTH_KEY: &str = "ghostlight:dungeon:health";
pub const ADVERTISEMENT_KEY: &str = "eve:provider:gamecult.ghostlight.dungeon";
#[derive(Clone)]
pub struct CampaignMeshSnapshot {
pub campaign: Campaign,
pub narrations: Vec<NarrationProjection>,
pub evidence: Vec<VaultEvidenceReceipt>,
pub commits: Vec<WorldCommitReceipt>,
pub stages: Vec<ModelStageReceipt>,
pub strategic_ticks: Vec<StrategicTickReceipt>,
pub gestalt_receipts: Vec<GestaltMaterializationReceipt>,
pub rejected: Vec<RejectedProposalReceipt>,
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(type = "gamecult.eve.surface", schema = "gamecult.eve.surface.v1")]
pub struct EveSurfaceRecord {
@ -117,7 +129,7 @@ impl MeshPublisher {
pub fn publish_snapshot(
&self,
campaigns: &[(Campaign, Vec<NarrationProjection>)],
campaigns: &[CampaignMeshSnapshot],
deepseek_status: &str,
live_turn_pressure: usize,
) -> Result<Value> {
@ -157,18 +169,32 @@ impl MeshPublisher {
});
let surfaces = campaigns
.iter()
.map(|(campaign, _)| {
json!({
"schema":"gamecult.eve.surface.v1",
"surfaceId":format!("ghostlight.campaign.{}",campaign.id),
"key":format!("eve:surface:ghostlight.campaign.{}",campaign.id),
"transport":"cultmesh-record",
"status":"available",
"audience":"authenticated-player",
"mode":"interactive",
"surfaceKind":"interactive-world",
"interactionModel":"provider-command-receipts"
})
.flat_map(|snapshot| {
let campaign = &snapshot.campaign;
[
json!({
"schema":"gamecult.eve.surface.v1",
"surfaceId":format!("ghostlight.campaign.{}",campaign.id),
"key":format!("eve:surface:ghostlight.campaign.{}",campaign.id),
"transport":"cultmesh-record",
"status":"available",
"audience":"authenticated-player",
"mode":"interactive",
"surfaceKind":"interactive-world",
"interactionModel":"provider-command-receipts"
}),
json!({
"schema":"gamecult.eve.surface.v1",
"surfaceId":format!("ghostlight.operator.{}",campaign.id),
"key":format!("eve:operator:ghostlight.campaign.{}",campaign.id),
"transport":"cultmesh-record",
"status":"available",
"audience":"authenticated-operator",
"mode":"inspect",
"surfaceKind":"operator-inspector",
"interactionModel":"read-only-receipt-inspection"
}),
]
})
.collect::<Vec<_>>();
let advertisement = json!({
@ -212,8 +238,9 @@ impl MeshPublisher {
value: advertisement,
},
)?;
for (campaign, narrations) in campaigns {
let surface = player_surface(campaign, narrations);
for snapshot in campaigns {
let campaign = &snapshot.campaign;
let surface = player_surface(campaign, &snapshot.narrations);
let key = format!("eve:surface:ghostlight.campaign.{}", campaign.id);
self.put_and_publish(
&mut node,
@ -222,6 +249,34 @@ impl MeshPublisher {
value: surface.clone(),
},
)?;
let operator = operator_surface(
campaign,
&snapshot.evidence,
&snapshot.commits,
&snapshot.stages,
&snapshot.strategic_ticks,
&snapshot.gestalt_receipts,
&snapshot.rejected,
live_turn_pressure,
);
self.put_and_publish(
&mut node,
format!("eve:operator:ghostlight.campaign.{}", campaign.id),
&EveSurfaceRecord {
value: operator.clone(),
},
)?;
self.put_and_publish(
&mut node,
format!("eve:operator-state:ghostlight.campaign.{}", campaign.id),
&EveSurfaceStateRecord {
provider_id: PROVIDER_ID.into(),
title: format!("{} operator", campaign.name),
version: campaign.revision as i64,
updated_at: updated_at.clone(),
surface: operator,
},
)?;
self.put_and_publish(
&mut node,
format!("eve:surface-state:ghostlight.campaign.{}", campaign.id),
@ -246,6 +301,16 @@ impl MeshPublisher {
.map(|record| record.value)
}
pub fn operator_surface(&self, campaign_id: uuid::Uuid) -> Result<Value> {
self.node
.lock()
.map_err(|_| anyhow::anyhow!("mesh lock poisoned"))?
.get_required::<EveSurfaceRecord>(&format!(
"eve:operator:ghostlight.campaign.{campaign_id}"
))
.map(|record| record.value)
}
fn put_and_publish<T>(
&self,
node: &mut CultMeshNode,

View file

@ -12,10 +12,12 @@ use std::sync::Arc;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct PermittedActorSlice {
pub actor_id: String,
pub location_id: String,
pub snapshot_binding: String,
pub identity_experience: Vec<String>,
pub memories: Vec<String>,
pub perceived_events: Vec<String>,
pub perceived_actors: std::collections::BTreeMap<String, String>,
pub relationships: Vec<String>,
pub goals: Vec<String>,
pub knowledge: Vec<String>,
@ -129,7 +131,8 @@ impl PersonaProjectionEngine {
self.permit
.require(&slice.actor_id, &slice.snapshot_binding, "interpreter")
.await?;
let schema = serde_json::to_value(schema_for!(PersonaProposalBundle))?;
let mut schema = serde_json::to_value(schema_for!(PersonaProposalBundle))?;
constrain_interpreter_schema(&mut schema, &slice)?;
let interpreted = run_validated_stage(
self.model.as_ref(),
&ModelStageRequest {
@ -138,10 +141,11 @@ impl PersonaProjectionEngine {
snapshot_binding: slice.snapshot_binding.clone(),
lived_stream: build_interpreter_prompt(&InterpreterPrompt {
identity: &slice.actor_id,
typed_context: &typed_context,
lived_stream: &lived.text,
persona_output: &persona.narrative,
output_schema: &serde_json::to_string_pretty(&schema)?,
domain_guidance: "Record only private changes supported by the lived stream. World actions are attempts, not completed effects. Every world action actor_id must match the identity.",
domain_guidance: "Record only private changes supported by the lived stream and typed context. World actions are attempts, not completed effects. Use only exact actor ids and state-reference tokens admitted by the output schema.",
}),
output_schema: Some(schema),
source_receipt_ids: slice.source_receipt_ids,
@ -166,6 +170,51 @@ impl PersonaProjectionEngine {
}
}
fn constrain_interpreter_schema(
schema: &mut serde_json::Value,
slice: &PermittedActorSlice,
) -> Result<()> {
let allowed_references = slice
.capabilities
.iter()
.map(|value| format!("capability:{value}"))
.chain(
slice
.knowledge
.iter()
.map(|value| format!("knowledge:{value}")),
)
.chain(
slice
.affordances
.iter()
.map(|value| format!("equipment:{value}")),
)
.chain(std::iter::once(format!("location:{}", slice.location_id)))
.collect::<Vec<_>>();
let world_action = schema
.pointer_mut("/$defs/WorldActionProposal/properties")
.and_then(|value| value.as_object_mut())
.ok_or_else(|| anyhow!("Persona proposal schema has no world action properties"))?;
world_action.insert(
"actor_id".into(),
serde_json::json!({"const":slice.actor_id}),
);
world_action.insert(
"state_references".into(),
serde_json::json!({"type":"array","items":{"type":"string","enum":allowed_references}}),
);
let relationship_updates = schema
.pointer_mut("/$defs/ActorStateDelta/properties/relationship_updates")
.and_then(|value| value.as_object_mut())
.ok_or_else(|| anyhow!("Persona proposal schema has no relationship updates"))?;
relationship_updates.insert(
"propertyNames".into(),
serde_json::json!({"enum":slice.perceived_actors.keys().collect::<Vec<_>>()}),
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
@ -181,10 +230,15 @@ mod tests {
};
let slice = PermittedActorSlice {
actor_id: "npc".into(),
location_id: "room".into(),
snapshot_binding: "campaign:1".into(),
identity_experience: vec!["A tired navigator".into()],
memories: vec![],
perceived_events: vec![],
perceived_actors: std::collections::BTreeMap::from([(
"player".into(),
"Player".into(),
)]),
relationships: vec![],
goals: vec![],
knowledge: vec![],

View file

@ -1,4 +1,10 @@
use crate::domain::{Campaign, NarrationProjection};
use crate::{
domain::{
Campaign, GestaltMaterializationReceipt, NarrationProjection, RejectedProposalReceipt,
StrategicTickReceipt, VaultEvidenceReceipt, WorldCommitReceipt,
},
model::ModelStageReceipt,
};
use serde_json::{Value, json};
pub fn player_surface(campaign: &Campaign, narrations: &[NarrationProjection]) -> Value {
@ -47,6 +53,37 @@ pub fn player_surface(campaign: &Campaign, narrations: &[NarrationProjection]) -
})
}
pub fn operator_surface(
campaign: &Campaign,
evidence: &[VaultEvidenceReceipt],
commits: &[WorldCommitReceipt],
stages: &[ModelStageReceipt],
strategic_ticks: &[StrategicTickReceipt],
gestalt_receipts: &[GestaltMaterializationReceipt],
rejected: &[RejectedProposalReceipt],
live_turn_pressure: usize,
) -> Value {
let typed = json!({
"campaign": campaign,
"evidence": evidence,
"commit_receipts": commits,
"model_stage_receipts": stages,
"strategic_ticks": strategic_ticks,
"gestalt_materialization_receipts": gestalt_receipts,
"rejected_proposals": rejected,
"scheduler": {"live_turn_pressure": live_turn_pressure}
});
json!({
"type":"surface-state", "schema":"gamecult.eve.surface.v1", "providerId":"gamecult.ghostlight.dungeon",
"providerKind":"narrative.simulation.operator", "title":format!("{} operator",campaign.name), "version":campaign.revision,
"surface":{"id":format!("ghostlight.operator.{}",campaign.id),"root":{"id":"dungeon.operator.root","kind":"surface","props":{},"children":[
{"id":"dungeon.operator.status","kind":"card","props":{"title":format!("Revision {} · {} model stages · {} rejected proposals",campaign.revision,stages.len(),rejected.len())},"children":[]},
{"id":"dungeon.operator.typed","kind":"code","props":{"language":"json","value":serde_json::to_string_pretty(&typed).unwrap_or_else(|_| "operator projection failed".into())},"children":[]}
]},"styles":{"tokens":{"colorBackground":"#0c1110","colorPanel":"#17201d","colorText":"#e8e1cf","colorMuted":"#9aa69f","colorAccent":"#d49b58"}}},
"commands":[]
})
}
fn join(values: &std::collections::BTreeSet<String>) -> String {
if values.is_empty() {
"none".into()

View file

@ -58,19 +58,28 @@ pub async fn appraise_present(
})
.cloned()
.collect();
let perceived_actors = campaign
.actors
.values()
.filter(|actor| actor.location_id == player.location_id)
.map(|actor| (actor.id.clone(), actor.name.clone()))
.collect::<std::collections::BTreeMap<_, _>>();
let mut jobs = tokio::task::JoinSet::new();
for actor in actors {
let engine = engine.clone();
let snapshot = format!("campaign:{}:revision:{}", campaign.id, campaign.revision);
let event = event_summary.to_owned();
let receipts = campaign.branch_origin.evidence_receipt_ids.clone();
let perceived_actors = perceived_actors.clone();
jobs.spawn(async move {
let slice = PermittedActorSlice {
actor_id: actor.id.clone(),
location_id: actor.location_id.clone(),
snapshot_binding: snapshot,
identity_experience: vec![format!("You are {}.", actor.name)],
memories: actor.memories,
perceived_events: vec![event],
perceived_actors,
relationships: actor
.relationships
.into_iter()

View file

@ -21,6 +21,7 @@ pub struct PersonaPrompt<'a> {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct InterpreterPrompt<'a> {
pub identity: &'a str,
pub typed_context: &'a str,
pub lived_stream: &'a str,
pub persona_output: &'a str,
pub output_schema: &'a str,
@ -48,9 +49,10 @@ pub fn build_persona_prompt(input: &PersonaPrompt<'_>) -> String {
pub fn build_interpreter_prompt(input: &InterpreterPrompt<'_>) -> String {
format!(
"<!-- membrane:{MEMBRANE_SCHEMA}:interpreter -->\nYou are the private Interpreter for {identity}. Convert the natural Persona turn into typed candidate effects supported by the lived stream. Candidates are proposals only; the owning runtime validates and commits them. Do not invent knowledge, capability, custody, perception, or completed consequences.\n\nDomain guidance:\n{guidance}\n\nLived stream:\n{stream}\n\nPersona turn:\n{output}\n\nReturn exactly one JSON object matching this schema:\n{schema}",
"<!-- membrane:{MEMBRANE_SCHEMA}:interpreter -->\nYou are the private Interpreter for {identity}. Convert the natural Persona turn into typed candidate effects supported by the lived stream and permissioned typed context. Candidates are proposals only; the owning runtime validates and commits them. Do not invent knowledge, capability, custody, perception, identifiers, state references, or completed consequences.\n\nDomain guidance:\n{guidance}\n\nPermissioned typed context:\n{context}\n\nLived stream:\n{stream}\n\nPersona turn:\n{output}\n\nReturn exactly one JSON object matching this schema:\n{schema}",
identity = input.identity,
guidance = input.domain_guidance,
context = input.typed_context,
stream = input.lived_stream,
output = input.persona_output,
schema = input.output_schema,

View file

@ -71,8 +71,12 @@ materialize newly relevant members and fold irrelevant members back into their
persistent deltas. The planner may propose presence only; it cannot promote
population-wide learning or mutate state directly.
World compilation seeds collective populations and a bounded roster of durable
member identities. Population-scale capabilities, knowledge, resources, goals,
World compilation may seed collective populations and a bounded roster of durable
member identities, but that roster is not exhaustive. When an anonymous member
first becomes identity-bearing in play, the relevance planner may propose a new
member delta and the WorldKernel atomically creates that durable identity and its
active actor slot. Existing deltas are always preferred when they fit, preventing
the same person from being invented twice. Population-scale capabilities, knowledge, resources, goals,
and pressures belong to the gestalt. A member record owns identity, memories,
relationships, possessions, injuries, obligations, last known location, and
additions to or removals from the shared baseline. Materialization is a derived

File diff suppressed because one or more lines are too long

4
web/dist/index.html vendored
View file

@ -1,5 +1,5 @@
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>GhostlightDungeon</title> <script type="module" crossorigin src="/assets/index-DDHZWg6R.js"></script>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>GhostlightDungeon</title> <script type="module" crossorigin src="/assets/index-DSqcLvft.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Dt7lvyL0.css">
</head>
<body><header><p class="eyebrow">STARFIRE / PRIVATE VERSE</p><h1>GhostlightDungeon</h1><p id="status">Reading the world…</p></header><main>
@ -9,5 +9,5 @@
</div><div id="compiler-results"></div></section><section id="surface"></section>
<form id="composer"><label for="attempt">Describe what you attempt</label><textarea id="attempt" required></textarea><div><button type="submit">Assess</button><button type="button" id="wait">Wait one hour</button></div></form><pre id="receipt" hidden></pre>
<form id="destination-form"><label>Compile a destination without moving there</label><input name="origin_location_id" placeholder="Current location id" required><input name="destination" placeholder="Where do you want the world to extend?" required><button type="submit">Retrieve destination</button></form>
<section id="campaign-lab" hidden><h2>Campaign laboratory</h2><div id="campaign-list"></div><form id="fork-form"><input name="name" placeholder="Fork name" required><button type="submit">Fork selected campaign</button></form><form id="reset-form"><input name="name" placeholder="Reset branch name" required><button type="submit">Reset from approved seed</button></form><a href="/api/campaigns/export" download>Export selected CultCache .cc</a><details><summary>Operator inspector</summary><button type="button" id="load-operator">Load private state and receipts</button><pre id="operator-output"></pre></details></section>
<section id="campaign-lab" hidden><h2>Campaign laboratory</h2><div id="campaign-list"></div><form id="fork-form"><input name="name" placeholder="Fork name" required><button type="submit">Fork selected campaign</button></form><form id="reset-form"><input name="name" placeholder="Reset branch name" required><button type="submit">Reset from approved seed</button></form><a href="/api/campaigns/export" download>Export selected CultCache .cc</a><details><summary>Operator inspector</summary><button type="button" id="load-operator">Load private state and receipts</button><div id="operator-output"></div></details></section>
</main></body></html>

View file

@ -7,5 +7,5 @@
</div><div id="compiler-results"></div></section><section id="surface"></section>
<form id="composer"><label for="attempt">Describe what you attempt</label><textarea id="attempt" required></textarea><div><button type="submit">Assess</button><button type="button" id="wait">Wait one hour</button></div></form><pre id="receipt" hidden></pre>
<form id="destination-form"><label>Compile a destination without moving there</label><input name="origin_location_id" placeholder="Current location id" required><input name="destination" placeholder="Where do you want the world to extend?" required><button type="submit">Retrieve destination</button></form>
<section id="campaign-lab" hidden><h2>Campaign laboratory</h2><div id="campaign-list"></div><form id="fork-form"><input name="name" placeholder="Fork name" required><button type="submit">Fork selected campaign</button></form><form id="reset-form"><input name="name" placeholder="Reset branch name" required><button type="submit">Reset from approved seed</button></form><a href="/api/campaigns/export" download>Export selected CultCache .cc</a><details><summary>Operator inspector</summary><button type="button" id="load-operator">Load private state and receipts</button><pre id="operator-output"></pre></details></section>
<section id="campaign-lab" hidden><h2>Campaign laboratory</h2><div id="campaign-list"></div><form id="fork-form"><input name="name" placeholder="Fork name" required><button type="submit">Fork selected campaign</button></form><form id="reset-form"><input name="name" placeholder="Reset branch name" required><button type="submit">Reset from approved seed</button></form><a href="/api/campaigns/export" download>Export selected CultCache .cc</a><details><summary>Operator inspector</summary><button type="button" id="load-operator">Load private state and receipts</button><div id="operator-output"></div></details></section>
</main><script type="module" src="/src/main.ts"></script></body></html>

View file

@ -64,5 +64,5 @@ document.querySelector<HTMLButtonElement>("#wait")!.addEventListener("click", ()
destinationForm.addEventListener("submit",async event=>{event.preventDefault();const result=await compilerPost("/api/compiler/destination",Object.fromEntries(new FormData(destinationForm)));const preview=result.preview;receipt.hidden=false;receipt.textContent=JSON.stringify(preview,null,2);status.textContent="Destination preview compiled; topology is unchanged until approval.";const button=document.createElement("button");button.textContent="Approve destination";button.addEventListener("click",async()=>{await compilerPost(`/api/compiler/destination/approve/${result.preview_id}`,{});button.remove();await refresh();});destinationForm.append(button);});
document.querySelector<HTMLFormElement>("#fork-form")!.addEventListener("submit",async event=>{event.preventDefault();await compilerPost("/api/campaigns/fork",Object.fromEntries(new FormData(event.currentTarget as HTMLFormElement)));await refresh();});
document.querySelector<HTMLFormElement>("#reset-form")!.addEventListener("submit",async event=>{event.preventDefault();await compilerPost("/api/campaigns/reset",Object.fromEntries(new FormData(event.currentTarget as HTMLFormElement)));await refresh();});
document.querySelector<HTMLButtonElement>("#load-operator")!.addEventListener("click",async()=>{const response=await fetch("/api/operator");document.querySelector<HTMLElement>("#operator-output")!.textContent=JSON.stringify(await response.json(),null,2);});
document.querySelector<HTMLButtonElement>("#load-operator")!.addEventListener("click",async()=>{const response=await fetch("/api/operator");const surface=await response.json();renderEveSurface(surface,document.querySelector<HTMLElement>("#operator-output")!,{body:document.body,clientId:"ghostlight.operator",statusElement:status});});
void refresh();