1use serde::{Deserialize, Serialize};
35
36#[repr(u8)]
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
39#[serde(rename_all = "snake_case")]
40pub enum EccSegmentType {
41 CausalEdge = 0x50,
43 CausalNode = 0x51,
45 Impulse = 0x52,
47 CalibrationProfile = 0x53,
49 CrossRef = 0x54,
51 SpectralCheckpoint = 0x55,
53 CausalGraphSnap = 0x56,
55}
56
57impl EccSegmentType {
58 pub const fn as_str(self) -> &'static str {
60 match self {
61 Self::CausalEdge => "ecc.causal_edge",
62 Self::CausalNode => "ecc.causal_node",
63 Self::Impulse => "ecc.impulse",
64 Self::CalibrationProfile => "ecc.calibration_profile",
65 Self::CrossRef => "ecc.cross_ref",
66 Self::SpectralCheckpoint => "ecc.spectral_checkpoint",
67 Self::CausalGraphSnap => "ecc.causal_graph_snap",
68 }
69 }
70
71 pub const ALL: &'static [EccSegmentType] = &[
73 Self::CausalEdge,
74 Self::CausalNode,
75 Self::Impulse,
76 Self::CalibrationProfile,
77 Self::CrossRef,
78 Self::SpectralCheckpoint,
79 Self::CausalGraphSnap,
80 ];
81}
82
83impl TryFrom<u8> for EccSegmentType {
84 type Error = u8;
85
86 fn try_from(value: u8) -> Result<Self, Self::Error> {
87 match value {
88 0x50 => Ok(Self::CausalEdge),
89 0x51 => Ok(Self::CausalNode),
90 0x52 => Ok(Self::Impulse),
91 0x53 => Ok(Self::CalibrationProfile),
92 0x54 => Ok(Self::CrossRef),
93 0x55 => Ok(Self::SpectralCheckpoint),
94 0x56 => Ok(Self::CausalGraphSnap),
95 other => Err(other),
96 }
97 }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
102#[serde(rename_all = "snake_case")]
103pub enum EccSegmentCodec {
104 #[default]
106 Json,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111pub struct EccSegment {
112 pub kind: EccSegmentType,
113 pub codec: EccSegmentCodec,
114 pub payload: Vec<u8>,
115}
116
117pub fn encode_ecc_segment<T: Serialize>(
119 kind: EccSegmentType,
120 value: &T,
121) -> Result<EccSegment, EccSegmentError> {
122 let payload =
123 serde_json::to_vec(value).map_err(|e| EccSegmentError::Encode(e.to_string()))?;
124 Ok(EccSegment {
125 kind,
126 codec: EccSegmentCodec::Json,
127 payload,
128 })
129}
130
131pub fn decode_ecc_segment<T: for<'de> Deserialize<'de>>(
133 segment: &EccSegment,
134) -> Result<T, EccSegmentError> {
135 match segment.codec {
136 EccSegmentCodec::Json => serde_json::from_slice(&segment.payload)
137 .map_err(|e| EccSegmentError::Decode(e.to_string())),
138 }
139}
140
141pub fn segment_to_wire(segment: &EccSegment) -> Vec<u8> {
143 let mut out = Vec::with_capacity(2 + segment.payload.len());
144 out.push(segment.kind as u8);
145 out.push(match segment.codec {
146 EccSegmentCodec::Json => 0x01,
147 });
148 out.extend_from_slice(&segment.payload);
149 out
150}
151
152pub fn segment_from_wire(bytes: &[u8]) -> Result<EccSegment, EccSegmentError> {
154 if bytes.len() < 2 {
155 return Err(EccSegmentError::Truncated);
156 }
157 let kind = EccSegmentType::try_from(bytes[0]).map_err(EccSegmentError::UnknownType)?;
158 let codec = match bytes[1] {
159 0x01 => EccSegmentCodec::Json,
160 other => return Err(EccSegmentError::UnknownCodec(other)),
161 };
162 Ok(EccSegment {
163 kind,
164 codec,
165 payload: bytes[2..].to_vec(),
166 })
167}
168
169#[non_exhaustive]
171#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
172pub enum EccSegmentError {
173 #[error("encode failed: {0}")]
174 Encode(String),
175 #[error("decode failed: {0}")]
176 Decode(String),
177 #[error("truncated segment")]
178 Truncated,
179 #[error("unknown ECC segment type byte: 0x{0:02x}")]
180 UnknownType(u8),
181 #[error("unknown codec byte: 0x{0:02x}")]
182 UnknownCodec(u8),
183}
184
185#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
187pub struct SpectralCheckpoint {
188 pub lambda_2: f64,
189 pub node_count: u64,
190 pub edge_count: u64,
191 pub timestamp: u64,
192 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub notes: Option<String>,
194}
195
196#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
198pub struct CalibrationProfileSegment {
199 pub profile_id: String,
200 pub version: u32,
201 #[serde(default)]
202 pub params: serde_json::Value,
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use crate::causal::{CausalEdge, CausalEdgeType};
209
210 #[test]
211 fn discriminants_stable() {
212 assert_eq!(EccSegmentType::CausalEdge as u8, 0x50);
213 assert_eq!(EccSegmentType::CausalGraphSnap as u8, 0x56);
214 for &t in EccSegmentType::ALL {
215 assert_eq!(EccSegmentType::try_from(t as u8), Ok(t));
216 }
217 }
218
219 #[test]
220 fn unknown_type_err() {
221 assert_eq!(EccSegmentType::try_from(0x12), Err(0x12));
222 assert_eq!(EccSegmentType::try_from(0x40), Err(0x40));
223 }
224
225 #[test]
226 fn causal_edge_roundtrip() {
227 let edge = CausalEdge {
228 source: 1,
229 target: 2,
230 edge_type: CausalEdgeType::Causes,
231 weight: 0.9,
232 timestamp: 10,
233 chain_seq: 3,
234 source_universal_id: [1u8; 32],
235 target_universal_id: [2u8; 32],
236 };
237 let seg = encode_ecc_segment(EccSegmentType::CausalEdge, &edge).unwrap();
238 assert_eq!(seg.kind, EccSegmentType::CausalEdge);
239 let wire = segment_to_wire(&seg);
240 let restored_seg = segment_from_wire(&wire).unwrap();
241 let restored: CausalEdge = decode_ecc_segment(&restored_seg).unwrap();
242 assert_eq!(restored.source, 1);
243 assert_eq!(restored.target, 2);
244 assert_eq!(restored.edge_type, CausalEdgeType::Causes);
245 assert!((restored.weight - 0.9).abs() < f32::EPSILON);
246 }
247
248 #[test]
249 fn spectral_checkpoint_roundtrip() {
250 let cp = SpectralCheckpoint {
251 lambda_2: 0.42,
252 node_count: 10,
253 edge_count: 20,
254 timestamp: 99,
255 notes: Some("tick".into()),
256 };
257 let seg = encode_ecc_segment(EccSegmentType::SpectralCheckpoint, &cp).unwrap();
258 let cp2: SpectralCheckpoint = decode_ecc_segment(&seg).unwrap();
259 assert_eq!(cp2, cp);
260 }
261
262 #[test]
263 fn calibration_profile_roundtrip() {
264 let p = CalibrationProfileSegment {
265 profile_id: "democritus-default".into(),
266 version: 1,
267 params: serde_json::json!({"tick_us": 1000}),
268 };
269 let seg = encode_ecc_segment(EccSegmentType::CalibrationProfile, &p).unwrap();
270 let p2: CalibrationProfileSegment = decode_ecc_segment(&seg).unwrap();
271 assert_eq!(p2.profile_id, "democritus-default");
272 assert_eq!(p2.version, 1);
273 }
274
275 #[test]
276 fn wire_truncated() {
277 assert_eq!(segment_from_wire(&[0x50]), Err(EccSegmentError::Truncated));
278 }
279}