Skip to main content

agentic_contracts/
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///
136/// Returns None for JSON-based formats (Identity) or unknown formats.
137pub fn identify_sister_by_magic(magic: &[u8; 4]) -> Option<SisterType> {
138    match magic {
139        b"AMEM" => Some(SisterType::Memory),
140        b"AVIS" => Some(SisterType::Vision),
141        b"ACDB" => Some(SisterType::Codebase),
142        b"ATIM" => Some(SisterType::Time),
143        _ => None,
144    }
145}
146
147/// Helper: Check if a file is a JSON-based sister format (e.g., Identity .aid files).
148///
149/// Peeks at the first non-whitespace byte to see if it's '{'.
150pub fn is_json_format(path: &Path) -> SisterResult<bool> {
151    use std::io::Read;
152    let mut file = std::fs::File::open(path)?;
153    let mut buf = [0u8; 64];
154    let n = file.read(&mut buf).map_err(|e| {
155        SisterError::new(
156            ErrorCode::StorageError,
157            format!("Failed to read file: {}", e),
158        )
159    })?;
160    let slice = &buf[..n];
161    Ok(slice.iter().find(|b| !b.is_ascii_whitespace()) == Some(&b'{'))
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn test_identify_sister_by_magic() {
170        assert_eq!(identify_sister_by_magic(b"AMEM"), Some(SisterType::Memory));
171        assert_eq!(identify_sister_by_magic(b"AVIS"), Some(SisterType::Vision));
172        assert_eq!(
173            identify_sister_by_magic(b"ACDB"),
174            Some(SisterType::Codebase)
175        );
176        assert_eq!(identify_sister_by_magic(b"ATIM"), Some(SisterType::Time));
177        assert_eq!(identify_sister_by_magic(b"XXXX"), None);
178        assert_eq!(identify_sister_by_magic(b"AGNT"), None); // v0.1.0 magic, no longer used
179    }
180
181    #[test]
182    fn test_version_compatibility() {
183        let v1 = Version::new(1, 0, 0);
184        let v2 = Version::new(2, 0, 0);
185        let v1_1 = Version::new(1, 1, 0);
186
187        assert!(VersionCompatibility::can_read(&v2, &v1));
188        assert!(!VersionCompatibility::can_read(&v1, &v2));
189        assert!(VersionCompatibility::is_compatible(&v1, &v1_1));
190        assert!(VersionCompatibility::needs_migration(&v2, &v1));
191    }
192}