Skip to main content

kernel/
version.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! Optimistic concurrency: writes carry a
7//! `base_version`; a stale base yields a verdict, never a silent merge.
8//! The part `crates/kernel/spec/Version.lean` specifies this module.
9//!
10//! The mapping from [`VersionVerdict::Stale`] to `E_VERSION_CONFLICT`
11//! plus a fresh diff lives with the edit tool; kernel only judges
12//! freshness and knows nothing about diffs.
13
14use serde::{Deserialize, Serialize};
15
16use crate::error::{AxCode, AxError};
17
18/// Monotonic per-subject version; starts at [`Version::FIRST`].
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
20#[serde(transparent)]
21pub struct Version(u64);
22
23impl Version {
24    /// The first visible version of any subject.
25    pub const FIRST: Version = Version(1);
26
27    pub fn new(value: u64) -> Self {
28        Version(value)
29    }
30
31    pub fn value(&self) -> u64 {
32        self.0
33    }
34
35    pub fn next(self) -> Result<Version, AxError> {
36        self.0.checked_add(1).map(Version).ok_or_else(|| {
37            AxError::failure(AxCode::InvalidArgs, "advance version", self.0.to_string())
38                .with_recovery("version space exhausted for this subject")
39        })
40    }
41}
42
43/// Freshness verdict — exhaustive, not a bool (decision-function shape).
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum VersionVerdict {
46    Fresh,
47    Stale { current: Version },
48}
49
50/// Pure freshness judgment. A base ahead of `current` is caller fiction
51/// and reports `Stale` too: only `current` is the truth.
52pub fn check_base(current: Version, base: Version) -> VersionVerdict {
53    if base == current {
54        VersionVerdict::Fresh
55    } else {
56        VersionVerdict::Stale { current }
57    }
58}
59
60#[cfg(test)]
61#[allow(
62    clippy::unwrap_used,
63    clippy::expect_used,
64    clippy::panic,
65    clippy::indexing_slicing,
66    reason = "test code"
67)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn first_version_is_one_and_advances_checked() {
73        assert_eq!(Version::FIRST.value(), 1);
74        assert_eq!(Version::FIRST.next().unwrap().value(), 2);
75        assert!(Version::new(u64::MAX).next().is_err());
76    }
77
78    #[test]
79    fn matching_base_is_fresh() {
80        let current = Version::new(7);
81        assert_eq!(check_base(current, Version::new(7)), VersionVerdict::Fresh);
82    }
83
84    #[test]
85    fn stale_and_ahead_bases_both_report_stale_with_current() {
86        let current = Version::new(7);
87        for base in [Version::new(6), Version::new(8)] {
88            assert_eq!(
89                check_base(current, base),
90                VersionVerdict::Stale { current },
91                "only `current` is the truth; an ahead base is caller fiction"
92            );
93        }
94    }
95
96    #[test]
97    fn serde_is_transparent_u64() {
98        assert_eq!(serde_json::to_string(&Version::new(3)).unwrap(), "3");
99        let v: Version = serde_json::from_str("9").unwrap();
100        assert_eq!(v, Version::new(9));
101    }
102}