use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use super::canonical::{self, CanonicalError};
use super::types::{Component, Provenance, Timestamp};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum EventKind {
ObservationRecorded,
ExperienceCreated,
ContextCompressed,
GroundingFailed,
MemoryConsolidated,
BeliefUpdated,
SkillLearned,
ReflectionCompleted,
MutationProposed,
SimulationCompleted,
FitnessMeasured,
MutationAccepted,
MutationRejected,
GenomeCommitted,
}
impl EventKind {
pub fn as_str(self) -> &'static str {
match self {
EventKind::ObservationRecorded => "ObservationRecorded",
EventKind::ExperienceCreated => "ExperienceCreated",
EventKind::ContextCompressed => "ContextCompressed",
EventKind::GroundingFailed => "GroundingFailed",
EventKind::MemoryConsolidated => "MemoryConsolidated",
EventKind::BeliefUpdated => "BeliefUpdated",
EventKind::SkillLearned => "SkillLearned",
EventKind::ReflectionCompleted => "ReflectionCompleted",
EventKind::MutationProposed => "MutationProposed",
EventKind::SimulationCompleted => "SimulationCompleted",
EventKind::FitnessMeasured => "FitnessMeasured",
EventKind::MutationAccepted => "MutationAccepted",
EventKind::MutationRejected => "MutationRejected",
EventKind::GenomeCommitted => "GenomeCommitted",
}
}
pub fn parse(value: &str) -> Option<Self> {
Some(match value {
"ObservationRecorded" => EventKind::ObservationRecorded,
"ExperienceCreated" => EventKind::ExperienceCreated,
"ContextCompressed" => EventKind::ContextCompressed,
"GroundingFailed" => EventKind::GroundingFailed,
"MemoryConsolidated" => EventKind::MemoryConsolidated,
"BeliefUpdated" => EventKind::BeliefUpdated,
"SkillLearned" => EventKind::SkillLearned,
"ReflectionCompleted" => EventKind::ReflectionCompleted,
"MutationProposed" => EventKind::MutationProposed,
"SimulationCompleted" => EventKind::SimulationCompleted,
"FitnessMeasured" => EventKind::FitnessMeasured,
"MutationAccepted" => EventKind::MutationAccepted,
"MutationRejected" => EventKind::MutationRejected,
"GenomeCommitted" => EventKind::GenomeCommitted,
_ => return None,
})
}
pub fn emitter(self) -> Component {
match self {
EventKind::ObservationRecorded
| EventKind::ExperienceCreated
| EventKind::SimulationCompleted
| EventKind::FitnessMeasured => Component::Eve,
EventKind::ContextCompressed | EventKind::GroundingFailed => Component::Axiom,
EventKind::MemoryConsolidated
| EventKind::BeliefUpdated
| EventKind::SkillLearned
| EventKind::ReflectionCompleted
| EventKind::MutationProposed
| EventKind::MutationAccepted
| EventKind::MutationRejected
| EventKind::GenomeCommitted => Component::Adam,
}
}
pub const ALL: [EventKind; 14] = [
EventKind::ObservationRecorded,
EventKind::ExperienceCreated,
EventKind::ContextCompressed,
EventKind::GroundingFailed,
EventKind::MemoryConsolidated,
EventKind::BeliefUpdated,
EventKind::SkillLearned,
EventKind::ReflectionCompleted,
EventKind::MutationProposed,
EventKind::SimulationCompleted,
EventKind::FitnessMeasured,
EventKind::MutationAccepted,
EventKind::MutationRejected,
EventKind::GenomeCommitted,
];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SubjectType {
Identity,
Genome,
Capability,
Belief,
Memory,
Skill,
Mutation,
Reflection,
Observation,
Experience,
FitnessResult,
Context,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PayloadValue {
Bool(bool),
Int(i64),
Text(String),
}
impl From<bool> for PayloadValue {
fn from(v: bool) -> Self {
PayloadValue::Bool(v)
}
}
impl From<i64> for PayloadValue {
fn from(v: i64) -> Self {
PayloadValue::Int(v)
}
}
impl From<u32> for PayloadValue {
fn from(v: u32) -> Self {
PayloadValue::Int(i64::from(v))
}
}
impl From<&str> for PayloadValue {
fn from(v: &str) -> Self {
PayloadValue::Text(v.to_string())
}
}
impl From<String> for PayloadValue {
fn from(v: String) -> Self {
PayloadValue::Text(v)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Event {
pub cp: String,
#[serde(rename = "type")]
pub kind: EventKind,
pub id: String,
pub occurred_at: Timestamp,
pub actor: Component,
pub subject_id: String,
pub subject_type: SubjectType,
pub correlation_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub causation_id: Option<String>,
pub payload: BTreeMap<String, PayloadValue>,
pub provenance: Provenance,
}
impl Event {
pub fn try_new(
kind: EventKind,
subject_id: impl Into<String>,
subject_type: SubjectType,
correlation_id: impl Into<String>,
payload: BTreeMap<String, PayloadValue>,
provenance: Provenance,
) -> Result<Self, EventError> {
if provenance.authored_by != kind.emitter() {
return Err(EventError::EmitterMismatch {
kind,
expected: kind.emitter(),
found: provenance.authored_by,
});
}
Ok(Self::new(
kind,
subject_id,
subject_type,
correlation_id,
payload,
provenance,
))
}
pub fn new(
kind: EventKind,
subject_id: impl Into<String>,
subject_type: SubjectType,
correlation_id: impl Into<String>,
payload: BTreeMap<String, PayloadValue>,
provenance: Provenance,
) -> Self {
Self {
cp: "cp1".to_string(),
kind,
id: uuid::Uuid::new_v4().to_string(),
occurred_at: Timestamp::now(),
actor: kind.emitter(),
subject_id: subject_id.into(),
subject_type,
correlation_id: correlation_id.into(),
causation_id: None,
payload,
provenance,
}
}
pub fn caused_by(mut self, event_id: impl Into<String>) -> Self {
self.causation_id = Some(event_id.into());
self
}
pub fn seal(&self) -> Result<serde_json::Value, CanonicalError> {
let mut value = serde_json::to_value(self).expect("Event always serializes");
canonical::seal(&mut value)?;
Ok(value)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EventError {
EmitterMismatch {
kind: EventKind,
expected: Component,
found: Component,
},
}
impl std::fmt::Display for EventError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EventError::EmitterMismatch {
kind,
expected,
found,
} => write!(
f,
"{} may only be emitted by {}, but the provenance is authored by {}",
kind.as_str(),
expected.as_str(),
found.as_str()
),
}
}
}
impl std::error::Error for EventError {}
pub trait EventSink: Send + Sync {
fn emit(&self, event: &Event);
}
#[derive(Debug, Default)]
pub struct RecordingSink {
events: std::sync::Mutex<Vec<Event>>,
}
impl RecordingSink {
pub fn new() -> Self {
Self::default()
}
pub fn events(&self) -> Vec<Event> {
self.events
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone()
}
pub fn kinds(&self) -> Vec<EventKind> {
self.events().iter().map(|e| e.kind).collect()
}
}
impl EventSink for RecordingSink {
fn emit(&self, event: &Event) {
self.events
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.push(event.clone());
}
}
#[cfg(test)]
mod tests {
use super::*;
fn payload(pairs: &[(&str, PayloadValue)]) -> BTreeMap<String, PayloadValue> {
pairs
.iter()
.map(|(k, v)| ((*k).to_string(), v.clone()))
.collect()
}
#[test]
fn every_event_name_round_trips() {
for kind in EventKind::ALL {
assert_eq!(EventKind::parse(kind.as_str()), Some(kind));
}
}
#[test]
fn event_names_serialize_as_bare_strings() {
let json = serde_json::to_string(&EventKind::GenomeCommitted).unwrap();
assert_eq!(json, "\"GenomeCommitted\"");
}
#[test]
fn unknown_event_names_are_rejected() {
assert_eq!(EventKind::parse("GenomeDeleted"), None);
}
#[test]
fn actor_defaults_to_the_owning_emitter() {
let event = Event::new(
EventKind::ContextCompressed,
"dddddddd-dddd-4ddd-8ddd-dddddddddddd",
SubjectType::Context,
"0f0f0f0f-0f0f-4f0f-8f0f-0f0f0f0f0f0f",
payload(&[("tokens_after", PayloadValue::Int(3780))]),
Provenance::now(Component::Axiom, "axiom:context/compress"),
);
assert_eq!(event.actor, Component::Axiom);
assert_eq!(EventKind::ContextCompressed.emitter(), Component::Axiom);
}
#[test]
fn emitter_assignment_covers_all_three_components() {
let emitters: std::collections::BTreeSet<&str> = EventKind::ALL
.iter()
.map(|k| k.emitter().as_str())
.collect();
assert_eq!(
emitters,
["adam", "axiom", "eve"].into_iter().collect::<std::collections::BTreeSet<_>>()
);
}
#[test]
fn a_sealed_event_verifies_and_omits_absent_causation() {
let event = Event::new(
EventKind::GroundingFailed,
"dddddddd-dddd-4ddd-8ddd-dddddddddddd",
SubjectType::Context,
"0f0f0f0f-0f0f-4f0f-8f0f-0f0f0f0f0f0f",
payload(&[("claim", PayloadValue::Text("unsupported".into()))]),
Provenance::now(Component::Axiom, "axiom:grounding/verify"),
);
let sealed = event.seal().unwrap();
assert!(canonical::verify_seal(&sealed).unwrap());
assert!(
sealed.get("causation_id").is_none(),
"an absent causation must be an absent key, never a null"
);
}
#[test]
fn causation_is_recorded_when_set() {
let event = Event::new(
EventKind::ContextCompressed,
"dddddddd-dddd-4ddd-8ddd-dddddddddddd",
SubjectType::Context,
"0f0f0f0f-0f0f-4f0f-8f0f-0f0f0f0f0f0f",
BTreeMap::new(),
Provenance::now(Component::Axiom, "axiom:context/compress"),
)
.caused_by("1f1f1f1f-1f1f-4f1f-8f1f-1f1f1f1f1f1f");
let sealed = event.seal().unwrap();
assert_eq!(
sealed["causation_id"].as_str(),
Some("1f1f1f1f-1f1f-4f1f-8f1f-1f1f1f1f1f1f")
);
}
#[test]
fn try_new_refuses_provenance_that_contradicts_the_owning_emitter() {
let refused = Event::try_new(
EventKind::ObservationRecorded,
"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
SubjectType::Observation,
"0f0f0f0f-0f0f-4f0f-8f0f-0f0f0f0f0f0f",
BTreeMap::new(),
Provenance::now(Component::Adam, "adam:organism"),
);
let err = refused.expect_err("an ADAM-authored EVE event must be refused");
assert!(err.to_string().contains("may only be emitted by eve"));
}
#[test]
fn try_new_accepts_provenance_from_the_owning_emitter() {
assert!(Event::try_new(
EventKind::ContextCompressed,
"dddddddd-dddd-4ddd-8ddd-dddddddddddd",
SubjectType::Context,
"0f0f0f0f-0f0f-4f0f-8f0f-0f0f0f0f0f0f",
BTreeMap::new(),
Provenance::now(Component::Axiom, "axiom:context/compress"),
)
.is_ok());
}
#[test]
fn recording_sink_survives_a_poisoned_lock() {
let sink = std::sync::Arc::new(RecordingSink::new());
let poisoner = std::sync::Arc::clone(&sink);
let _ = std::thread::spawn(move || {
let _guard = poisoner.events.lock().unwrap();
panic!("poison the lock");
})
.join();
sink.emit(&Event::new(
EventKind::ContextCompressed,
"dddddddd-dddd-4ddd-8ddd-dddddddddddd",
SubjectType::Context,
"0f0f0f0f-0f0f-4f0f-8f0f-0f0f0f0f0f0f",
BTreeMap::new(),
Provenance::now(Component::Axiom, "axiom:test"),
));
assert_eq!(sink.events().len(), 1);
}
#[test]
fn recording_sink_captures_emission_order() {
let sink = RecordingSink::new();
for kind in [EventKind::ContextCompressed, EventKind::GroundingFailed] {
sink.emit(&Event::new(
kind,
"dddddddd-dddd-4ddd-8ddd-dddddddddddd",
SubjectType::Context,
"0f0f0f0f-0f0f-4f0f-8f0f-0f0f0f0f0f0f",
BTreeMap::new(),
Provenance::now(Component::Axiom, "axiom:test"),
));
}
assert_eq!(
sink.kinds(),
vec![EventKind::ContextCompressed, EventKind::GroundingFailed]
);
}
}