Skip to main content

rvf_types/
lineage.rs

1//! DNA-style lineage provenance types for RVF files.
2//!
3//! Each RVF file carries a `FileIdentity` in the Level0Root reserved area,
4//! enabling provenance chains: parent→child→grandchild with hash verification.
5
6/// Derivation type describing how a child file was produced from its parent.
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[repr(u8)]
10pub enum DerivationType {
11    /// Exact copy of the parent.
12    Clone = 0,
13    /// Subset of parent data (filtered).
14    Filter = 1,
15    /// Multiple parents merged into one.
16    Merge = 2,
17    /// Re-quantized from parent.
18    Quantize = 3,
19    /// Re-indexed (HNSW rebuild, etc.).
20    Reindex = 4,
21    /// Arbitrary transformation.
22    Transform = 5,
23    /// Point-in-time snapshot.
24    Snapshot = 6,
25    /// User-defined derivation.
26    UserDefined = 0xFF,
27}
28
29impl TryFrom<u8> for DerivationType {
30    type Error = u8;
31
32    fn try_from(value: u8) -> Result<Self, Self::Error> {
33        match value {
34            0 => Ok(Self::Clone),
35            1 => Ok(Self::Filter),
36            2 => Ok(Self::Merge),
37            3 => Ok(Self::Quantize),
38            4 => Ok(Self::Reindex),
39            5 => Ok(Self::Transform),
40            6 => Ok(Self::Snapshot),
41            0xFF => Ok(Self::UserDefined),
42            other => Err(other),
43        }
44    }
45}
46
47/// File identity embedded in the Level0Root reserved area at offset 0xF00.
48///
49/// Exactly 68 bytes, fitting within the 252-byte reserved area.
50/// Old readers that ignore the reserved area see zeros and continue working.
51///
52/// Layout:
53/// | Offset | Size | Field          |
54/// |--------|------|----------------|
55/// | 0x00   | 16   | file_id        |
56/// | 0x10   | 16   | parent_id      |
57/// | 0x20   | 32   | parent_hash    |
58/// | 0x40   | 4    | lineage_depth  |
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
61#[repr(C)]
62pub struct FileIdentity {
63    /// Unique identifier for this file (UUID-style, 16 bytes).
64    pub file_id: [u8; 16],
65    /// Identifier of the parent file (all zeros for root files).
66    pub parent_id: [u8; 16],
67    /// SHAKE-256-256 hash of the parent's manifest (all zeros for root).
68    pub parent_hash: [u8; 32],
69    /// Lineage depth: 0 for root, incremented for each derivation.
70    pub lineage_depth: u32,
71}
72
73// Compile-time assertion: FileIdentity must be exactly 68 bytes.
74const _: () = assert!(core::mem::size_of::<FileIdentity>() == 68);
75
76impl FileIdentity {
77    /// Create a root identity (no parent) with the given file_id.
78    pub const fn new_root(file_id: [u8; 16]) -> Self {
79        Self {
80            file_id,
81            parent_id: [0u8; 16],
82            parent_hash: [0u8; 32],
83            lineage_depth: 0,
84        }
85    }
86
87    /// Returns true if this is a root identity (no parent).
88    pub fn is_root(&self) -> bool {
89        self.parent_id == [0u8; 16] && self.lineage_depth == 0
90    }
91
92    /// Create an all-zero identity (default for files without lineage).
93    pub const fn zeroed() -> Self {
94        Self {
95            file_id: [0u8; 16],
96            parent_id: [0u8; 16],
97            parent_hash: [0u8; 32],
98            lineage_depth: 0,
99        }
100    }
101
102    /// Serialize to a 68-byte array.
103    pub fn to_bytes(&self) -> [u8; 68] {
104        let mut buf = [0u8; 68];
105        buf[0..16].copy_from_slice(&self.file_id);
106        buf[16..32].copy_from_slice(&self.parent_id);
107        buf[32..64].copy_from_slice(&self.parent_hash);
108        buf[64..68].copy_from_slice(&self.lineage_depth.to_le_bytes());
109        buf
110    }
111
112    /// Deserialize from a 68-byte slice.
113    pub fn from_bytes(data: &[u8; 68]) -> Self {
114        let mut file_id = [0u8; 16];
115        file_id.copy_from_slice(&data[0..16]);
116        let mut parent_id = [0u8; 16];
117        parent_id.copy_from_slice(&data[16..32]);
118        let mut parent_hash = [0u8; 32];
119        parent_hash.copy_from_slice(&data[32..64]);
120        // Safety: data is &[u8; 68], so data[64..68] is always exactly 4 bytes.
121        // Use an explicit array conversion to avoid the unwrap.
122        let lineage_depth = u32::from_le_bytes([data[64], data[65], data[66], data[67]]);
123        Self {
124            file_id,
125            parent_id,
126            parent_hash,
127            lineage_depth,
128        }
129    }
130}
131
132/// A lineage record for witness chain entries.
133///
134/// Fixed 128 bytes with a 47-byte description field.
135///
136/// Layout:
137/// | Offset | Size | Field            |
138/// |--------|------|------------------|
139/// | 0x00   | 16   | file_id          |
140/// | 0x10   | 16   | parent_id        |
141/// | 0x20   | 32   | parent_hash      |
142/// | 0x40   | 1    | derivation_type  |
143/// | 0x41   | 3    | _pad             |
144/// | 0x44   | 4    | mutation_count   |
145/// | 0x48   | 8    | timestamp_ns     |
146/// | 0x50   | 1    | description_len  |
147/// | 0x51   | 47   | description      |
148#[derive(Clone, Debug, PartialEq, Eq)]
149#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
150pub struct LineageRecord {
151    /// Unique identifier for this file.
152    pub file_id: [u8; 16],
153    /// Identifier of the parent file.
154    pub parent_id: [u8; 16],
155    /// SHAKE-256-256 hash of the parent's manifest.
156    pub parent_hash: [u8; 32],
157    /// How the child was derived from the parent.
158    pub derivation_type: DerivationType,
159    /// Number of mutations/changes applied.
160    pub mutation_count: u32,
161    /// Nanosecond UNIX timestamp of derivation.
162    pub timestamp_ns: u64,
163    /// Length of the description (max 47).
164    pub description_len: u8,
165    /// UTF-8 description of the derivation (47-byte buffer).
166    pub description: [u8; 47],
167}
168
169/// Size of a serialized LineageRecord.
170pub const LINEAGE_RECORD_SIZE: usize = 128;
171
172impl LineageRecord {
173    /// Create a new lineage record with a description string.
174    pub fn new(
175        file_id: [u8; 16],
176        parent_id: [u8; 16],
177        parent_hash: [u8; 32],
178        derivation_type: DerivationType,
179        mutation_count: u32,
180        timestamp_ns: u64,
181        desc: &str,
182    ) -> Self {
183        let desc_bytes = desc.as_bytes();
184        let desc_len = desc_bytes.len().min(47) as u8;
185        let mut description = [0u8; 47];
186        description[..desc_len as usize].copy_from_slice(&desc_bytes[..desc_len as usize]);
187        Self {
188            file_id,
189            parent_id,
190            parent_hash,
191            derivation_type,
192            mutation_count,
193            timestamp_ns,
194            description_len: desc_len,
195            description,
196        }
197    }
198
199    /// Get the description as a string slice.
200    pub fn description_str(&self) -> &str {
201        let len = (self.description_len as usize).min(47);
202        core::str::from_utf8(&self.description[..len]).unwrap_or("")
203    }
204}
205
206// ---- Witness type constants for lineage entries ----
207
208/// Witness type: file derivation event.
209pub const WITNESS_DERIVATION: u8 = 0x09;
210/// Witness type: lineage merge (multi-parent).
211pub const WITNESS_LINEAGE_MERGE: u8 = 0x0A;
212/// Witness type: lineage snapshot.
213pub const WITNESS_LINEAGE_SNAPSHOT: u8 = 0x0B;
214/// Witness type: lineage transform.
215pub const WITNESS_LINEAGE_TRANSFORM: u8 = 0x0C;
216/// Witness type: lineage verification.
217pub const WITNESS_LINEAGE_VERIFY: u8 = 0x0D;
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn file_identity_size() {
225        assert_eq!(core::mem::size_of::<FileIdentity>(), 68);
226    }
227
228    #[test]
229    fn file_identity_fits_in_reserved() {
230        // Level0Root reserved area is 252 bytes; FileIdentity is 68 bytes
231        assert!(core::mem::size_of::<FileIdentity>() <= 252);
232    }
233
234    #[test]
235    fn file_identity_root() {
236        let id = [0x42u8; 16];
237        let fi = FileIdentity::new_root(id);
238        assert!(fi.is_root());
239        assert_eq!(fi.file_id, id);
240        assert_eq!(fi.parent_id, [0u8; 16]);
241        assert_eq!(fi.parent_hash, [0u8; 32]);
242        assert_eq!(fi.lineage_depth, 0);
243    }
244
245    #[test]
246    fn file_identity_zeroed_is_root() {
247        let fi = FileIdentity::zeroed();
248        assert!(fi.is_root());
249    }
250
251    #[test]
252    fn file_identity_round_trip() {
253        let fi = FileIdentity {
254            file_id: [1u8; 16],
255            parent_id: [2u8; 16],
256            parent_hash: [3u8; 32],
257            lineage_depth: 42,
258        };
259        let bytes = fi.to_bytes();
260        let decoded = FileIdentity::from_bytes(&bytes);
261        assert_eq!(fi, decoded);
262    }
263
264    #[test]
265    fn file_identity_non_root() {
266        let fi = FileIdentity {
267            file_id: [1u8; 16],
268            parent_id: [2u8; 16],
269            parent_hash: [3u8; 32],
270            lineage_depth: 1,
271        };
272        assert!(!fi.is_root());
273    }
274
275    #[test]
276    fn derivation_type_round_trip() {
277        let cases: &[(u8, DerivationType)] = &[
278            (0, DerivationType::Clone),
279            (1, DerivationType::Filter),
280            (2, DerivationType::Merge),
281            (3, DerivationType::Quantize),
282            (4, DerivationType::Reindex),
283            (5, DerivationType::Transform),
284            (6, DerivationType::Snapshot),
285            (0xFF, DerivationType::UserDefined),
286        ];
287        for &(raw, expected) in cases {
288            assert_eq!(DerivationType::try_from(raw), Ok(expected));
289            assert_eq!(expected as u8, raw);
290        }
291    }
292
293    #[test]
294    fn derivation_type_unknown() {
295        assert_eq!(DerivationType::try_from(7), Err(7));
296        assert_eq!(DerivationType::try_from(0xFE), Err(0xFE));
297    }
298
299    #[test]
300    fn lineage_record_description() {
301        let record = LineageRecord::new(
302            [1u8; 16],
303            [2u8; 16],
304            [3u8; 32],
305            DerivationType::Filter,
306            5,
307            1_000_000_000,
308            "filtered by category",
309        );
310        assert_eq!(record.description_str(), "filtered by category");
311        assert_eq!(record.description_len, 20);
312    }
313
314    #[test]
315    fn lineage_record_long_description_truncated() {
316        let long_desc = "a]".repeat(50); // 100 chars, way over 47
317        let record = LineageRecord::new(
318            [0u8; 16],
319            [0u8; 16],
320            [0u8; 32],
321            DerivationType::Clone,
322            0,
323            0,
324            &long_desc,
325        );
326        assert_eq!(record.description_len, 47);
327    }
328
329    #[test]
330    fn witness_type_constants() {
331        assert_eq!(WITNESS_DERIVATION, 0x09);
332        assert_eq!(WITNESS_LINEAGE_MERGE, 0x0A);
333        assert_eq!(WITNESS_LINEAGE_SNAPSHOT, 0x0B);
334        assert_eq!(WITNESS_LINEAGE_TRANSFORM, 0x0C);
335        assert_eq!(WITNESS_LINEAGE_VERIFY, 0x0D);
336    }
337}