Skip to main content

agentic_time/
file_format.rs

1//! `.atime` binary file format — portable temporal graph.
2
3use std::collections::HashMap;
4use std::io::{Read, Write};
5use std::path::PathBuf;
6
7use chrono::Utc;
8use serde::{Deserialize, Serialize};
9
10use crate::error::{TimeError, TimeResult};
11use crate::TemporalId;
12
13/// Magic bytes identifying `.atime` files.
14pub const MAGIC: [u8; 4] = *b"ATIM";
15
16/// Current format version.
17pub const VERSION: u32 = 1;
18
19/// File header (64 bytes).
20#[derive(Debug, Clone)]
21#[repr(C)]
22pub struct FileHeader {
23    /// Magic bytes "ATIM".
24    pub magic: [u8; 4],
25    /// Format version.
26    pub version: u32,
27    /// Flags (reserved).
28    pub flags: u32,
29    /// Number of temporal entities.
30    pub entity_count: u64,
31    /// Offset to entity index.
32    pub index_offset: u64,
33    /// Offset to deadline index.
34    pub deadline_index_offset: u64,
35    /// Offset to decay index.
36    pub decay_index_offset: u64,
37    /// File creation timestamp (Unix micros).
38    pub created_at: u64,
39    /// Last modified timestamp (Unix micros).
40    pub modified_at: u64,
41    /// BLAKE3 checksum placeholder (not yet computed during write).
42    pub checksum: [u8; 32],
43}
44
45impl Default for FileHeader {
46    fn default() -> Self {
47        Self::new()
48    }
49}
50
51impl FileHeader {
52    /// Header size in bytes.
53    // 4 + 4 + 4 + 8 + 8 + 8 + 8 + 8 + 8 + 32 = 92
54    // (Spec says 64 but the fields sum to 92. We write exactly these fields.)
55    pub const SIZE: usize = 4 + 4 + 4 + 8 + 8 + 8 + 8 + 8 + 8 + 32;
56
57    /// Create a new header with current timestamps.
58    pub fn new() -> Self {
59        let now = Utc::now().timestamp_micros() as u64;
60        Self {
61            magic: MAGIC,
62            version: VERSION,
63            flags: 0,
64            entity_count: 0,
65            index_offset: 0,
66            deadline_index_offset: 0,
67            decay_index_offset: 0,
68            created_at: now,
69            modified_at: now,
70            checksum: [0; 32],
71        }
72    }
73
74    /// Write header to a writer.
75    pub fn write_to<W: Write>(&self, writer: &mut W) -> TimeResult<()> {
76        writer.write_all(&self.magic)?;
77        writer.write_all(&self.version.to_le_bytes())?;
78        writer.write_all(&self.flags.to_le_bytes())?;
79        writer.write_all(&self.entity_count.to_le_bytes())?;
80        writer.write_all(&self.index_offset.to_le_bytes())?;
81        writer.write_all(&self.deadline_index_offset.to_le_bytes())?;
82        writer.write_all(&self.decay_index_offset.to_le_bytes())?;
83        writer.write_all(&self.created_at.to_le_bytes())?;
84        writer.write_all(&self.modified_at.to_le_bytes())?;
85        writer.write_all(&self.checksum)?;
86        Ok(())
87    }
88
89    /// Read header from a reader.
90    pub fn read_from<R: Read>(reader: &mut R) -> TimeResult<Self> {
91        let mut magic = [0u8; 4];
92        reader.read_exact(&mut magic)?;
93
94        if magic != MAGIC {
95            return Err(TimeError::FileFormat(format!(
96                "Invalid magic bytes: {:?} (expected {:?}). File may be corrupted — try creating a new .atime file.",
97                magic, MAGIC
98            )));
99        }
100
101        let mut buf4 = [0u8; 4];
102        let mut buf8 = [0u8; 8];
103        let mut checksum = [0u8; 32];
104
105        reader.read_exact(&mut buf4)?;
106        let version = u32::from_le_bytes(buf4);
107
108        reader.read_exact(&mut buf4)?;
109        let flags = u32::from_le_bytes(buf4);
110
111        reader.read_exact(&mut buf8)?;
112        let entity_count = u64::from_le_bytes(buf8);
113
114        reader.read_exact(&mut buf8)?;
115        let index_offset = u64::from_le_bytes(buf8);
116
117        reader.read_exact(&mut buf8)?;
118        let deadline_index_offset = u64::from_le_bytes(buf8);
119
120        reader.read_exact(&mut buf8)?;
121        let decay_index_offset = u64::from_le_bytes(buf8);
122
123        reader.read_exact(&mut buf8)?;
124        let created_at = u64::from_le_bytes(buf8);
125
126        reader.read_exact(&mut buf8)?;
127        let modified_at = u64::from_le_bytes(buf8);
128
129        reader.read_exact(&mut checksum)?;
130
131        Ok(Self {
132            magic,
133            version,
134            flags,
135            entity_count,
136            index_offset,
137            deadline_index_offset,
138            decay_index_offset,
139            created_at,
140            modified_at,
141            checksum,
142        })
143    }
144}
145
146/// Entity types stored in file.
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148#[repr(u8)]
149pub enum EntityType {
150    /// Duration estimate.
151    Duration = 1,
152    /// Deadline.
153    Deadline = 2,
154    /// Schedule.
155    Schedule = 3,
156    /// Sequence.
157    Sequence = 4,
158    /// Decay model.
159    Decay = 5,
160}
161
162impl TryFrom<u8> for EntityType {
163    type Error = TimeError;
164
165    fn try_from(value: u8) -> Result<Self, Self::Error> {
166        match value {
167            1 => Ok(EntityType::Duration),
168            2 => Ok(EntityType::Deadline),
169            3 => Ok(EntityType::Schedule),
170            4 => Ok(EntityType::Sequence),
171            5 => Ok(EntityType::Decay),
172            _ => Err(TimeError::FileFormat(format!(
173                "Unknown entity type: {}",
174                value
175            ))),
176        }
177    }
178}
179
180/// Entity block — wraps a serialized temporal entity.
181#[derive(Debug, Clone)]
182pub struct EntityBlock {
183    /// Entity type.
184    pub entity_type: EntityType,
185    /// Entity ID.
186    pub id: TemporalId,
187    /// Payload length.
188    pub payload_len: u32,
189    /// Payload (JSON-serialized entity).
190    pub payload: Vec<u8>,
191}
192
193impl EntityBlock {
194    /// Create a new entity block from a serializable entity.
195    pub fn new<T: Serialize>(
196        entity_type: EntityType,
197        id: TemporalId,
198        entity: &T,
199    ) -> TimeResult<Self> {
200        let payload = serde_json::to_vec(entity)?;
201        Ok(Self {
202            entity_type,
203            id,
204            payload_len: payload.len() as u32,
205            payload,
206        })
207    }
208
209    /// Write block to a writer.
210    pub fn write_to<W: Write>(&self, writer: &mut W) -> TimeResult<()> {
211        writer.write_all(&[self.entity_type as u8])?;
212        writer.write_all(self.id.0.as_bytes())?;
213        writer.write_all(&self.payload_len.to_le_bytes())?;
214        writer.write_all(&self.payload)?;
215        Ok(())
216    }
217
218    /// Read block from a reader.
219    pub fn read_from<R: Read>(reader: &mut R) -> TimeResult<Self> {
220        let mut type_buf = [0u8; 1];
221        reader.read_exact(&mut type_buf)?;
222        let entity_type = EntityType::try_from(type_buf[0])?;
223
224        let mut id_buf = [0u8; 16];
225        reader.read_exact(&mut id_buf)?;
226        let id = TemporalId(uuid::Uuid::from_bytes(id_buf));
227
228        let mut len_buf = [0u8; 4];
229        reader.read_exact(&mut len_buf)?;
230        let payload_len = u32::from_le_bytes(len_buf);
231
232        let mut payload = vec![0u8; payload_len as usize];
233        reader.read_exact(&mut payload)?;
234
235        Ok(Self {
236            entity_type,
237            id,
238            payload_len,
239            payload,
240        })
241    }
242
243    /// Deserialize payload into a typed entity.
244    pub fn deserialize<T: for<'de> Deserialize<'de>>(&self) -> TimeResult<T> {
245        Ok(serde_json::from_slice(&self.payload)?)
246    }
247}
248
249/// The complete `.atime` file — a temporal graph stored as a binary file.
250#[derive(Debug)]
251pub struct TimeFile {
252    /// File path.
253    pub path: PathBuf,
254    /// Header.
255    pub header: FileHeader,
256    /// All entities.
257    entities: HashMap<TemporalId, EntityBlock>,
258}
259
260impl TimeFile {
261    /// Create a new empty `.atime` file.
262    pub fn create(path: impl Into<PathBuf>) -> TimeResult<Self> {
263        let path = path.into();
264        let header = FileHeader::new();
265
266        let file = Self {
267            path,
268            header,
269            entities: HashMap::new(),
270        };
271
272        file.save()?;
273        Ok(file)
274    }
275
276    /// Open an existing `.atime` file.
277    pub fn open(path: impl Into<PathBuf>) -> TimeResult<Self> {
278        let path = path.into();
279        let mut file = std::fs::File::open(&path)?;
280
281        let header = FileHeader::read_from(&mut file)?;
282
283        let mut entities = HashMap::new();
284        for _ in 0..header.entity_count {
285            let block = EntityBlock::read_from(&mut file)?;
286            entities.insert(block.id, block);
287        }
288
289        Ok(Self {
290            path,
291            header,
292            entities,
293        })
294    }
295
296    /// Save to disk (atomic write: temp + rename).
297    pub fn save(&self) -> TimeResult<()> {
298        let tmp_path = self.path.with_extension("atime.tmp");
299        let mut file = std::fs::File::create(&tmp_path)?;
300
301        let mut header = self.header.clone();
302        header.entity_count = self.entities.len() as u64;
303        header.modified_at = Utc::now().timestamp_micros() as u64;
304
305        header.write_to(&mut file)?;
306
307        for block in self.entities.values() {
308            block.write_to(&mut file)?;
309        }
310
311        file.flush()?;
312        std::fs::rename(&tmp_path, &self.path)?;
313
314        Ok(())
315    }
316
317    /// Add or replace an entity.
318    pub fn add<T: Serialize>(
319        &mut self,
320        entity_type: EntityType,
321        id: TemporalId,
322        entity: &T,
323    ) -> TimeResult<()> {
324        let block = EntityBlock::new(entity_type, id, entity)?;
325        self.entities.insert(id, block);
326        Ok(())
327    }
328
329    /// Get an entity by ID.
330    pub fn get<T: for<'de> Deserialize<'de>>(&self, id: &TemporalId) -> TimeResult<Option<T>> {
331        match self.entities.get(id) {
332            Some(block) => Ok(Some(block.deserialize()?)),
333            None => Ok(None),
334        }
335    }
336
337    /// Remove an entity by ID.
338    pub fn remove(&mut self, id: &TemporalId) -> bool {
339        self.entities.remove(id).is_some()
340    }
341
342    /// List all entity blocks of a given type.
343    pub fn list_by_type(&self, entity_type: EntityType) -> Vec<&EntityBlock> {
344        self.entities
345            .values()
346            .filter(|b| b.entity_type == entity_type)
347            .collect()
348    }
349
350    /// Total number of entities.
351    pub fn entity_count(&self) -> usize {
352        self.entities.len()
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359    use crate::deadline::Deadline;
360    use chrono::Duration as ChronoDuration;
361    use tempfile::tempdir;
362
363    #[test]
364    fn test_create_and_reopen() {
365        let dir = tempdir().unwrap();
366        let path = dir.path().join("test.atime");
367
368        // Create
369        let mut tf = TimeFile::create(&path).unwrap();
370        let d = Deadline::new("Ship v1", Utc::now() + ChronoDuration::hours(24));
371        let id = d.id;
372        tf.add(EntityType::Deadline, id, &d).unwrap();
373        tf.save().unwrap();
374
375        // Reopen
376        let tf2 = TimeFile::open(&path).unwrap();
377        assert_eq!(tf2.entity_count(), 1);
378        let loaded: Deadline = tf2.get(&id).unwrap().unwrap();
379        assert_eq!(loaded.label, "Ship v1");
380    }
381
382    #[test]
383    fn test_remove_entity() {
384        let dir = tempdir().unwrap();
385        let path = dir.path().join("test.atime");
386
387        let mut tf = TimeFile::create(&path).unwrap();
388        let d = Deadline::new("Remove me", Utc::now() + ChronoDuration::hours(1));
389        let id = d.id;
390        tf.add(EntityType::Deadline, id, &d).unwrap();
391        assert_eq!(tf.entity_count(), 1);
392
393        assert!(tf.remove(&id));
394        assert_eq!(tf.entity_count(), 0);
395    }
396
397    #[test]
398    fn test_empty_file() {
399        let dir = tempdir().unwrap();
400        let path = dir.path().join("empty.atime");
401
402        let tf = TimeFile::create(&path).unwrap();
403        assert_eq!(tf.entity_count(), 0);
404        assert!(tf.list_by_type(EntityType::Deadline).is_empty());
405
406        let tf2 = TimeFile::open(&path).unwrap();
407        assert_eq!(tf2.entity_count(), 0);
408    }
409
410    #[test]
411    fn test_invalid_magic() {
412        let dir = tempdir().unwrap();
413        let path = dir.path().join("bad.atime");
414        std::fs::write(&path, b"BAD_DATA_NOT_ATIM").unwrap();
415
416        let result = TimeFile::open(&path);
417        assert!(result.is_err());
418        let err = result.unwrap_err().to_string();
419        assert!(err.contains("Invalid magic bytes"));
420    }
421}