Merge ghostlight-verse/verse-genesis: the canonical verse world is created from Aetheria's catalog
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
4eff650755
7 changed files with 740 additions and 15 deletions
|
|
@ -524,3 +524,66 @@ fn bin_and_ext_fields_aetheria_adds_at_higher_keys_are_ignored() {
|
|||
read_aetheria_catalog(FIXTURE).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
/// A faction record re-encoded with `change` applied to its decoded cells, for
|
||||
/// shapes `serde_json` cannot spell: a non-string map key, raw invalid bytes.
|
||||
fn with_cells(key: &str, change: impl FnOnce(&mut Vec<rmpv::Value>)) -> Vec<CultCacheEnvelope> {
|
||||
let mut rows = fixture_rows();
|
||||
let row = row_of(&mut rows, key);
|
||||
let rmpv::Value::Array(mut cells) =
|
||||
rmpv::decode::read_value(&mut row.payload.as_slice()).unwrap()
|
||||
else {
|
||||
panic!("not positional");
|
||||
};
|
||||
change(&mut cells);
|
||||
let mut payload = Vec::new();
|
||||
rmpv::encode::write_value(&mut payload, &rmpv::Value::Array(cells)).unwrap();
|
||||
row.payload = payload;
|
||||
rows
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_allegiance_key_that_is_not_a_string_is_malformed_by_key_and_field() {
|
||||
for key in [
|
||||
rmpv::Value::from(5),
|
||||
rmpv::Value::Nil,
|
||||
rmpv::Value::Binary(b"faction-adrasteia".to_vec()),
|
||||
] {
|
||||
let rows = with_cells("faction-brannoch", |cells| {
|
||||
cells[12] = rmpv::Value::Map(vec![(key, rmpv::Value::F64(0.5))]);
|
||||
});
|
||||
assert_eq!(
|
||||
read(&rows),
|
||||
Err(CatalogReadError::MalformedRecord {
|
||||
record: "faction-brannoch".into(),
|
||||
field: 12
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_that_is_not_utf8_is_malformed_by_key_and_field() {
|
||||
// The marker is overwritten in the encoded bytes with an invalid sequence of
|
||||
// the same length, since a decoded string cannot hold one.
|
||||
const MARKER: &str = "ZZZZ";
|
||||
for field in [1usize, 3] {
|
||||
let mut rows = with_cells("faction-brannoch", |cells| {
|
||||
cells[field] = rmpv::Value::from(MARKER);
|
||||
});
|
||||
let payload = &mut row_of(&mut rows, "faction-brannoch").payload;
|
||||
let at = payload
|
||||
.windows(MARKER.len())
|
||||
.position(|window| window == MARKER.as_bytes())
|
||||
.unwrap();
|
||||
payload[at..at + MARKER.len()].copy_from_slice(&[0xff, 0xfe, 0xfd, 0xfc]);
|
||||
assert_eq!(
|
||||
read(&rows),
|
||||
Err(CatalogReadError::MalformedRecord {
|
||||
record: "faction-brannoch".into(),
|
||||
field: field as u8
|
||||
}),
|
||||
"field {field}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
//! digest its verbs do not have.
|
||||
|
||||
use crate::SubjectKind;
|
||||
use crate::patch::{AffordanceKindName, EntityKind, RefKind, RoleSpec, kernel_speak_entry};
|
||||
use crate::patch::{AffordanceKindName, EntityKind, RefKind, Role, RoleSpec, kernel_speak_entry};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::BTreeMap;
|
||||
|
|
@ -63,10 +63,20 @@ impl GrammarVerb {
|
|||
/// Whether a declared role list is this verb's signature: same names, same
|
||||
/// referent kinds, same order.
|
||||
pub(crate) fn admits_roles(&self, roles: &[RoleSpec]) -> bool {
|
||||
self.roles.len() == roles.len()
|
||||
&& self.roles.iter().zip(roles).all(|(want, have)| {
|
||||
want.role == have.role.0 && want.referent.ref_kind() == have.kind
|
||||
self.role_specs() == roles
|
||||
}
|
||||
|
||||
/// This verb's signature as the kernel declares it: the one lowering of a
|
||||
/// grammar role, so declaring a verb and admitting a declaration cannot
|
||||
/// disagree.
|
||||
pub(crate) fn role_specs(&self) -> Vec<RoleSpec> {
|
||||
self.roles
|
||||
.iter()
|
||||
.map(|role| RoleSpec {
|
||||
role: Role(role.role.clone()),
|
||||
kind: role.referent.ref_kind(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -165,6 +175,11 @@ impl GrammarBinding {
|
|||
}
|
||||
}
|
||||
|
||||
/// Every verb, in kind order.
|
||||
pub(crate) fn verbs(&self) -> impl Iterator<Item = (&str, &GrammarVerb)> {
|
||||
self.verbs.iter().map(|(kind, verb)| (kind.as_str(), verb))
|
||||
}
|
||||
|
||||
pub(crate) fn verb(&self, kind: &str) -> Option<&GrammarVerb> {
|
||||
self.verbs.get(kind)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,8 +144,8 @@ pub use table::{
|
|||
};
|
||||
pub use vault::{VaultError, VaultEvidenceSource};
|
||||
pub use verse::{
|
||||
CALL_RECORD_SCHEMA, CallRecordError, CallRecordStore, ProviderCallRecord, RecordingInferencePort,
|
||||
ReplayInferencePort, RunField, RunId, RunStart,
|
||||
CALL_RECORD_SCHEMA, CallRecordError, CallRecordStore, CreateVerseWorldIntent, ProviderCallRecord,
|
||||
RecordingInferencePort, ReplayInferencePort, RunField, RunId, RunStart, VerseGenesisError,
|
||||
};
|
||||
use patch::{EdgeRecord, EntityRecord, LedgerDelta, ResolvedOp, ResolvedPatch};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ use super::{
|
|||
ConsumerId, CreateWorld, CreateWorldIntent, CreationReceipt, DecisionInvocation,
|
||||
DecisionOpportunity, Declaration, DraftHandle, EntityDeclaration, EntityKind, JurisdictionKey,
|
||||
KernelError, NewController, OperatorEvent, PatchAnswer, PrincipalCommandIntent, PrincipalId,
|
||||
Ref, SubjectDeclaration, SubjectKind, SubmitReceipt, SystemCapability, TickMinutes, WorldId,
|
||||
EntityId, Ref, SubjectDeclaration, SubjectKind, SubmitReceipt, SystemCapability, TickMinutes, WorldId,
|
||||
WorldKernel, WorldPatch, WorldPhase, WorldScaleIntentRef, WorldSnapshot, journal,
|
||||
patch::kernel_speak_grant, prepare_creation,
|
||||
verse::{CreateVerseWorldIntent, VerseGenesisError},
|
||||
};
|
||||
use crate::VerifiedPrincipalEvidence;
|
||||
use chrono::Utc;
|
||||
|
|
@ -18,6 +19,21 @@ use tokio::task::JoinHandle;
|
|||
/// other. A world that wants more declares them through the patch lane.
|
||||
const GENESIS_PLACE: &str = "commons";
|
||||
|
||||
/// The commons every genesis declares, the one room its first subjects share.
|
||||
pub(crate) fn commons_declaration() -> Declaration {
|
||||
Declaration::Entity(EntityDeclaration {
|
||||
handle: DraftHandle::new(GENESIS_PLACE),
|
||||
label: "The Commons".into(),
|
||||
kind: EntityKind::Place,
|
||||
container: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Where a genesis subject stands.
|
||||
pub(crate) fn commons_ref() -> Ref<EntityId> {
|
||||
Ref::Draft(DraftHandle::new(GENESIS_PLACE))
|
||||
}
|
||||
|
||||
const REQUEST_CAPACITY: usize = 32;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
@ -120,7 +136,7 @@ impl WorldMailbox {
|
|||
kind,
|
||||
controller,
|
||||
affordances: kernel_speak_grant(),
|
||||
position: Some(Ref::Draft(DraftHandle::new(GENESIS_PLACE))),
|
||||
position: Some(commons_ref()),
|
||||
import_key: None,
|
||||
})
|
||||
};
|
||||
|
|
@ -130,12 +146,7 @@ impl WorldMailbox {
|
|||
// jurisdiction's population target. The owner subject lands in
|
||||
// `Uncovered`, visible and reducing nothing.
|
||||
let mut declarations = vec![
|
||||
Declaration::Entity(EntityDeclaration {
|
||||
handle: DraftHandle::new(GENESIS_PLACE),
|
||||
label: "The Commons".into(),
|
||||
kind: EntityKind::Place,
|
||||
container: None,
|
||||
}),
|
||||
commons_declaration(),
|
||||
declare(
|
||||
"first-person",
|
||||
input.human_subject_label,
|
||||
|
|
@ -219,6 +230,20 @@ impl WorldMailbox {
|
|||
.map_err(mailbox_outcome)
|
||||
}
|
||||
|
||||
/// Creates the canonical Aetheria verse world through the same
|
||||
/// authenticated creation `create` uses.
|
||||
pub async fn create_verse(
|
||||
&self,
|
||||
intent: CreateVerseWorldIntent,
|
||||
principal: &VerifiedPrincipalEvidence,
|
||||
) -> Result<CreationReceipt, VerseGenesisError> {
|
||||
let principal_id = PrincipalId::new(principal.account_subject_hash());
|
||||
let input = intent.lower(principal_id.clone())?;
|
||||
Ok(self
|
||||
.create_authenticated(input, AuthenticatedCaller::verified_principal(principal_id))
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// A wall-clock read at the boundary a request crosses into the mailbox is
|
||||
/// ingress, not reduce: the reducer stays deterministic over its journal,
|
||||
/// and this is the one place a caller's own claim of continued authority
|
||||
|
|
|
|||
143
crates/ghostlight/src/verse/genesis.rs
Normal file
143
crates/ghostlight/src/verse/genesis.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
//! The canonical Aetheria verse world's genesis.
|
||||
//!
|
||||
//! One intent lowers to one `CreateWorld`: a commons, every grammar verb
|
||||
//! declared as an affordance, and each Aetheria faction as an institution
|
||||
//! imported by its record key. The world is bound to the catalog's grammar, so
|
||||
//! the kernel refuses any affordance the grammar does not carry.
|
||||
//!
|
||||
//! Phase A verbs record the act and change no component: an outcome band with
|
||||
//! no effects, and the act carried as the utterance that makes an entry
|
||||
//! admissible. Ghostlight owns each verb's lowering to kernel effects (ruling
|
||||
//! `verb-effects-ghostlight`), and those arrive with phase B.
|
||||
|
||||
use crate::aetheria::AetheriaCatalog;
|
||||
use crate::mailbox::{MailboxError, commons_declaration, commons_ref};
|
||||
use crate::patch::{
|
||||
AffordanceDeclaration, AffordanceKindName, AudienceSpec, Declaration, DraftHandle, EvidenceRef,
|
||||
FactDeclaration, FactStandingRef, OutcomeBand, Precondition, Ref, Statement, SubjectDeclaration,
|
||||
kernel_speak_entry, kernel_speak_grant,
|
||||
};
|
||||
use crate::{
|
||||
CommandId, CreateWorld, LensWeights, NewController, PrincipalId, SubjectKind,
|
||||
WorldPatch, WorldScaleIntentRef,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
const COMMIT_LEN: usize = 40;
|
||||
|
||||
/// What a caller states to create the canonical verse world. Everything else a
|
||||
/// world needs (the commons, the subjects, the affordances, their grants and
|
||||
/// the provenance of what is known) is derived from the catalog.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CreateVerseWorldIntent {
|
||||
pub id: CommandId,
|
||||
pub title: String,
|
||||
pub brief: String,
|
||||
pub catalog: AetheriaCatalog,
|
||||
/// The Aetheria commit the catalog was read at: 40 lower-case hex digits.
|
||||
/// It is the provenance of every fact the world holds about a faction.
|
||||
pub aetheria_commit: String,
|
||||
pub lens_weights: LensWeights,
|
||||
pub targets: BTreeMap<SubjectKind, u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum VerseGenesisError {
|
||||
#[error("aetheria_commit is not 40 lower-case hex digits")]
|
||||
NotACommit,
|
||||
#[error(transparent)]
|
||||
Mailbox(#[from] MailboxError),
|
||||
}
|
||||
|
||||
impl CreateVerseWorldIntent {
|
||||
/// The one lowering. The kernel supplies the `speak` entry at genesis and
|
||||
/// holds it to the grammar, so it is granted here and never declared.
|
||||
pub(crate) fn lower(self, owner: PrincipalId) -> Result<CreateWorld, VerseGenesisError> {
|
||||
let commit = &self.aetheria_commit;
|
||||
if commit.len() != COMMIT_LEN
|
||||
|| !commit
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
|
||||
{
|
||||
return Err(VerseGenesisError::NotACommit);
|
||||
}
|
||||
|
||||
let speak = kernel_speak_entry().kind;
|
||||
let mut grants = kernel_speak_grant();
|
||||
let mut declarations = vec![commons_declaration()];
|
||||
for (kind, verb) in self.catalog.grammar.verbs() {
|
||||
if kind == speak.0 {
|
||||
continue;
|
||||
}
|
||||
let handle = DraftHandle::new(format!("verb-{kind}"));
|
||||
grants.insert(Ref::Draft(handle.clone()));
|
||||
declarations.push(Declaration::Affordance(AffordanceDeclaration {
|
||||
handle,
|
||||
kind: AffordanceKindName(kind.to_owned()),
|
||||
roles: verb.role_specs(),
|
||||
preconditions: vec![Precondition::CanBroadcast {
|
||||
via: AudienceSpec::Colocated,
|
||||
}],
|
||||
effect_slots: Vec::new(),
|
||||
outcome_bands: vec![OutcomeBand {
|
||||
weight: 1,
|
||||
effects: Vec::new(),
|
||||
}],
|
||||
// The kernel refuses an entry that can neither change a
|
||||
// component nor speak, and a speaking entry needs an audience,
|
||||
// so a phase A verb is its own utterance to the room: the act
|
||||
// is recorded as speech and nothing else changes.
|
||||
carries_speech: true,
|
||||
}));
|
||||
}
|
||||
|
||||
let mut evidence = Vec::new();
|
||||
for (position, faction) in self.catalog.factions.iter().enumerate() {
|
||||
declarations.push(Declaration::Subject(SubjectDeclaration {
|
||||
handle: DraftHandle::new(format!("faction-{position}")),
|
||||
label: faction.name.trim().to_owned(),
|
||||
kind: SubjectKind::Institution,
|
||||
controller: NewController::OperationalAgent,
|
||||
affordances: grants.clone(),
|
||||
position: Some(commons_ref()),
|
||||
import_key: Some(faction.key.clone()),
|
||||
}));
|
||||
// A faction with no description has nothing to state.
|
||||
if let Some(statement) = Statement::new(faction.description.trim()) {
|
||||
let receipt = EvidenceRef::new(format!(
|
||||
"aetheria:{commit}:{}/{}",
|
||||
faction.key.schema(),
|
||||
faction.key.record()
|
||||
));
|
||||
evidence.push(receipt.clone());
|
||||
declarations.push(Declaration::Fact(FactDeclaration {
|
||||
handle: DraftHandle::new(format!("fact-{position}")),
|
||||
label: faction.name.trim().to_owned(),
|
||||
statement,
|
||||
standing: FactStandingRef::Canonical { evidence: receipt },
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CreateWorld {
|
||||
id: self.id,
|
||||
owner,
|
||||
title: self.title,
|
||||
brief: self.brief,
|
||||
patch: WorldPatch {
|
||||
declarations,
|
||||
operations: Vec::new(),
|
||||
evidence,
|
||||
},
|
||||
scale_intent: WorldScaleIntentRef {
|
||||
targets: self.targets,
|
||||
jurisdictions: BTreeMap::new(),
|
||||
},
|
||||
lens_weights: self.lens_weights,
|
||||
grammar: Some(self.catalog.grammar),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
476
crates/ghostlight/src/verse/genesis/tests.rs
Normal file
476
crates/ghostlight/src/verse/genesis/tests.rs
Normal file
|
|
@ -0,0 +1,476 @@
|
|||
use super::*;
|
||||
use crate::aetheria::read_aetheria_catalog;
|
||||
use crate::patch::{FactStanding, Mismatch};
|
||||
use crate::tests::{auth_principal, owner, stock_weights};
|
||||
use crate::{
|
||||
CommandBody, ControllerMode, GrammarBinding, GrammarReferentKind, GrammarRole, GrammarVerb,
|
||||
KernelError, PrincipalCommandIntent, RenderPath, VerifiedPrincipalEvidence, WorldKernel,
|
||||
WorldMailbox, WorldPhase,
|
||||
};
|
||||
use chrono::{Duration, Utc};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
/// Aetheria master when the fixture catalog was written.
|
||||
const COMMIT: &str = "92b82e9aa5bb422c568c4bfc85a8f641426a41fc";
|
||||
const OTHER_COMMIT: &str = "0123456789abcdef0123456789abcdef01234567";
|
||||
|
||||
const FIXTURE: &str = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/fixtures/aetheria-verse-catalog.cc"
|
||||
);
|
||||
|
||||
fn catalog() -> AetheriaCatalog {
|
||||
read_aetheria_catalog(FIXTURE).unwrap()
|
||||
}
|
||||
|
||||
fn intent_of(catalog: AetheriaCatalog, commit: &str) -> CreateVerseWorldIntent {
|
||||
CreateVerseWorldIntent {
|
||||
id: CommandId::new(),
|
||||
title: "The Verse".into(),
|
||||
brief: "Aetheria.".into(),
|
||||
catalog,
|
||||
aetheria_commit: commit.into(),
|
||||
lens_weights: stock_weights(),
|
||||
targets: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A world created through the kernel from the lowering, for what the
|
||||
/// snapshot hides: the facts' evidence.
|
||||
fn kernel_of(intent: CreateVerseWorldIntent) -> (tempfile::TempDir, Result<WorldKernel, KernelError>) {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let world = intent.lower(owner()).unwrap();
|
||||
let result = WorldKernel::create(dir.path().join("world.cc"), world, &auth_principal(owner()))
|
||||
.map(|(kernel, _)| kernel);
|
||||
(dir, result)
|
||||
}
|
||||
|
||||
struct Verse {
|
||||
_dir: tempfile::TempDir,
|
||||
_task: tokio::task::JoinHandle<()>,
|
||||
mailbox: WorldMailbox,
|
||||
principal: VerifiedPrincipalEvidence,
|
||||
receipt: crate::CreationReceipt,
|
||||
}
|
||||
|
||||
async fn verse(intent: CreateVerseWorldIntent) -> Verse {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let (mailbox, task) = WorldMailbox::open(dir.path().join("world.cc")).unwrap();
|
||||
let principal =
|
||||
VerifiedPrincipalEvidence::fixture("verse-owner", Utc::now() + Duration::hours(1));
|
||||
let receipt = mailbox.create_verse(intent, &principal).await.unwrap();
|
||||
Verse {
|
||||
_dir: dir,
|
||||
_task: task,
|
||||
mailbox,
|
||||
principal,
|
||||
receipt,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn verse_genesis_from_the_aetheria_fixture() {
|
||||
let catalog = catalog();
|
||||
let verse = verse(intent_of(catalog.clone(), COMMIT)).await;
|
||||
let snapshot = verse.mailbox.snapshot().await.unwrap();
|
||||
|
||||
// One institution per faction, keyed by its record, named by its Name.
|
||||
let mut factions: Vec<_> = snapshot
|
||||
.subjects
|
||||
.iter()
|
||||
.map(|subject| {
|
||||
(
|
||||
subject.import_key.clone().unwrap(),
|
||||
subject.label.clone(),
|
||||
subject.kind,
|
||||
subject.controller_mode,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
factions.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
let expected: Vec<_> = catalog
|
||||
.factions
|
||||
.iter()
|
||||
.map(|faction| {
|
||||
(
|
||||
faction.key.clone(),
|
||||
faction.name.clone(),
|
||||
SubjectKind::Institution,
|
||||
Some(ControllerMode::OperationalAgent),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(factions.len(), 2);
|
||||
assert_eq!(factions, expected);
|
||||
|
||||
// One fact per faction, carrying its description.
|
||||
let statements: BTreeSet<_> = snapshot
|
||||
.facts
|
||||
.iter()
|
||||
.map(|fact| fact.statement.as_str().to_owned())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
statements,
|
||||
catalog
|
||||
.factions
|
||||
.iter()
|
||||
.map(|faction| faction.description.clone())
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
|
||||
// The world is bound to the catalog's grammar.
|
||||
assert_eq!(snapshot.grammar, Some(catalog.grammar.identity()));
|
||||
assert_eq!(verse.receipt.world_id, snapshot.world_id);
|
||||
|
||||
// Every faction stands in the one commons.
|
||||
let commons: Vec<_> = snapshot
|
||||
.places
|
||||
.iter()
|
||||
.filter(|place| place.label == "The Commons")
|
||||
.collect();
|
||||
assert_eq!(commons.len(), 1);
|
||||
assert!(
|
||||
snapshot
|
||||
.subjects
|
||||
.iter()
|
||||
.all(|subject| subject.position == Some(commons[0].id))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_fact_cites_its_faction_record_and_the_commit_it_was_read_at() {
|
||||
for commit in [COMMIT, OTHER_COMMIT] {
|
||||
let (_dir, kernel) = kernel_of(intent_of(catalog(), commit));
|
||||
let kernel = kernel.unwrap();
|
||||
let evidence: BTreeSet<String> = kernel
|
||||
.state
|
||||
.facts
|
||||
.values()
|
||||
.map(|fact| match &fact.standing {
|
||||
FactStanding::Canonical { evidence } => evidence.text().to_owned(),
|
||||
other => panic!("a verse fact is canonical, not {other:?}"),
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
evidence,
|
||||
catalog()
|
||||
.factions
|
||||
.iter()
|
||||
.map(|faction| format!(
|
||||
"aetheria:{commit}:aetheria.faction/{}",
|
||||
faction.key.record()
|
||||
))
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_commit_is_forty_lower_case_hex_digits() {
|
||||
for bad in [
|
||||
"",
|
||||
&COMMIT[..39],
|
||||
&format!("{COMMIT}0"),
|
||||
&COMMIT.to_uppercase(),
|
||||
&format!("{}g", &COMMIT[..39]),
|
||||
] {
|
||||
assert!(
|
||||
matches!(
|
||||
intent_of(catalog(), bad).lower(owner()),
|
||||
Err(VerseGenesisError::NotACommit)
|
||||
),
|
||||
"{bad:?}"
|
||||
);
|
||||
}
|
||||
assert!(intent_of(catalog(), COMMIT).lower(owner()).is_ok());
|
||||
assert!(!VerseGenesisError::NotACommit.to_string().contains(COMMIT));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verse_genesis_refuses_a_duplicate_faction_record() {
|
||||
let mut twice = catalog();
|
||||
let first = twice.factions[0].clone();
|
||||
twice.factions.push(first);
|
||||
// The lowering does not decide: it declares all three.
|
||||
let world = intent_of(twice.clone(), COMMIT).lower(owner()).unwrap();
|
||||
let subjects = world
|
||||
.patch
|
||||
.declarations
|
||||
.iter()
|
||||
.filter(|declaration| matches!(declaration, Declaration::Subject(_)))
|
||||
.count();
|
||||
assert_eq!(subjects, 3);
|
||||
|
||||
let (_dir, kernel) = kernel_of(intent_of(twice, COMMIT));
|
||||
let Err(KernelError::PatchRejected(set)) = kernel else {
|
||||
panic!("a second faction under one record key was admitted");
|
||||
};
|
||||
assert!(
|
||||
set.iter()
|
||||
.any(|mismatch| matches!(mismatch, Mismatch::DuplicateImportKey { .. })),
|
||||
"{set:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn every_granted_affordance_is_a_grammar_verb() {
|
||||
let catalog = catalog();
|
||||
let verse = verse(intent_of(catalog.clone(), COMMIT)).await;
|
||||
let snapshot = verse.mailbox.snapshot().await.unwrap();
|
||||
|
||||
let mut kinds: Vec<_> = snapshot
|
||||
.affordances
|
||||
.iter()
|
||||
.map(|affordance| affordance.entry.kind.0.clone())
|
||||
.collect();
|
||||
kinds.sort();
|
||||
// Each verb once: the kernel's speak is the grammar's speak.
|
||||
assert_eq!(
|
||||
kinds,
|
||||
catalog
|
||||
.grammar
|
||||
.verbs()
|
||||
.map(|(kind, _)| kind.to_owned())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
assert!(kinds.contains(&"speak".to_owned()));
|
||||
|
||||
// Each has its render path from the grammar, and phase A changes nothing.
|
||||
for affordance in &snapshot.affordances {
|
||||
let verb = catalog.grammar.verb(&affordance.entry.kind.0).unwrap();
|
||||
assert_eq!(affordance.render_path, Some(verb.render_path));
|
||||
assert!(affordance.entry.effect_slots.is_empty());
|
||||
assert_eq!(affordance.entry.outcome_bands.len(), 1);
|
||||
assert!(affordance.entry.outcome_bands[0].effects.is_empty());
|
||||
}
|
||||
|
||||
// Every faction is granted every verb.
|
||||
let all: BTreeSet<_> = snapshot.affordances.iter().map(|a| a.id).collect();
|
||||
for subject in &snapshot.subjects {
|
||||
assert_eq!(subject.affordances, all);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn factions_get_opportunities() {
|
||||
let verse = verse(intent_of(catalog(), COMMIT)).await;
|
||||
for body in [CommandBody::ApproveDraft, CommandBody::ActivateWorld] {
|
||||
let revision = verse.mailbox.snapshot().await.unwrap().revision;
|
||||
verse
|
||||
.mailbox
|
||||
.submit_principal(
|
||||
PrincipalCommandIntent {
|
||||
id: CommandId::new(),
|
||||
world_id: verse.receipt.world_id,
|
||||
expected_revision: revision,
|
||||
body,
|
||||
},
|
||||
&verse.principal,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
let snapshot = verse.mailbox.snapshot().await.unwrap();
|
||||
assert_eq!(snapshot.phase, WorldPhase::Active);
|
||||
assert_eq!(snapshot.opportunities.len(), catalog().factions.len());
|
||||
let scopes: BTreeSet<_> = snapshot.opportunities.iter().map(|o| o.scope).collect();
|
||||
assert_eq!(scopes.len(), snapshot.opportunities.len());
|
||||
for opportunity in &snapshot.opportunities {
|
||||
assert_eq!(opportunity.controller_mode, ControllerMode::OperationalAgent);
|
||||
assert_eq!(opportunity.affordance_ids.len(), snapshot.affordances.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_faction_without_a_description_holds_no_fact() {
|
||||
let mut bare = catalog();
|
||||
bare.factions[0].description = " ".into();
|
||||
bare.factions[1].description = " Second. ".into();
|
||||
let (_dir, kernel) = kernel_of(intent_of(bare, COMMIT));
|
||||
let facts: Vec<_> = kernel
|
||||
.unwrap()
|
||||
.state
|
||||
.facts
|
||||
.values()
|
||||
.map(|fact| fact.statement.as_str().to_owned())
|
||||
.collect();
|
||||
assert_eq!(facts, vec!["Second.".to_owned()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_grammar_the_kernel_speak_cannot_live_in_refuses_genesis() {
|
||||
let mut mute = catalog();
|
||||
mute.grammar = GrammarBinding::new(
|
||||
"aetheria.verse_grammar",
|
||||
1,
|
||||
[(
|
||||
"haul".to_owned(),
|
||||
GrammarVerb {
|
||||
render_path: RenderPath::ShipAction,
|
||||
roles: vec![GrammarRole {
|
||||
role: "cargo".into(),
|
||||
referent: GrammarReferentKind::Cargo,
|
||||
}],
|
||||
},
|
||||
)],
|
||||
)
|
||||
.unwrap();
|
||||
let (_dir, kernel) = kernel_of(intent_of(mute, COMMIT));
|
||||
let Err(KernelError::PatchRejected(set)) = kernel else {
|
||||
panic!("a world whose grammar has no speak was admitted");
|
||||
};
|
||||
assert!(
|
||||
set.iter()
|
||||
.any(|mismatch| matches!(mismatch, Mismatch::AffordanceKindNotInGrammar { .. })),
|
||||
"{set:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn the_dungeon_ingress_still_creates_an_unbound_world() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let (mailbox, _task) = WorldMailbox::open(dir.path().join("world.cc")).unwrap();
|
||||
let principal =
|
||||
VerifiedPrincipalEvidence::fixture("dungeon-owner", Utc::now() + Duration::hours(1));
|
||||
mailbox
|
||||
.create(
|
||||
crate::CreateWorldIntent {
|
||||
id: CommandId::new(),
|
||||
title: "Dungeon".into(),
|
||||
brief: String::new(),
|
||||
human_subject_label: "Player".into(),
|
||||
narrative_persona_label: None,
|
||||
operational_agent_label: None,
|
||||
targets: BTreeMap::new(),
|
||||
jurisdictions: Vec::new(),
|
||||
lens_weights: stock_weights(),
|
||||
},
|
||||
&principal,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let snapshot = mailbox.snapshot().await.unwrap();
|
||||
assert_eq!(snapshot.grammar, None);
|
||||
assert!(
|
||||
snapshot
|
||||
.places
|
||||
.iter()
|
||||
.any(|place| place.label == "The Commons")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_faction_name_is_trimmed_into_its_institution_label() {
|
||||
let mut padded = catalog();
|
||||
let expected = padded.factions[0].name.trim().to_owned();
|
||||
padded.factions[0].name = format!(" {expected}\t ");
|
||||
let key = padded.factions[0].key.clone();
|
||||
let verse = verse(intent_of(padded, COMMIT)).await;
|
||||
let snapshot = verse.mailbox.snapshot().await.unwrap();
|
||||
let subject = snapshot
|
||||
.subjects
|
||||
.iter()
|
||||
.find(|subject| subject.import_key.as_ref() == Some(&key))
|
||||
.expect("the padded faction was created");
|
||||
assert_eq!(subject.label, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn genesis_twice_with_one_intent_is_one_creation() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let (mailbox, _task) = WorldMailbox::open(dir.path().join("world.cc")).unwrap();
|
||||
let principal =
|
||||
VerifiedPrincipalEvidence::fixture("verse-owner", Utc::now() + Duration::hours(1));
|
||||
let intent = intent_of(catalog(), COMMIT);
|
||||
let first = mailbox.create_verse(intent.clone(), &principal).await.unwrap();
|
||||
let before = mailbox.snapshot().await.unwrap();
|
||||
let again = mailbox.create_verse(intent, &principal).await.unwrap();
|
||||
let after = mailbox.snapshot().await.unwrap();
|
||||
assert_eq!(again, first);
|
||||
assert_eq!(after.revision, before.revision);
|
||||
assert_eq!(after.state_digest, before.state_digest);
|
||||
assert_eq!(after.subjects.len(), before.subjects.len());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn genesis_under_another_command_id_finds_the_world_occupied() {
|
||||
let verse = verse(intent_of(catalog(), COMMIT)).await;
|
||||
let before = verse.mailbox.snapshot().await.unwrap();
|
||||
let refused = verse
|
||||
.mailbox
|
||||
.create_verse(intent_of(catalog(), COMMIT), &verse.principal)
|
||||
.await;
|
||||
assert!(
|
||||
matches!(
|
||||
refused,
|
||||
Err(VerseGenesisError::Mailbox(MailboxError::Kernel(
|
||||
KernelError::CreationTargetOccupied
|
||||
)))
|
||||
),
|
||||
"{refused:?}"
|
||||
);
|
||||
let after = verse.mailbox.snapshot().await.unwrap();
|
||||
assert_eq!(after.revision, before.revision);
|
||||
assert_eq!(after.state_digest, before.state_digest);
|
||||
}
|
||||
|
||||
/// Evidence for the open question `ship-acts-are-not-speech`, pinning what the
|
||||
/// phase A lowering does today: a ship verb is only admissible as an utterance,
|
||||
/// and committing it asserts a claim fact the room hears.
|
||||
#[test]
|
||||
fn phase_a_ship_verbs_are_spoken_acts_in_the_room() {
|
||||
use crate::tests::{command, opportunity_for, spoken, submit_owner};
|
||||
use crate::{
|
||||
AuthenticatedCaller, CallerId, DecisionInvocation, Role, RoleBinding, SubmitReceipt,
|
||||
Target,
|
||||
};
|
||||
|
||||
let (_scratch, kernel) = kernel_of(intent_of(catalog(), COMMIT));
|
||||
let mut kernel = kernel.unwrap();
|
||||
for body in [CommandBody::ApproveDraft, CommandBody::ActivateWorld] {
|
||||
let snapshot = kernel.snapshot().unwrap();
|
||||
submit_owner(&mut kernel, &snapshot, body);
|
||||
}
|
||||
let active = kernel.snapshot().unwrap();
|
||||
let attack = active
|
||||
.affordances
|
||||
.iter()
|
||||
.find(|a| a.entry.kind.0 == "attack")
|
||||
.unwrap()
|
||||
.id;
|
||||
let [first, second] = [&active.subjects[0], &active.subjects[1]];
|
||||
let opportunity = opportunity_for(&active, first.id);
|
||||
let caller = CallerId::Controller(opportunity.controller_id);
|
||||
let invocation = |speech: Option<&str>| DecisionInvocation {
|
||||
affordance: attack,
|
||||
bindings: vec![RoleBinding {
|
||||
role: Role("target".into()),
|
||||
target: Target::Subject(second.id),
|
||||
}],
|
||||
proposed: Vec::new(),
|
||||
speech: speech.map(|text| crate::patch::Statement::new(text).unwrap()),
|
||||
display: None,
|
||||
};
|
||||
let try_it = |kernel: &mut WorldKernel, speech: Option<&str>| {
|
||||
kernel.submit(
|
||||
command(
|
||||
&active,
|
||||
CommandId::new(),
|
||||
caller.clone(),
|
||||
CommandBody::ExerciseDecision {
|
||||
opportunity: opportunity.clone(),
|
||||
invocation: invocation(speech),
|
||||
},
|
||||
),
|
||||
&AuthenticatedCaller::fixture(caller.clone()),
|
||||
)
|
||||
};
|
||||
|
||||
// Without an utterance the act is refused: the verb has no other way in.
|
||||
assert!(try_it(&mut kernel, None).is_err());
|
||||
// With one, the act commits and its words are a claim fact.
|
||||
let receipt = try_it(&mut kernel, Some("We strike.")).expect("the attack commits");
|
||||
assert!(matches!(receipt, SubmitReceipt::Applied(_)));
|
||||
let event = kernel.state.events.last().unwrap();
|
||||
assert_eq!(spoken(&kernel.state, event), Some("We strike."));
|
||||
}
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
//! The Aetheria verse run's own organs. The verse binary and its run record
|
||||
//! live above this module; here sit the parts of a run that belong to the
|
||||
//! library: the record of every provider call and the spend it implies.
|
||||
//! library: the record of every provider call and the spend it implies, and the
|
||||
//! genesis of the canonical verse world.
|
||||
|
||||
mod calls;
|
||||
mod genesis;
|
||||
|
||||
pub use genesis::{CreateVerseWorldIntent, VerseGenesisError};
|
||||
pub use calls::{
|
||||
CALL_RECORD_SCHEMA, CallRecordError, CallRecordStore, ProviderCallRecord,
|
||||
RecordingInferencePort, ReplayInferencePort, RunField, RunId, RunStart,
|
||||
|
|
|
|||
Loading…
Reference in a new issue