Skip to main content

memstead_base/
build_info.rs

1//! Build identity of the running binary.
2//!
3//! Between releases every dev build reports the same crate semver, so
4//! version-keyed signals — the plan-05 "engine version changed →
5//! re-read the tool roster" hint and the plan-02 mutation-stamp /
6//! `ENGINE_VERSION_SKEW` comparison — could never fire in dogfood or
7//! field use. `build.rs` captures the git commit at build time
8//! (`MEMSTEAD_BUILD_SHA`, empty outside a git checkout); this module
9//! renders the full build version every version-carrying surface
10//! serves: CLI `--version`, both MCP flavours' `serverInfo.version`,
11//! the overview's `_engine_version`, and the per-mem mutation stamp.
12
13/// The short git sha of the commit this binary was built from, with a
14/// `-dirty` suffix when tracked build inputs (`crates/`, `Cargo.toml`,
15/// `Cargo.lock`) were modified at build time.
16/// Empty for builds outside a git checkout (crates.io, vendored
17/// trees) — emptiness, not absence, is the sha-less signal.
18pub const BUILD_SHA: &str = env!("MEMSTEAD_BUILD_SHA");
19
20/// The full build version of the running binary: the bare crate
21/// semver ([`crate::ENGINE_VERSION`]) when no build sha exists, else
22/// `<semver>+g<sha>[-dirty]` (semver build-metadata syntax, so the
23/// value still parses as a `semver::Version`). Two dev builds of the
24/// same crate version compare unequal whenever their commits differ —
25/// exactly the property the skew stamp and the roster-refresh hint
26/// need.
27pub fn full_version() -> &'static str {
28    static FULL: std::sync::OnceLock<String> = std::sync::OnceLock::new();
29    FULL.get_or_init(|| {
30        if BUILD_SHA.is_empty() {
31            crate::ENGINE_VERSION.to_string()
32        } else {
33            format!("{}+g{}", crate::ENGINE_VERSION, BUILD_SHA)
34        }
35    })
36}
37
38/// Which way a mem's stamped engine version differs from the running binary.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
40#[serde(rename_all = "kebab-case")]
41pub enum SkewDirection {
42    /// The mem was last written by a NEWER binary than this one. The
43    /// interesting direction: this binary may not understand what that one
44    /// wrote.
45    StampedNewer,
46    /// The mem was last written by an OLDER binary than this one.
47    StampedOlder,
48}
49
50/// The direction of engine-version skew between a mem's stamp and the running
51/// binary, or `None` when there is none to report.
52///
53/// Compared as semver, which ignores build metadata, so two builds of the same
54/// release differ in their `+g<sha>` suffix and are NOT skew: the stamp writer
55/// still restamps (the sha is provenance worth keeping current) but nobody is
56/// told their engine disagrees when it does not (consistency-sweep 04/04,
57/// criterion 8). The previous rule was raw string inequality, which called
58/// every rebuild between releases a skew.
59///
60/// `None` also when either side fails to parse. A stamp this binary cannot
61/// read is not evidence of a direction, and guessing one would be worse than
62/// the silence.
63pub fn skew_direction(stamped: &str, running: &str) -> Option<SkewDirection> {
64    let (a, b) = (
65        stamped.parse::<semver::Version>().ok()?,
66        running.parse::<semver::Version>().ok()?,
67    );
68    match a.cmp_precedence(&b) {
69        std::cmp::Ordering::Greater => Some(SkewDirection::StampedNewer),
70        std::cmp::Ordering::Less => Some(SkewDirection::StampedOlder),
71        std::cmp::Ordering::Equal => None,
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    /// The full build version is the bare semver exactly when no sha
80    /// was captured, else semver plus `+g<sha>` build metadata — and
81    /// either way it parses as a real `semver::Version` whose
82    /// version core equals the crate version.
83    #[test]
84    fn full_version_is_semver_with_optional_build_sha() {
85        let full = full_version();
86        if BUILD_SHA.is_empty() {
87            assert_eq!(full, crate::ENGINE_VERSION);
88        } else {
89            assert_eq!(
90                full,
91                format!("{}+g{}", crate::ENGINE_VERSION, BUILD_SHA).as_str()
92            );
93        }
94        let parsed: semver::Version = full.parse().expect("full build version parses as semver");
95        let bare: semver::Version = crate::ENGINE_VERSION.parse().unwrap();
96        assert_eq!(
97            (parsed.major, parsed.minor, parsed.patch),
98            (bare.major, bare.minor, bare.patch)
99        );
100    }
101
102    /// 04/04, criterion 8. The build-metadata case is the one the old raw
103    /// string comparison got wrong: every rebuild between releases read as
104    /// skew, which is why the warning was noise on a dogfood workspace.
105    #[test]
106    fn skew_is_semver_difference_and_never_a_build_hash() {
107        use super::{SkewDirection, skew_direction};
108        assert_eq!(
109            skew_direction("0.11.0", "0.12.0"),
110            Some(SkewDirection::StampedOlder)
111        );
112        assert_eq!(
113            skew_direction("0.13.0", "0.12.0"),
114            Some(SkewDirection::StampedNewer)
115        );
116        // Same release, different commit: not skew in either direction.
117        assert_eq!(skew_direction("0.12.0+gabc123", "0.12.0+gdef456"), None);
118        assert_eq!(skew_direction("0.12.0", "0.12.0+gabc123"), None);
119        assert_eq!(skew_direction("0.12.0+gabc123-dirty", "0.12.0"), None);
120        // A pre-release IS a semver difference, and the direction is real.
121        assert_eq!(
122            skew_direction("0.12.0-rc.1", "0.12.0"),
123            Some(SkewDirection::StampedOlder)
124        );
125        // Unparseable: no direction rather than a guessed one.
126        assert_eq!(skew_direction("not-a-version", "0.12.0"), None);
127        assert_eq!(skew_direction("0.12.0", ""), None);
128    }
129}