//! The mutation pipeline contracts (GameKernel §2.2, K1–K3): //! command → validate → events → infallible fold. use crate::ids::PlayerId; use serde::{Deserialize, Serialize}; /// Who issued a command (GameKernel K2). #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum Actor { Player(PlayerId), /// Runtime-driven steps (reveal, resolution ordering, round end). System, } /// Typed, testable command rejections (GameKernel K3). Game packages add /// specific rejection kinds via `Game`-typed payloads in their own enums; /// this kernel-level enum carries the cross-game cases. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum Rejection { /// The command's `CommandId` was already applied (idempotency, K2). DuplicateCommand, /// The actor may not issue this command now (turn/window gating). NotAllowedNow, /// Game-specific rejection, stable machine-readable code + human text. Game { code: String, detail: String }, } impl core::fmt::Display for Rejection { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Rejection::DuplicateCommand => write!(f, "duplicate command"), Rejection::NotAllowedNow => write!(f, "not allowed now"), Rejection::Game { code, detail } => write!(f, "{code}: {detail}"), } } } /// An event-sourced aggregate (GameKernel K1): all validation happens in /// `validate`; `fold` is total and infallible for any logged event. pub trait Aggregate: Sized { type Command; type Event; /// Decide whether `command` is legal in the current state, producing /// the events it implies. Must not mutate state. fn validate( &self, actor: Actor, command: &Self::Command, ) -> Result, Rejection>; /// Apply one event. Infallible: every logged event must apply (K1). fn fold(&mut self, event: &Self::Event); }