Skip to main content

oxirs_core/stability/
compatibility.rs

1//! # OxiRS Version Compatibility Matrix
2//!
3//! This module tracks the version history of OxiRS, LTS policy, breaking changes,
4//! and compatibility guarantees for dependent projects.
5
6/// A single entry in the OxiRS version history.
7#[derive(Debug, Clone)]
8pub struct VersionEntry {
9    /// The release version string (e.g. "1.0.0").
10    pub version: &'static str,
11    /// ISO 8601 release date (e.g. "2026-03-01").
12    pub release_date: &'static str,
13    /// Whether this is a Long-Term Support release.
14    pub lts: bool,
15    /// End-of-life date for security patches (None = not yet determined).
16    pub supported_until: Option<&'static str>,
17    /// List of breaking API changes introduced in this version.
18    pub breaking_changes: Vec<&'static str>,
19    /// APIs deprecated in this version.
20    pub deprecated_in: Vec<&'static str>,
21}
22
23impl VersionEntry {
24    /// Returns true if this version is currently within its support window.
25    ///
26    /// Uses a conservative heuristic: returns true unless `supported_until` is set
27    /// and is known to be in the past (hardcoded reference date: 2026-02-24).
28    pub fn is_supported(&self) -> bool {
29        match self.supported_until {
30            None => true,
31            Some(eol) => eol >= "2026-02-24",
32        }
33    }
34
35    /// Returns the number of breaking changes in this release.
36    pub fn breaking_change_count(&self) -> usize {
37        self.breaking_changes.len()
38    }
39
40    /// Returns the number of deprecations in this release.
41    pub fn deprecation_count(&self) -> usize {
42        self.deprecated_in.len()
43    }
44}
45
46/// OxiRS LTS policy constants.
47pub struct LtsPolicy;
48
49impl LtsPolicy {
50    /// LTS releases receive security patches for this many months after EOL of next LTS.
51    pub const SECURITY_PATCH_MONTHS: u32 = 24;
52
53    /// LTS releases receive bug-fix patches for this many months.
54    pub const BUG_FIX_PATCH_MONTHS: u32 = 12;
55
56    /// Breaking changes are only introduced in major versions.
57    pub const BREAKING_CHANGES_IN_MAJOR_ONLY: bool = true;
58
59    /// Security patches are backported to the current LTS series.
60    pub const SECURITY_BACKPORT_TO_LTS: bool = true;
61
62    /// API deprecations must survive at least one full minor version before removal.
63    pub const MIN_DEPRECATION_NOTICE_VERSIONS: u32 = 1;
64}
65
66/// The version compatibility matrix for OxiRS.
67#[derive(Debug)]
68pub struct CompatibilityMatrix {
69    /// All known OxiRS releases, ordered from oldest to newest.
70    pub versions: Vec<VersionEntry>,
71}
72
73impl CompatibilityMatrix {
74    /// Creates an empty compatibility matrix.
75    pub fn new() -> Self {
76        Self {
77            versions: Vec::new(),
78        }
79    }
80
81    /// Returns the canonical OxiRS release history.
82    pub fn oxirs_history() -> Self {
83        let mut matrix = Self::new();
84
85        matrix.versions.push(VersionEntry {
86            version: "0.1.0",
87            release_date: "2025-12-01",
88            lts: false,
89            supported_until: Some("2026-03-01"),
90            breaking_changes: vec![],
91            deprecated_in: vec![],
92        });
93
94        matrix.versions.push(VersionEntry {
95            version: "0.1.1",
96            release_date: "2025-12-15",
97            lts: false,
98            supported_until: Some("2026-03-01"),
99            breaking_changes: vec![],
100            deprecated_in: vec![],
101        });
102
103        matrix.versions.push(VersionEntry {
104            version: "0.1.2",
105            release_date: "2026-01-05",
106            lts: false,
107            supported_until: Some("2026-03-01"),
108            breaking_changes: vec![],
109            deprecated_in: vec![],
110        });
111
112        matrix.versions.push(VersionEntry {
113            version: "0.2.0",
114            release_date: "2026-02-24",
115            lts: false,
116            supported_until: Some("2026-06-01"),
117            breaking_changes: vec![
118                "oxirs-star: Renamed Iri to NamedNode for consistency with RDF 1.2 spec",
119                "oxirs-vec: Removed persistence.rs in favour of modular persistence/ directory",
120                "oxirs-wasm: Removed monolithic parser.rs, store.rs in favour of sub-modules",
121                "oxirs-stream: Removed state.rs; state management moved to state/ sub-module",
122            ],
123            deprecated_in: vec!["RdfStore::legacy_query() - use RdfStore::query() instead"],
124        });
125
126        matrix.versions.push(VersionEntry {
127            version: "0.3.0",
128            release_date: "2026-04-01",
129            lts: false,
130            supported_until: Some("2026-09-01"),
131            breaking_changes: vec![],
132            deprecated_in: vec![],
133        });
134
135        matrix.versions.push(VersionEntry {
136            version: "1.0.0",
137            release_date: "2026-06-01",
138            lts: true,
139            supported_until: Some("2028-06-01"),
140            breaking_changes: vec![
141                "Minimum Rust edition raised to 2024",
142                "All previously Deprecated APIs removed from public surface",
143                "WASM API now requires explicit initialisation via oxirs_wasm::init()",
144                "StabilityLevel enum is now #[non_exhaustive] to allow future variants",
145            ],
146            deprecated_in: vec![],
147        });
148
149        matrix
150    }
151
152    /// Returns only LTS releases from the history.
153    pub fn lts_releases(&self) -> Vec<&VersionEntry> {
154        self.versions.iter().filter(|v| v.lts).collect()
155    }
156
157    /// Returns only currently supported releases.
158    pub fn supported_releases(&self) -> Vec<&VersionEntry> {
159        self.versions.iter().filter(|v| v.is_supported()).collect()
160    }
161
162    /// Returns the latest release entry, if any.
163    pub fn latest(&self) -> Option<&VersionEntry> {
164        self.versions.last()
165    }
166
167    /// Finds a release by exact version string.
168    pub fn find_version(&self, version: &str) -> Option<&VersionEntry> {
169        self.versions.iter().find(|v| v.version == version)
170    }
171
172    /// Returns all releases that introduced at least one breaking change.
173    pub fn releases_with_breaking_changes(&self) -> Vec<&VersionEntry> {
174        self.versions
175            .iter()
176            .filter(|v| !v.breaking_changes.is_empty())
177            .collect()
178    }
179
180    /// Returns all releases that deprecated at least one API.
181    pub fn releases_with_deprecations(&self) -> Vec<&VersionEntry> {
182        self.versions
183            .iter()
184            .filter(|v| !v.deprecated_in.is_empty())
185            .collect()
186    }
187
188    /// Generates a Markdown compatibility report.
189    pub fn generate_report(&self) -> String {
190        let mut report = String::with_capacity(4096);
191        report.push_str("# OxiRS Version Compatibility Matrix\n\n");
192        report.push_str("## LTS Policy\n\n");
193        report.push_str(&format!(
194            "- Security patches backported to LTS: {}\n",
195            LtsPolicy::SECURITY_BACKPORT_TO_LTS
196        ));
197        report.push_str(&format!(
198            "- Security patch support window: {} months\n",
199            LtsPolicy::SECURITY_PATCH_MONTHS
200        ));
201        report.push_str(&format!(
202            "- Bug-fix support window: {} months\n",
203            LtsPolicy::BUG_FIX_PATCH_MONTHS
204        ));
205        report.push_str(&format!(
206            "- Breaking changes in major versions only: {}\n",
207            LtsPolicy::BREAKING_CHANGES_IN_MAJOR_ONLY
208        ));
209        report.push_str(&format!(
210            "- Minimum deprecation notice: {} minor version(s)\n\n",
211            LtsPolicy::MIN_DEPRECATION_NOTICE_VERSIONS
212        ));
213
214        report.push_str("## Release History\n\n");
215        report.push_str("| Version | Release Date | LTS | Supported Until | Breaking Changes | Deprecations |\n");
216        report.push_str(
217            "|---------|-------------|-----|----------------|-----------------|-------------|\n",
218        );
219        for v in &self.versions {
220            let lts_flag = if v.lts { "YES" } else { "No" };
221            let eol = v.supported_until.unwrap_or("TBD");
222            report.push_str(&format!(
223                "| {} | {} | {} | {} | {} | {} |\n",
224                v.version,
225                v.release_date,
226                lts_flag,
227                eol,
228                v.breaking_changes.len(),
229                v.deprecated_in.len()
230            ));
231        }
232        report.push('\n');
233
234        // Breaking changes detail
235        for v in &self.versions {
236            if !v.breaking_changes.is_empty() {
237                report.push_str(&format!("### Breaking changes in {}\n\n", v.version));
238                for change in &v.breaking_changes {
239                    report.push_str(&format!("- {change}\n"));
240                }
241                report.push('\n');
242            }
243        }
244
245        report
246    }
247}
248
249impl Default for CompatibilityMatrix {
250    fn default() -> Self {
251        Self::new()
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    fn matrix() -> CompatibilityMatrix {
260        CompatibilityMatrix::oxirs_history()
261    }
262
263    #[test]
264    fn test_matrix_is_non_empty() {
265        assert!(!matrix().versions.is_empty());
266    }
267
268    #[test]
269    fn test_matrix_has_v1_lts() {
270        let m = matrix();
271        let lts = m.lts_releases();
272        assert!(!lts.is_empty(), "Should have at least one LTS release");
273        assert!(lts.iter().any(|v| v.version == "1.0.0"));
274    }
275
276    #[test]
277    fn test_v1_is_lts() {
278        let m = matrix();
279        let v1 = m.find_version("1.0.0");
280        assert!(v1.is_some());
281        assert!(v1.expect("version should be registered").lts);
282    }
283
284    #[test]
285    fn test_v1_supported_until_set() {
286        let m = matrix();
287        let v1 = m.find_version("1.0.0").expect("operation should succeed");
288        assert!(v1.supported_until.is_some());
289    }
290
291    #[test]
292    fn test_v1_supported_until_two_years() {
293        let m = matrix();
294        let v1 = m.find_version("1.0.0").expect("operation should succeed");
295        let eol = v1.supported_until.expect("supported_until should be set");
296        assert!(
297            eol >= "2028-01-01",
298            "v1.0.0 LTS should be supported at least until 2028"
299        );
300    }
301
302    #[test]
303    fn test_v010_exists() {
304        assert!(matrix().find_version("0.1.0").is_some());
305    }
306
307    #[test]
308    fn test_v020_exists() {
309        assert!(matrix().find_version("0.2.0").is_some());
310    }
311
312    #[test]
313    fn test_v010_is_not_lts() {
314        let m = matrix();
315        let v = m.find_version("0.1.0").expect("operation should succeed");
316        assert!(!v.lts);
317    }
318
319    #[test]
320    fn test_v020_has_breaking_changes() {
321        let m = matrix();
322        let v = m.find_version("0.2.0").expect("operation should succeed");
323        assert!(!v.breaking_changes.is_empty());
324    }
325
326    #[test]
327    fn test_v010_has_no_breaking_changes() {
328        let m = matrix();
329        let v = m.find_version("0.1.0").expect("operation should succeed");
330        assert!(v.breaking_changes.is_empty());
331    }
332
333    #[test]
334    fn test_lts_policy_constants() {
335        assert_eq!(LtsPolicy::SECURITY_PATCH_MONTHS, 24);
336        assert_eq!(LtsPolicy::BUG_FIX_PATCH_MONTHS, 12);
337        const _: () = assert!(LtsPolicy::BREAKING_CHANGES_IN_MAJOR_ONLY);
338        const _: () = assert!(LtsPolicy::SECURITY_BACKPORT_TO_LTS);
339    }
340
341    #[test]
342    fn test_latest_is_v100() {
343        let m = matrix();
344        let latest = m.latest().expect("operation should succeed");
345        assert_eq!(latest.version, "1.0.0");
346    }
347
348    #[test]
349    fn test_releases_with_breaking_changes_non_empty() {
350        let m = matrix();
351        assert!(!m.releases_with_breaking_changes().is_empty());
352    }
353
354    #[test]
355    fn test_releases_with_deprecations_non_empty() {
356        let m = matrix();
357        assert!(!m.releases_with_deprecations().is_empty());
358    }
359
360    #[test]
361    fn test_supported_releases_non_empty() {
362        let m = matrix();
363        assert!(!m.supported_releases().is_empty());
364    }
365
366    #[test]
367    fn test_find_nonexistent_version_returns_none() {
368        let m = matrix();
369        assert!(m.find_version("99.99.99").is_none());
370    }
371
372    #[test]
373    fn test_version_entry_breaking_change_count() {
374        let m = matrix();
375        let v = m.find_version("0.2.0").expect("operation should succeed");
376        assert_eq!(v.breaking_change_count(), v.breaking_changes.len());
377    }
378
379    #[test]
380    fn test_version_entry_deprecation_count() {
381        let m = matrix();
382        let v = m.find_version("0.2.0").expect("operation should succeed");
383        assert_eq!(v.deprecation_count(), v.deprecated_in.len());
384    }
385
386    #[test]
387    fn test_generate_report_non_empty() {
388        assert!(!matrix().generate_report().is_empty());
389    }
390
391    #[test]
392    fn test_generate_report_contains_lts_policy() {
393        let report = matrix().generate_report();
394        assert!(report.contains("LTS Policy"));
395    }
396
397    #[test]
398    fn test_generate_report_contains_release_history() {
399        let report = matrix().generate_report();
400        assert!(report.contains("Release History"));
401    }
402
403    #[test]
404    fn test_generate_report_contains_v1() {
405        let report = matrix().generate_report();
406        assert!(report.contains("1.0.0"));
407    }
408
409    #[test]
410    fn test_default_matrix_is_empty() {
411        let m = CompatibilityMatrix::default();
412        assert!(m.versions.is_empty());
413    }
414}