Skip to main content

omgbase_surface/
version.rs

1//! The `version` tool (`spec/surface` §4, 1.4): which engine and which
2//! versions — the serving binary's own, every component crate's, the spec
3//! versions they were built against, the database schema, the MCP protocol,
4//! the runtime, the build's commit and time.
5//!
6//! Who knows what. This crate knows the components (every `omgbase-*` crate
7//! and `oqx` are its dependencies, each with a `VERSION` and a
8//! `SPEC_VERSION`), the schema (the open store) and the protocol revision the
9//! catalog is written against. It does **not** know the binary: the `omgbase`
10//! crate depends on this one, not the other way round, so the host passes a
11//! [`BuildInfo`] — its version, its `spec/cli` version, the build's commit
12//! and time (`build.rs`), the compiler. Without one (a library embedding)
13//! the tool answers with this crate's own version and nulls, which is the
14//! shape the fixture runner records. Port of `packages/core/src/version.ts`.
15
16use omgbase_store::Store;
17use serde_json::{Map, Value as Json, json};
18
19use crate::catalog::Profile;
20
21/// The MCP protocol revision the catalog (§4) is written against and the
22/// `omgbase` binary serves by default when a client names none (the
23/// reference's SDK `LATEST_PROTOCOL_VERSION`). A client's own revision is
24/// echoed at `initialize`; the four methods the server speaks are identical
25/// across revisions.
26pub const MCP_PROTOCOL_VERSION: &str = "2025-11-25";
27
28/// What the serving binary knows about itself.
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct BuildInfo {
31    /// The binary's own version (the `omgbase` crate's; what `--version` prints).
32    pub version: String,
33    /// The `spec/cli/VERSION` the binary renders (`specs.cli`); `None` for a
34    /// host without a CLI (`null` on the wire).
35    pub cli_spec: Option<String>,
36    /// The build's short git revision; `None` when unknown.
37    pub commit: Option<String>,
38    /// The build's RFC 3339 time; `None` when unknown.
39    pub built: Option<String>,
40    /// `rustc --version` at build time; `None` → `"rustc"`.
41    pub rustc: Option<String>,
42}
43
44impl Default for BuildInfo {
45    /// The library embedding: this crate's own version, no CLI, no build.
46    fn default() -> Self {
47        Self {
48            version: crate::VERSION.to_owned(),
49            cli_spec: None,
50            commit: None,
51            built: None,
52            rustc: None,
53        }
54    }
55}
56
57/// Every omgbase crate the binary is built from, with its version — the
58/// binary's own first (its version is the host's), then the dependency
59/// crates; keys sorted bytewise.
60fn components(build: &BuildInfo) -> Json {
61    let mut m: Vec<(&str, &str)> = vec![
62        ("omgbase", build.version.as_str()),
63        ("omgbase-format", omgbase_format::VERSION),
64        ("omgbase-graph", omgbase_graph::VERSION),
65        ("omgbase-mutate", omgbase_mutate::VERSION),
66        ("omgbase-properties", omgbase_properties::VERSION),
67        ("omgbase-reconcile", omgbase_reconcile::VERSION),
68        ("omgbase-search", omgbase_search::VERSION),
69        ("omgbase-store", omgbase_store::VERSION),
70        ("omgbase-surface", crate::VERSION),
71        ("omgbase-sync", omgbase_sync::VERSION),
72        ("oqx", oqx::VERSION),
73    ];
74    m.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
75    Json::Object(
76        m.into_iter()
77            .map(|(k, v)| (k.to_owned(), Json::String(v.to_owned())))
78            .collect::<Map<_, _>>(),
79    )
80}
81
82/// The `spec/<x>/VERSION` each crate implements, in the order §4 lists them.
83fn specs(build: &BuildInfo) -> Json {
84    json!({
85        "oqx": oqx::LANGUAGE_VERSION,
86        "format": omgbase_format::SPEC_VERSION,
87        "reconcile": omgbase_reconcile::SPEC_VERSION,
88        "store": omgbase_store::SPEC_VERSION,
89        "properties": omgbase_properties::SPEC_VERSION,
90        "graph": omgbase_graph::SPEC_VERSION,
91        "search": omgbase_search::SPEC_VERSION,
92        "mutate": omgbase_mutate::SPEC_VERSION,
93        "sync": omgbase_sync::SPEC_VERSION,
94        "surface": crate::SPEC_VERSION,
95        "cli": build.cli_spec,
96    })
97}
98
99/// The `version` result: `{ engine, version, components, specs, schema,
100/// mcp, runtime, commit, built, profile }`. `store` supplies `schema` (its
101/// `PRAGMA user_version`; `null` without one); `profile` is the catalog
102/// profile the host serves (§4 "Profiles", 4.3) — a server's `--tools`, and
103/// `Full` for the CLI's own `version` verb and a library embedding.
104pub fn version_info(
105    store: Option<&Store>,
106    build: &BuildInfo,
107    profile: Profile,
108) -> crate::Result<Json> {
109    let schema = match store {
110        Some(s) => Json::from(s.user_version()?),
111        None => Json::Null,
112    };
113    let runtime = build.rustc.clone().unwrap_or_else(|| "rustc".to_owned());
114    Ok(json!({
115        "engine": "rust",
116        "version": build.version,
117        "components": components(build),
118        "specs": specs(build),
119        "schema": schema,
120        "mcp": { "protocol": MCP_PROTOCOL_VERSION },
121        "runtime": runtime,
122        "commit": build.commit,
123        "built": build.built,
124        "profile": profile.as_str(),
125    }))
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn library_embedding_answers_with_its_own_version_and_nulls() {
134        let store = Store::open_in_memory().unwrap();
135        let v = version_info(Some(&store), &BuildInfo::default(), Profile::Full).unwrap();
136        assert_eq!(v["engine"], "rust");
137        assert_eq!(v["version"], crate::VERSION);
138        assert_eq!(v["components"]["omgbase-surface"], crate::VERSION);
139        assert_eq!(v["components"]["omgbase"], crate::VERSION);
140        assert_eq!(v["components"]["oqx"], oqx::VERSION);
141        let keys: Vec<&String> = v["components"].as_object().unwrap().keys().collect();
142        let mut sorted = keys.clone();
143        sorted.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
144        assert_eq!(keys, sorted);
145        assert_eq!(keys.len(), 11);
146        assert_eq!(v["specs"]["surface"], crate::SPEC_VERSION);
147        assert_eq!(v["specs"]["store"], omgbase_store::SPEC_VERSION);
148        assert_eq!(v["specs"]["cli"], Json::Null);
149        assert_eq!(
150            v["specs"].as_object().unwrap().keys().collect::<Vec<_>>(),
151            [
152                "oqx",
153                "format",
154                "reconcile",
155                "store",
156                "properties",
157                "graph",
158                "search",
159                "mutate",
160                "sync",
161                "surface",
162                "cli"
163            ]
164        );
165        assert_eq!(v["schema"], omgbase_store::schema::SCHEMA_VERSION);
166        assert_eq!(v["mcp"], json!({ "protocol": MCP_PROTOCOL_VERSION }));
167        assert_eq!(v["runtime"], "rustc");
168        assert_eq!(v["commit"], Json::Null);
169        assert_eq!(v["built"], Json::Null);
170        assert_eq!(v["profile"], "full");
171        assert_eq!(
172            v.as_object().unwrap().keys().collect::<Vec<_>>(),
173            [
174                "engine",
175                "version",
176                "components",
177                "specs",
178                "schema",
179                "mcp",
180                "runtime",
181                "commit",
182                "built",
183                "profile"
184            ]
185        );
186        let none = version_info(None, &BuildInfo::default(), Profile::Full).unwrap();
187        assert_eq!(none["schema"], Json::Null);
188    }
189
190    #[test]
191    fn a_host_supplies_its_own_identity() {
192        let build = BuildInfo {
193            version: "9.9.9".to_owned(),
194            cli_spec: Some("1.1".to_owned()),
195            commit: Some("abc1234".to_owned()),
196            built: Some("2026-09-27T00:00:00Z".to_owned()),
197            rustc: Some("rustc 1.97.0".to_owned()),
198        };
199        let v = version_info(None, &build, Profile::Minimal).unwrap();
200        assert_eq!(v["version"], "9.9.9");
201        assert_eq!(v["components"]["omgbase"], "9.9.9");
202        assert_eq!(v["specs"]["cli"], "1.1");
203        assert_eq!(v["runtime"], "rustc 1.97.0");
204        assert_eq!(v["commit"], "abc1234");
205        assert_eq!(v["built"], "2026-09-27T00:00:00Z");
206    }
207}