Skip to main content

clawft_kernel/
ecc_segment.rs

1//! ECC RVF segment type definitions (WEFT-508).
2//!
3//! Upstream `rvf-types::SegmentType` covers vector/index/federation codes
4//! (`0x00`–`0x36`). WeftOS ECC structures need **domain-local** discriminants
5//! that do not collide with:
6//!
7//! - rvf-types (`≤ 0x36`)
8//! - context-router research reservations (`0x40`–`0x42`)
9//!
10//! We allocate the **`0x50`–`0x5F`** block for ECC persistence payloads.
11//! Each variant wraps a CBOR (preferred) or JSON body; this module defines
12//! the type table + encode/decode helpers. Full migration of on-disk chain
13//! blobs onto these segments is follow-up work (see migration notes).
14//!
15//! # Segment table
16//!
17//! | Code   | Name                 | Payload                                      |
18//! |--------|----------------------|----------------------------------------------|
19//! | `0x50` | `CausalEdge`         | [`CausalEdge`](crate::causal::CausalEdge)    |
20//! | `0x51` | `CausalNode`         | [`CausalNode`](crate::causal::CausalNode)    |
21//! | `0x52` | `Impulse`            | [`Impulse`](crate::impulse::Impulse)         |
22//! | `0x53` | `CalibrationProfile` | JSON profile blob (calibration / democritus) |
23//! | `0x54` | `CrossRef`           | [`CrossRef`](crate::crossref::CrossRef)      |
24//! | `0x55` | `SpectralCheckpoint` | λ₂ / eigenvalue snapshot                     |
25//! | `0x56` | `CausalGraphSnap`    | Whole-graph snapshot envelope                |
26//!
27//! # Migration plan
28//!
29//! 1. **Today**: ad-hoc JSON / ExoChain CBOR events remain authoritative.
30//! 2. **Writers** may dual-write ECC segments alongside chain events.
31//! 3. **Readers** prefer ECC segments when present; fall back to legacy.
32//! 4. After one release cycle, stop writing legacy-only forms for new events.
33
34use serde::{Deserialize, Serialize};
35
36/// WeftOS ECC segment discriminants (RVF `type` byte, domain `0x50`–`0x5F`).
37#[repr(u8)]
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
39#[serde(rename_all = "snake_case")]
40pub enum EccSegmentType {
41    /// Directed causal edge record.
42    CausalEdge = 0x50,
43    /// Causal graph node record.
44    CausalNode = 0x51,
45    /// Ephemeral impulse / causal event.
46    Impulse = 0x52,
47    /// Calibration or Democritus profile blob.
48    CalibrationProfile = 0x53,
49    /// Cross-tree reference.
50    CrossRef = 0x54,
51    /// Spectral analysis checkpoint (λ₂, eigenvalues).
52    SpectralCheckpoint = 0x55,
53    /// Full causal-graph snapshot envelope.
54    CausalGraphSnap = 0x56,
55}
56
57impl EccSegmentType {
58    /// Stable name for logs / docs.
59    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    /// All defined ECC segment types (for tests / docs generation).
72    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/// Codec for ECC segment payloads.
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
102#[serde(rename_all = "snake_case")]
103pub enum EccSegmentCodec {
104    /// JSON (`serde_json`) — default for interoperability / tests.
105    #[default]
106    Json,
107}
108
109/// Framed ECC segment: type byte + codec + payload bytes.
110#[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
117/// Encode a serializable payload as an [`EccSegment`].
118pub 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
131/// Decode payload bytes into `T`.
132pub 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
141/// Wire bytes: `[type:u8][codec:u8][payload...]`.
142pub 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
152/// Parse wire bytes produced by [`segment_to_wire`].
153pub 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/// Errors for ECC segment codec.
170#[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/// Spectral checkpoint payload (λ₂ snapshot).
186#[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/// Calibration profile payload (opaque JSON-friendly map for now).
197#[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}