use std::fmt;
use std::str::FromStr;
use chrono::{SecondsFormat, Utc};
use serde::{Deserialize, Serialize};
fn normalized_decision_plane_token(input: &str) -> String {
let trimmed = input.trim();
let mut normalized = String::with_capacity(trimmed.len());
let mut previous_was_lowercase = false;
let mut previous_was_separator = false;
for character in trimmed.chars() {
match character {
'-' | '_' => {
if !normalized.is_empty() && !previous_was_separator {
normalized.push('_');
}
previous_was_lowercase = false;
previous_was_separator = true;
}
character if character.is_ascii_uppercase() => {
if previous_was_lowercase && !previous_was_separator {
normalized.push('_');
}
normalized.push(character.to_ascii_lowercase());
previous_was_lowercase = false;
previous_was_separator = false;
}
character => {
normalized.push(character.to_ascii_lowercase());
previous_was_lowercase = character.is_ascii_lowercase();
previous_was_separator = false;
}
}
}
normalized
}
pub const DECISION_PLANE_SCHEMA_V1: &str = "ee.decision_plane.v1";
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DecisionPlane {
Packing,
Ranking,
Curation,
RepairOrder,
CacheAdmission,
Observe,
}
impl DecisionPlane {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Packing => "packing",
Self::Ranking => "ranking",
Self::Curation => "curation",
Self::RepairOrder => "repair_order",
Self::CacheAdmission => "cache_admission",
Self::Observe => "observe",
}
}
#[must_use]
pub const fn all() -> &'static [Self] {
&[
Self::Packing,
Self::Ranking,
Self::Curation,
Self::RepairOrder,
Self::CacheAdmission,
Self::Observe,
]
}
}
impl fmt::Display for DecisionPlane {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParseDecisionPlaneError {
pub invalid: String,
}
impl fmt::Display for ParseDecisionPlaneError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"invalid decision plane '{}'; expected one of: packing, ranking, curation, repair_order, cache_admission, observe",
self.invalid
)
}
}
impl std::error::Error for ParseDecisionPlaneError {}
impl FromStr for DecisionPlane {
type Err = ParseDecisionPlaneError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match normalized_decision_plane_token(s).as_str() {
"packing" => Ok(Self::Packing),
"ranking" => Ok(Self::Ranking),
"curation" => Ok(Self::Curation),
"repair_order" => Ok(Self::RepairOrder),
"cache_admission" => Ok(Self::CacheAdmission),
"observe" => Ok(Self::Observe),
_ => Err(ParseDecisionPlaneError {
invalid: s.to_owned(),
}),
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct DecisionPlaneMetadata {
pub policy_id: Option<String>,
pub decision_id: Option<String>,
pub trace_id: Option<String>,
}
impl DecisionPlaneMetadata {
#[must_use]
pub const fn empty() -> Self {
Self {
policy_id: None,
decision_id: None,
trace_id: None,
}
}
#[must_use]
pub fn with_policy(policy_id: impl Into<String>) -> Self {
Self {
policy_id: Some(policy_id.into()),
decision_id: None,
trace_id: None,
}
}
#[must_use]
pub fn full(
policy_id: impl Into<String>,
decision_id: impl Into<String>,
trace_id: impl Into<String>,
) -> Self {
Self {
policy_id: Some(policy_id.into()),
decision_id: Some(decision_id.into()),
trace_id: Some(trace_id.into()),
}
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.policy_id.is_none() && self.decision_id.is_none() && self.trace_id.is_none()
}
#[must_use]
pub const fn has_policy(&self) -> bool {
self.policy_id.is_some()
}
#[must_use]
pub const fn is_auditable(&self) -> bool {
self.policy_id.is_some() && self.decision_id.is_some()
}
#[must_use]
pub fn policy(mut self, policy_id: impl Into<String>) -> Self {
self.policy_id = Some(policy_id.into());
self
}
#[must_use]
pub fn decision(mut self, decision_id: impl Into<String>) -> Self {
self.decision_id = Some(decision_id.into());
self
}
#[must_use]
pub fn trace(mut self, trace_id: impl Into<String>) -> Self {
self.trace_id = Some(trace_id.into());
self
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct DecisionRecord {
pub schema: String,
pub plane: DecisionPlane,
pub metadata: DecisionPlaneMetadata,
pub decided_at: String,
pub outcome: String,
pub reason: Option<String>,
pub confidence: Option<f64>,
pub shadow: bool,
pub incumbent_outcome: Option<String>,
}
impl DecisionRecord {
#[must_use]
pub fn builder() -> DecisionRecordBuilder {
DecisionRecordBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
pub struct DecisionRecordBuilder {
plane: Option<DecisionPlane>,
metadata: DecisionPlaneMetadata,
decided_at: Option<String>,
outcome: Option<String>,
reason: Option<String>,
confidence: Option<f64>,
shadow: bool,
incumbent_outcome: Option<String>,
}
impl DecisionRecordBuilder {
#[must_use]
pub fn plane(mut self, plane: DecisionPlane) -> Self {
self.plane = Some(plane);
self
}
#[must_use]
pub fn metadata(mut self, metadata: DecisionPlaneMetadata) -> Self {
self.metadata = metadata;
self
}
#[must_use]
pub fn policy_id(mut self, policy_id: impl Into<String>) -> Self {
self.metadata.policy_id = Some(policy_id.into());
self
}
#[must_use]
pub fn decision_id(mut self, decision_id: impl Into<String>) -> Self {
self.metadata.decision_id = Some(decision_id.into());
self
}
#[must_use]
pub fn trace_id(mut self, trace_id: impl Into<String>) -> Self {
self.metadata.trace_id = Some(trace_id.into());
self
}
#[must_use]
pub fn decided_at(mut self, decided_at: impl Into<String>) -> Self {
self.decided_at = Some(decided_at.into());
self
}
#[must_use]
pub fn outcome(mut self, outcome: impl Into<String>) -> Self {
self.outcome = Some(outcome.into());
self
}
#[must_use]
pub fn reason(mut self, reason: impl Into<String>) -> Self {
self.reason = Some(reason.into());
self
}
#[must_use]
pub fn confidence(mut self, confidence: f64) -> Self {
self.confidence = Some(confidence);
self
}
#[must_use]
pub fn shadow(mut self, shadow: bool) -> Self {
self.shadow = shadow;
self
}
#[must_use]
pub fn incumbent_outcome(mut self, incumbent_outcome: impl Into<String>) -> Self {
self.incumbent_outcome = Some(incumbent_outcome.into());
self
}
#[must_use]
pub fn build(self) -> DecisionRecord {
DecisionRecord {
schema: DECISION_PLANE_SCHEMA_V1.to_owned(),
plane: self.plane.unwrap_or(DecisionPlane::Packing),
metadata: self.metadata,
decided_at: self.decided_at.unwrap_or_else(now_rfc3339),
outcome: self.outcome.unwrap_or_default(),
reason: self.reason,
confidence: self.confidence,
shadow: self.shadow,
incumbent_outcome: self.incumbent_outcome,
}
}
pub fn try_build(self) -> Result<DecisionRecord, DecisionBuildError> {
let outcome = match self.outcome {
Some(value) if !value.trim().is_empty() => value.trim().to_owned(),
Some(_) => return Err(DecisionBuildError::EmptyOutcome),
None => return Err(DecisionBuildError::MissingOutcome),
};
Ok(DecisionRecord {
schema: DECISION_PLANE_SCHEMA_V1.to_owned(),
plane: self.plane.unwrap_or(DecisionPlane::Packing),
metadata: self.metadata,
decided_at: self.decided_at.unwrap_or_else(now_rfc3339),
outcome,
reason: self.reason,
confidence: self.confidence,
shadow: self.shadow,
incumbent_outcome: self.incumbent_outcome,
})
}
}
fn now_rfc3339() -> String {
Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DecisionBuildError {
MissingOutcome,
EmptyOutcome,
}
impl fmt::Display for DecisionBuildError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingOutcome => f.write_str("DecisionRecord requires outcome to be set"),
Self::EmptyOutcome => f.write_str("DecisionRecord outcome must not be empty"),
}
}
}
impl std::error::Error for DecisionBuildError {}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
type TestResult = Result<(), String>;
fn ensure<T: std::fmt::Debug + PartialEq>(actual: T, expected: T, ctx: &str) -> TestResult {
if actual == expected {
Ok(())
} else {
Err(format!("{ctx}: expected {expected:?}, got {actual:?}"))
}
}
#[test]
fn decision_plane_roundtrip() -> TestResult {
for plane in DecisionPlane::all() {
let s = plane.as_str();
let parsed: DecisionPlane = s
.parse()
.map_err(|e: ParseDecisionPlaneError| e.to_string())?;
ensure(parsed, *plane, &format!("roundtrip {s}"))?;
}
Ok(())
}
#[test]
fn decision_plane_accepts_operator_spelling_variants() {
assert_eq!(
DecisionPlane::from_str(" Repair-Order ").expect("hyphenated plane parses"),
DecisionPlane::RepairOrder
);
assert_eq!(
DecisionPlane::from_str("CACHE_ADMISSION").expect("uppercase plane parses"),
DecisionPlane::CacheAdmission
);
assert_eq!(
DecisionPlane::from_str("repairOrder").expect("camelCase plane parses"),
DecisionPlane::RepairOrder
);
assert_eq!(
DecisionPlane::from_str("CacheAdmission").expect("PascalCase plane parses"),
DecisionPlane::CacheAdmission
);
}
#[test]
fn decision_plane_display() {
assert_eq!(DecisionPlane::Packing.to_string(), "packing");
assert_eq!(DecisionPlane::Ranking.to_string(), "ranking");
assert_eq!(DecisionPlane::Curation.to_string(), "curation");
assert_eq!(DecisionPlane::RepairOrder.to_string(), "repair_order");
assert_eq!(DecisionPlane::CacheAdmission.to_string(), "cache_admission");
}
#[test]
fn decision_plane_metadata_empty() {
let meta = DecisionPlaneMetadata::empty();
assert!(meta.is_empty());
assert!(!meta.has_policy());
assert!(!meta.is_auditable());
}
#[test]
fn decision_plane_metadata_with_policy() {
let meta = DecisionPlaneMetadata::with_policy("aggressive-decay");
assert!(!meta.is_empty());
assert!(meta.has_policy());
assert!(!meta.is_auditable());
assert_eq!(meta.policy_id, Some("aggressive-decay".to_owned()));
}
#[test]
fn decision_plane_metadata_full() {
let meta = DecisionPlaneMetadata::full("policy-1", "dec-001", "trace-abc");
assert!(!meta.is_empty());
assert!(meta.has_policy());
assert!(meta.is_auditable());
assert_eq!(meta.policy_id, Some("policy-1".to_owned()));
assert_eq!(meta.decision_id, Some("dec-001".to_owned()));
assert_eq!(meta.trace_id, Some("trace-abc".to_owned()));
}
#[test]
fn decision_plane_metadata_builder_pattern() {
let meta = DecisionPlaneMetadata::empty()
.policy("my-policy")
.decision("dec-123")
.trace("trace-xyz");
assert_eq!(meta.policy_id, Some("my-policy".to_owned()));
assert_eq!(meta.decision_id, Some("dec-123".to_owned()));
assert_eq!(meta.trace_id, Some("trace-xyz".to_owned()));
}
#[test]
fn decision_record_builder() {
let record = DecisionRecord::builder()
.plane(DecisionPlane::Curation)
.policy_id("curation-v2")
.decision_id("dec-456")
.trace_id("trace-req-1")
.decided_at("2026-04-30T12:00:00Z")
.outcome("archive")
.reason("Low confidence, no recent access")
.confidence(0.3)
.shadow(false)
.build();
assert_eq!(record.schema, DECISION_PLANE_SCHEMA_V1);
assert_eq!(record.plane, DecisionPlane::Curation);
assert_eq!(record.metadata.policy_id, Some("curation-v2".to_owned()));
assert_eq!(record.outcome, "archive");
assert_eq!(record.confidence, Some(0.3));
assert!(!record.shadow);
}
#[test]
fn decision_record_shadow_comparison() {
let record = DecisionRecord::builder()
.plane(DecisionPlane::Ranking)
.policy_id("experimental-ranker")
.decided_at("2026-04-30T12:00:00Z")
.outcome("rank-3")
.shadow(true)
.incumbent_outcome("rank-1")
.build();
assert!(record.shadow);
assert_eq!(record.incumbent_outcome, Some("rank-1".to_owned()));
}
#[test]
fn decision_record_serializes_to_json() {
let record = DecisionRecord::builder()
.plane(DecisionPlane::Packing)
.policy_id("budget-tight")
.decided_at("2026-04-30T12:00:00Z")
.outcome("include")
.build();
let json = serde_json::to_string(&record).expect("serialize");
assert!(json.contains(r#""schema":"ee.decision_plane.v1""#));
assert!(json.contains(r#""plane":"packing""#));
assert!(json.contains(r#""policy_id":"budget-tight""#));
}
#[test]
fn decision_plane_metadata_serializes() {
let meta = DecisionPlaneMetadata::full("pol-1", "dec-1", "trace-1");
let json = serde_json::to_string(&meta).expect("serialize");
assert!(json.contains(r#""policy_id":"pol-1""#));
assert!(json.contains(r#""decision_id":"dec-1""#));
assert!(json.contains(r#""trace_id":"trace-1""#));
}
#[test]
fn parse_invalid_decision_plane_error() {
let result: Result<DecisionPlane, _> = "invalid".parse();
assert!(result.is_err());
let err = result.expect_err("avoid unwrap_err in production code");
assert!(err.to_string().contains("invalid decision plane"));
}
#[test]
fn build_autofills_decided_at_when_unset() -> TestResult {
let record = DecisionRecord::builder()
.plane(DecisionPlane::Curation)
.outcome("archive")
.build();
if record.decided_at.is_empty() {
return Err(format!(
"build() must auto-fill decided_at; got {:?}",
record.decided_at
));
}
if !record.decided_at.ends_with('Z') || record.decided_at.len() < 30 {
return Err(format!(
"auto-filled decided_at should be RFC3339 with Z suffix; got {:?}",
record.decided_at
));
}
Ok(())
}
#[test]
fn build_autofills_decided_at_with_nanosecond_precision() -> TestResult {
let record = DecisionRecord::builder()
.plane(DecisionPlane::Curation)
.outcome("archive")
.build();
chrono::DateTime::parse_from_rfc3339(&record.decided_at)
.map_err(|error| error.to_string())?;
let Some(fraction) = record
.decided_at
.strip_suffix('Z')
.and_then(|value| value.rsplit_once('.').map(|(_, fraction)| fraction))
else {
return Err(format!(
"auto-filled decided_at should include nanoseconds; got {:?}",
record.decided_at
));
};
if fraction.len() != 9 || !fraction.chars().all(|ch| ch.is_ascii_digit()) {
return Err(format!(
"auto-filled decided_at should have 9 fractional digits; got {:?}",
record.decided_at
));
}
Ok(())
}
#[test]
fn build_preserves_explicit_decided_at() -> TestResult {
let record = DecisionRecord::builder()
.plane(DecisionPlane::Curation)
.decided_at("2026-04-30T12:00:00Z")
.outcome("archive")
.build();
ensure(
record.decided_at,
"2026-04-30T12:00:00Z".to_owned(),
"explicit decided_at must round-trip unchanged",
)
}
#[test]
fn try_build_rejects_missing_outcome() -> TestResult {
let result = DecisionRecord::builder()
.plane(DecisionPlane::Curation)
.try_build();
ensure(
result,
Err(DecisionBuildError::MissingOutcome),
"try_build with no outcome must fail",
)
}
#[test]
fn try_build_rejects_empty_outcome() -> TestResult {
let result = DecisionRecord::builder()
.plane(DecisionPlane::Curation)
.outcome("")
.try_build();
ensure(
result,
Err(DecisionBuildError::EmptyOutcome),
"try_build with empty outcome must fail",
)
}
#[test]
fn try_build_rejects_whitespace_only_outcome() -> TestResult {
let result = DecisionRecord::builder()
.plane(DecisionPlane::Curation)
.outcome(" ")
.try_build();
ensure(
result,
Err(DecisionBuildError::EmptyOutcome),
"try_build with whitespace-only outcome must fail",
)
}
#[test]
fn try_build_returns_record_when_outcome_set() -> TestResult {
let record = DecisionRecord::builder()
.plane(DecisionPlane::Curation)
.outcome("archive")
.try_build()
.map_err(|err| err.to_string())?;
ensure(
record.outcome,
"archive".to_owned(),
"outcome must round-trip",
)?;
if record.decided_at.is_empty() {
return Err("try_build must auto-fill decided_at".to_owned());
}
Ok(())
}
#[test]
fn all_decision_planes_covered() {
let all = DecisionPlane::all();
assert_eq!(all.len(), 6);
assert!(all.contains(&DecisionPlane::Packing));
assert!(all.contains(&DecisionPlane::Ranking));
assert!(all.contains(&DecisionPlane::Curation));
assert!(all.contains(&DecisionPlane::RepairOrder));
assert!(all.contains(&DecisionPlane::CacheAdmission));
assert!(all.contains(&DecisionPlane::Observe));
}
}