use serde::{Deserialize, Serialize};
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EccSegmentType {
CausalEdge = 0x50,
CausalNode = 0x51,
Impulse = 0x52,
CalibrationProfile = 0x53,
CrossRef = 0x54,
SpectralCheckpoint = 0x55,
CausalGraphSnap = 0x56,
}
impl EccSegmentType {
pub const fn as_str(self) -> &'static str {
match self {
Self::CausalEdge => "ecc.causal_edge",
Self::CausalNode => "ecc.causal_node",
Self::Impulse => "ecc.impulse",
Self::CalibrationProfile => "ecc.calibration_profile",
Self::CrossRef => "ecc.cross_ref",
Self::SpectralCheckpoint => "ecc.spectral_checkpoint",
Self::CausalGraphSnap => "ecc.causal_graph_snap",
}
}
pub const ALL: &'static [EccSegmentType] = &[
Self::CausalEdge,
Self::CausalNode,
Self::Impulse,
Self::CalibrationProfile,
Self::CrossRef,
Self::SpectralCheckpoint,
Self::CausalGraphSnap,
];
}
impl TryFrom<u8> for EccSegmentType {
type Error = u8;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0x50 => Ok(Self::CausalEdge),
0x51 => Ok(Self::CausalNode),
0x52 => Ok(Self::Impulse),
0x53 => Ok(Self::CalibrationProfile),
0x54 => Ok(Self::CrossRef),
0x55 => Ok(Self::SpectralCheckpoint),
0x56 => Ok(Self::CausalGraphSnap),
other => Err(other),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EccSegmentCodec {
#[default]
Json,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EccSegment {
pub kind: EccSegmentType,
pub codec: EccSegmentCodec,
pub payload: Vec<u8>,
}
pub fn encode_ecc_segment<T: Serialize>(
kind: EccSegmentType,
value: &T,
) -> Result<EccSegment, EccSegmentError> {
let payload =
serde_json::to_vec(value).map_err(|e| EccSegmentError::Encode(e.to_string()))?;
Ok(EccSegment {
kind,
codec: EccSegmentCodec::Json,
payload,
})
}
pub fn decode_ecc_segment<T: for<'de> Deserialize<'de>>(
segment: &EccSegment,
) -> Result<T, EccSegmentError> {
match segment.codec {
EccSegmentCodec::Json => serde_json::from_slice(&segment.payload)
.map_err(|e| EccSegmentError::Decode(e.to_string())),
}
}
pub fn segment_to_wire(segment: &EccSegment) -> Vec<u8> {
let mut out = Vec::with_capacity(2 + segment.payload.len());
out.push(segment.kind as u8);
out.push(match segment.codec {
EccSegmentCodec::Json => 0x01,
});
out.extend_from_slice(&segment.payload);
out
}
pub fn segment_from_wire(bytes: &[u8]) -> Result<EccSegment, EccSegmentError> {
if bytes.len() < 2 {
return Err(EccSegmentError::Truncated);
}
let kind = EccSegmentType::try_from(bytes[0]).map_err(EccSegmentError::UnknownType)?;
let codec = match bytes[1] {
0x01 => EccSegmentCodec::Json,
other => return Err(EccSegmentError::UnknownCodec(other)),
};
Ok(EccSegment {
kind,
codec,
payload: bytes[2..].to_vec(),
})
}
#[non_exhaustive]
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
pub enum EccSegmentError {
#[error("encode failed: {0}")]
Encode(String),
#[error("decode failed: {0}")]
Decode(String),
#[error("truncated segment")]
Truncated,
#[error("unknown ECC segment type byte: 0x{0:02x}")]
UnknownType(u8),
#[error("unknown codec byte: 0x{0:02x}")]
UnknownCodec(u8),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SpectralCheckpoint {
pub lambda_2: f64,
pub node_count: u64,
pub edge_count: u64,
pub timestamp: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CalibrationProfileSegment {
pub profile_id: String,
pub version: u32,
#[serde(default)]
pub params: serde_json::Value,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::causal::{CausalEdge, CausalEdgeType};
#[test]
fn discriminants_stable() {
assert_eq!(EccSegmentType::CausalEdge as u8, 0x50);
assert_eq!(EccSegmentType::CausalGraphSnap as u8, 0x56);
for &t in EccSegmentType::ALL {
assert_eq!(EccSegmentType::try_from(t as u8), Ok(t));
}
}
#[test]
fn unknown_type_err() {
assert_eq!(EccSegmentType::try_from(0x12), Err(0x12));
assert_eq!(EccSegmentType::try_from(0x40), Err(0x40));
}
#[test]
fn causal_edge_roundtrip() {
let edge = CausalEdge {
source: 1,
target: 2,
edge_type: CausalEdgeType::Causes,
weight: 0.9,
timestamp: 10,
chain_seq: 3,
source_universal_id: [1u8; 32],
target_universal_id: [2u8; 32],
};
let seg = encode_ecc_segment(EccSegmentType::CausalEdge, &edge).unwrap();
assert_eq!(seg.kind, EccSegmentType::CausalEdge);
let wire = segment_to_wire(&seg);
let restored_seg = segment_from_wire(&wire).unwrap();
let restored: CausalEdge = decode_ecc_segment(&restored_seg).unwrap();
assert_eq!(restored.source, 1);
assert_eq!(restored.target, 2);
assert_eq!(restored.edge_type, CausalEdgeType::Causes);
assert!((restored.weight - 0.9).abs() < f32::EPSILON);
}
#[test]
fn spectral_checkpoint_roundtrip() {
let cp = SpectralCheckpoint {
lambda_2: 0.42,
node_count: 10,
edge_count: 20,
timestamp: 99,
notes: Some("tick".into()),
};
let seg = encode_ecc_segment(EccSegmentType::SpectralCheckpoint, &cp).unwrap();
let cp2: SpectralCheckpoint = decode_ecc_segment(&seg).unwrap();
assert_eq!(cp2, cp);
}
#[test]
fn calibration_profile_roundtrip() {
let p = CalibrationProfileSegment {
profile_id: "democritus-default".into(),
version: 1,
params: serde_json::json!({"tick_us": 1000}),
};
let seg = encode_ecc_segment(EccSegmentType::CalibrationProfile, &p).unwrap();
let p2: CalibrationProfileSegment = decode_ecc_segment(&seg).unwrap();
assert_eq!(p2.profile_id, "democritus-default");
assert_eq!(p2.version, 1);
}
#[test]
fn wire_truncated() {
assert_eq!(segment_from_wire(&[0x50]), Err(EccSegmentError::Truncated));
}
}