Skip to main content

ries_rs/
manifest.rs

1//! Run manifest for reproducibility
2//!
3//! Provides structured output of search configuration and results
4//! for academic reproducibility and verification.
5
6use serde::{Deserialize, Serialize};
7use std::time::{SystemTime, UNIX_EPOCH};
8
9/// Complete manifest of a search run
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct RunManifest {
12    /// Version of ries-rs
13    pub version: String,
14    /// Git commit hash (if available)
15    pub git_hash: Option<String>,
16    /// Timestamp of the run (ISO 8601)
17    pub timestamp: String,
18    /// Platform/OS info
19    pub platform: PlatformInfo,
20    /// Search configuration
21    pub config: SearchConfigInfo,
22    /// Top matches found
23    pub results: Vec<MatchInfo>,
24}
25
26/// Platform information
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct PlatformInfo {
29    /// Operating system
30    pub os: String,
31    /// Architecture
32    pub arch: String,
33    /// Rust version used to compile
34    pub rust_version: String,
35}
36
37/// Search configuration summary
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct SearchConfigInfo {
40    /// Target value searched for
41    pub target: f64,
42    /// Search level
43    pub level: f32,
44    /// Maximum LHS complexity
45    pub max_lhs_complexity: u32,
46    /// Maximum RHS complexity
47    pub max_rhs_complexity: u32,
48    /// Whether deterministic mode was enabled
49    pub deterministic: bool,
50    /// Whether parallel search was used
51    pub parallel: bool,
52    /// Maximum error tolerance
53    pub max_error: f64,
54    /// Maximum matches requested
55    pub max_matches: usize,
56    /// Ranking mode used
57    pub ranking_mode: String,
58    /// User constants (names and values)
59    pub user_constants: Vec<UserConstantInfo>,
60    /// Excluded symbols
61    pub excluded_symbols: Vec<String>,
62    /// Allowed symbols (if restricted)
63    pub allowed_symbols: Option<Vec<String>>,
64}
65
66/// User constant information
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct UserConstantInfo {
69    /// Name of the constant
70    pub name: String,
71    /// Value of the constant
72    pub value: f64,
73    /// Description
74    pub description: String,
75}
76
77/// Match result information
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct MatchInfo {
80    /// LHS expression (postfix)
81    pub lhs_postfix: String,
82    /// RHS expression (postfix)
83    pub rhs_postfix: String,
84    /// LHS expression (infix)
85    pub lhs_infix: String,
86    /// RHS expression (infix)
87    pub rhs_infix: String,
88    /// Error (absolute)
89    pub error: f64,
90    /// Whether this is an exact match
91    pub is_exact: bool,
92    /// Complexity score
93    pub complexity: u32,
94    /// X value that solves the equation
95    pub x_value: f64,
96    /// Stability score (0-1, higher is better)
97    pub stability: Option<f64>,
98}
99
100impl RunManifest {
101    /// Create a new manifest with current timestamp
102    pub fn new(config: SearchConfigInfo, results: Vec<MatchInfo>) -> Self {
103        let timestamp = SystemTime::now()
104            .duration_since(UNIX_EPOCH)
105            .map(|d| {
106                let secs = d.as_secs();
107                // Convert to ISO 8601-like format
108                chrono_like_timestamp(secs)
109            })
110            .unwrap_or_else(|_| "unknown".to_string());
111
112        Self {
113            version: env!("CARGO_PKG_VERSION").to_string(),
114            git_hash: get_git_hash(),
115            timestamp,
116            platform: PlatformInfo::current(),
117            config,
118            results,
119        }
120    }
121
122    /// Serialize to JSON
123    pub fn to_json(&self) -> Result<String, serde_json::Error> {
124        serde_json::to_string_pretty(self)
125    }
126
127    /// Serialize to JSON with compact format
128    pub fn to_json_compact(&self) -> Result<String, serde_json::Error> {
129        serde_json::to_string(self)
130    }
131}
132
133impl PlatformInfo {
134    /// Get current platform info
135    pub fn current() -> Self {
136        Self {
137            os: std::env::consts::OS.to_string(),
138            arch: std::env::consts::ARCH.to_string(),
139            rust_version: rustc_version().unwrap_or_else(|| "unknown".to_string()),
140        }
141    }
142}
143
144/// Get git commit hash from build time
145fn get_git_hash() -> Option<String> {
146    // Try to get from environment variable set during build
147    option_env!("GIT_HASH").map(|s| s.to_string()).or_else(|| {
148        // Fallback: try to read from .git at runtime (for development)
149        #[cfg(debug_assertions)]
150        {
151            std::process::Command::new("git")
152                .args(["rev-parse", "--short", "HEAD"])
153                .output()
154                .ok()
155                .and_then(|o| String::from_utf8(o.stdout).ok())
156                .map(|s| s.trim().to_string())
157        }
158        #[cfg(not(debug_assertions))]
159        {
160            None
161        }
162    })
163}
164
165/// Get rustc version
166fn rustc_version() -> Option<String> {
167    // In debug builds, try to get rustc version
168    #[cfg(debug_assertions)]
169    {
170        std::process::Command::new("rustc")
171            .arg("--version")
172            .output()
173            .ok()
174            .and_then(|o| String::from_utf8(o.stdout).ok())
175            .map(|s| s.trim().to_string())
176    }
177    #[cfg(not(debug_assertions))]
178    {
179        None
180    }
181}
182
183/// Create an ISO 8601-like timestamp from unix seconds
184fn chrono_like_timestamp(secs: u64) -> String {
185    // Simple implementation without chrono dependency
186    let days = secs / 86400;
187    let remaining = secs % 86400;
188    let hours = remaining / 3600;
189    let minutes = (remaining % 3600) / 60;
190    let seconds = remaining % 60;
191
192    // Unix epoch is 1970-01-01
193    // Calculate year, month, day from days since epoch
194    let (year, month, day) = days_to_ymd(days);
195
196    format!(
197        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
198        year, month, day, hours, minutes, seconds
199    )
200}
201
202/// Convert days since Unix epoch to (year, month, day)
203fn days_to_ymd(days: u64) -> (i32, u32, u32) {
204    // Start from 1970-01-01
205    let mut year = 1970_i32;
206    let mut remaining_days = days as i64;
207
208    loop {
209        let days_in_year = if is_leap_year(year) { 366 } else { 365 };
210        if remaining_days < days_in_year {
211            break;
212        }
213        remaining_days -= days_in_year;
214        year += 1;
215    }
216
217    let days_in_months = if is_leap_year(year) {
218        [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
219    } else {
220        [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
221    };
222
223    let mut month = 1_u32;
224    for &days_in_month in &days_in_months {
225        if remaining_days < days_in_month as i64 {
226            break;
227        }
228        remaining_days -= days_in_month as i64;
229        month += 1;
230    }
231
232    let day = (remaining_days + 1) as u32; // Days are 1-indexed
233    (year, month, day)
234}
235
236fn is_leap_year(year: i32) -> bool {
237    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    #[test]
245    fn test_timestamp_format() {
246        // 2024-01-15 12:30:45 UTC = 1705318245 seconds since epoch
247        let ts = chrono_like_timestamp(1705318245);
248        assert!(ts.starts_with("2024-01-"));
249        assert!(ts.ends_with("Z"));
250    }
251
252    #[test]
253    fn test_leap_year() {
254        assert!(is_leap_year(2024));
255        assert!(!is_leap_year(2023));
256        assert!(!is_leap_year(1900));
257        assert!(is_leap_year(2000));
258    }
259
260    #[test]
261    fn test_timestamp_leap_year_feb29() {
262        // 2000-02-29 00:00:00 UTC = unix timestamp 951782400
263        let ts = chrono_like_timestamp(951782400);
264        assert!(ts.starts_with("2000-02-29"), "got: {}", ts);
265    }
266
267    #[test]
268    fn test_timestamp_year_boundary() {
269        // 1999-12-31 23:59:59 UTC = 946684799
270        let ts = chrono_like_timestamp(946684799);
271        assert!(ts.starts_with("1999-12-31"), "got: {}", ts);
272        // 2000-01-01 00:00:00 UTC = 946684800
273        let ts2 = chrono_like_timestamp(946684800);
274        assert!(ts2.starts_with("2000-01-01"), "got: {}", ts2);
275    }
276
277    #[test]
278    fn test_leap_year_century_rules() {
279        // 1900: divisible by 100 but not 400 — NOT a leap year
280        assert!(!is_leap_year(1900));
281        // 2100: same
282        assert!(!is_leap_year(2100));
283        // 2000: divisible by 400 — IS a leap year
284        assert!(is_leap_year(2000));
285        // 2400: same
286        assert!(is_leap_year(2400));
287    }
288}