Skip to main content

agentic_sdk/
file_format.rs

1//! File format specifications for 20-year compatibility.
2//!
3//! Sisters use their OWN binary file headers (AMEM, AVIS, ACDB, ATIM)
4//! or JSON (Identity). This module provides traits and utilities that
5//! unify file format operations without forcing a single header layout.
6//!
7//! # The 20-Year Promise
8//!
9//! Any .a* file created today will be readable in 2046.
10//!
11//! # Reality (v0.2.0)
12//!
13//! Each sister has its own header format:
14//! - Memory:   "AMEM" magic, 64-byte header
15//! - Vision:   "AVIS" magic, 64-byte header
16//! - Codebase: "ACDB" magic, 128-byte header
17//! - Time:     "ATIM" magic, 92-byte header
18//! - Identity: JSON files (no binary header)
19//!
20//! The v0.1.0 `SisterFileHeader` (96-byte "AGNT" magic) was never adopted.
21//! v0.2.0 replaces it with a trait-based approach that each sister
22//! implements according to its actual format.
23
24use crate::errors::{ErrorCode, SisterError, SisterResult};
25use crate::types::{SisterType, Version};
26use chrono::{DateTime, Utc};
27use serde::{Deserialize, Serialize};
28use std::path::Path;
29
30/// Information about a file (without loading full content).
31///
32/// Every sister can produce this from any of its files,
33/// regardless of whether the format is binary or JSON.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct FileInfo {
36    /// Which sister owns this file
37    pub sister_type: SisterType,
38
39    /// Format version of the file
40    pub version: Version,
41
42    /// When the file was created
43    pub created_at: DateTime<Utc>,
44
45    /// When the file was last modified
46    pub updated_at: DateTime<Utc>,
47
48    /// Content length in bytes (payload, excluding header)
49    pub content_length: u64,
50
51    /// Whether this file needs migration to the current version
52    pub needs_migration: bool,
53
54    /// The magic bytes or format identifier (e.g., "AMEM", "AVIS", "aid-v1")
55    pub format_id: String,
56}
57
58/// File format reader trait for all sisters.
59///
60/// Each sister implements this for its own file format.
61/// Memory reads .amem files, Vision reads .avis files, etc.
62pub trait FileFormatReader: Sized {
63    /// Read a file with version handling
64    fn read_file(path: &Path) -> SisterResult<Self>;
65
66    /// Check if a file is readable (without full parse).
67    /// Returns file info for quick inspection
68    fn can_read(path: &Path) -> SisterResult<FileInfo>;
69
70    /// Get file version without full parse
71    fn file_version(path: &Path) -> SisterResult<Version>;
72
73    /// Migrate old version data to current format (in memory).
74    /// Returns the migrated bytes
75    fn migrate(data: &[u8], from_version: Version) -> SisterResult<Vec<u8>>;
76}
77
78/// File format writer trait for all sisters
79pub trait FileFormatWriter {
80    /// Write to a file path
81    fn write_file(&self, path: &Path) -> SisterResult<()>;
82
83    /// Serialize the content to bytes
84    fn to_bytes(&self) -> SisterResult<Vec<u8>>;
85}
86
87/// Version compatibility rules.
88///
89/// These rules ensure the 20-year compatibility promise.
90#[derive(Debug, Clone)]
91pub struct VersionCompatibility;
92
93impl VersionCompatibility {
94    /// Check if reader version can read file version.
95    ///
96    /// Rule: Newer readers can always read older files
97    pub fn can_read(reader_version: &Version, file_version: &Version) -> bool {
98        reader_version.major >= file_version.major
99    }
100
101    /// Check if file needs migration
102    pub fn needs_migration(current_version: &Version, file_version: &Version) -> bool {
103        file_version.major < current_version.major
104    }
105
106    /// Check if versions are fully compatible (same major)
107    pub fn is_compatible(v1: &Version, v2: &Version) -> bool {
108        v1.major == v2.major
109    }
110}
111
112/// Helper: Read 4-byte magic from a file path.
113///
114/// Useful for sisters with binary formats to quickly identify files.
115pub fn read_magic_bytes(path: &Path) -> SisterResult<[u8; 4]> {
116    use std::io::Read;
117    let mut file = std::fs::File::open(path)?;
118    let mut magic = [0u8; 4];
119    file.read_exact(&mut magic).map_err(|e| {
120        SisterError::new(
121            ErrorCode::StorageError,
122            format!("Failed to read magic bytes: {}", e),
123        )
124    })?;
125    Ok(magic)
126}
127
128/// Helper: Identify which sister a file belongs to by magic bytes.
129///
130/// Known magic bytes:
131/// - "AMEM" (0x414D454D) → Memory
132/// - "AVIS" (0x41564953) → Vision
133/// - "ACDB" (0x41434442) → Codebase
134/// - "ATIM" (0x4154494D) → Time
135/// - "ACON" (0x41434F4E) → Contract
136///
137/// Returns None for JSON-based formats (Identity) or unknown formats.
138pub fn identify_sister_by_magic(magic: &[u8; 4]) -> Option<SisterType> {
139    match magic {
140        b"AMEM" => Some(SisterType::Memory),
141        b"AVIS" => Some(SisterType::Vision),
142        b"ACDB" => Some(SisterType::Codebase),
143        b"ATIM" => Some(SisterType::Time),
144        b"ACON" => Some(SisterType::Contract),
145        _ => None,
146    }
147}
148
149/// Helper: Check if a file is a JSON-based sister format (e.g., Identity .aid files).
150///
151/// Peeks at the first non-whitespace byte to see if it's '{'.
152pub fn is_json_format(path: &Path) -> SisterResult<bool> {
153    use std::io::Read;
154    let mut file = std::fs::File::open(path)?;
155    let mut buf = [0u8; 64];
156    let n = file.read(&mut buf).map_err(|e| {
157        SisterError::new(
158            ErrorCode::StorageError,
159            format!("Failed to read file: {}", e),
160        )
161    })?;
162    let slice = &buf[..n];
163    Ok(slice.iter().find(|b| !b.is_ascii_whitespace()) == Some(&b'{'))
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_identify_sister_by_magic() {
172        assert_eq!(identify_sister_by_magic(b"AMEM"), Some(SisterType::Memory));
173        assert_eq!(identify_sister_by_magic(b"AVIS"), Some(SisterType::Vision));
174        assert_eq!(
175            identify_sister_by_magic(b"ACDB"),
176            Some(SisterType::Codebase)
177        );
178        assert_eq!(identify_sister_by_magic(b"ATIM"), Some(SisterType::Time));
179        assert_eq!(
180            identify_sister_by_magic(b"ACON"),
181            Some(SisterType::Contract)
182        );
183        assert_eq!(identify_sister_by_magic(b"XXXX"), None);
184        assert_eq!(identify_sister_by_magic(b"AGNT"), None); // v0.1.0 magic, no longer used
185    }
186
187    #[test]
188    fn test_version_compatibility() {
189        let v1 = Version::new(1, 0, 0);
190        let v2 = Version::new(2, 0, 0);
191        let v1_1 = Version::new(1, 1, 0);
192
193        assert!(VersionCompatibility::can_read(&v2, &v1));
194        assert!(!VersionCompatibility::can_read(&v1, &v2));
195        assert!(VersionCompatibility::is_compatible(&v1, &v1_1));
196        assert!(VersionCompatibility::needs_migration(&v2, &v1));
197    }
198}