From 149485566085543e30552810399fe537090e1571 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 14 May 2026 15:15:15 +0100 Subject: [PATCH] feat(version): bake git-sha + build-date; add --json to status and a new version subcommand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #56. `--version` previously printed just the Cargo.toml version. `status` had no machine-readable output. Wire both via a build script. - `build.rs`: emit `VERISIMISER_GIT_SHA`, `VERISIMISER_GIT_DESCRIBE`, `VERISIMISER_BUILD_DATE` as compile-time env vars. No new runtime dependency — uses the `git` CLI directly and chrono in `[build-dependencies]`. Re-runs when `.git/HEAD` or `.git/refs` changes. - `clap` `version = LONG_VERSION`: shows `verisimiser 0.1.0 (, built )`. - New `verisimiser version [--json]` subcommand returns the same info as plain text or a structured JSON object. - New `--json` flag on `verisimiser status`. The schema is a stable `StatusReport` struct in `src/manifest/mod.rs` with documented field stability. Includes per-dimension enablement and the count. Quick check: ``` $ verisimiser --version verisimiser 0.1.0 (b255f20-dirty, built 2026-05-14) $ verisimiser version --json { "build_date": "2026-05-14", "git_describe": "b255f20-dirty", "git_sha": "b255f20afb70", "version": "0.1.0" } ``` `cargo clippy --all-targets -- -D warnings` clean; 32 unit tests pass. Co-Authored-By: Claude Opus 4.7 --- Cargo.toml | 4 +++ build.rs | 39 +++++++++++++++++++++++++++ src/main.rs | 45 ++++++++++++++++++++++++++++--- src/manifest/mod.rs | 66 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 build.rs diff --git a/Cargo.toml b/Cargo.toml index 3898a07..cdd29c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ license-file = "LICENSE" repository = "https://github.com/hyperpolymath/verisimiser" keywords = ["verisim", "database", "augmentation", "drift-detection", "provenance"] categories = ["command-line-utilities", "database"] +build = "build.rs" [dependencies] clap = { version = "4", features = ["derive"] } @@ -20,5 +21,8 @@ thiserror = "2" chrono = { version = "0.4", features = ["serde"] } sha2 = "0.10" +[build-dependencies] +chrono = "0.4" + [dev-dependencies] tempfile = "3" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..1eb6b30 --- /dev/null +++ b/build.rs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: PMPL-1.0-or-later +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +// +// Emit git-sha + build-date as compile-time env vars so the binary can show +// them in `verisimiser --version` and `verisimiser version --json`. +// Closes #56 (V-L3-J1). No build-dep — uses the `git` CLI and `chrono` is +// available at runtime via the main dependency tree. + +use std::process::Command; + +fn main() { + let sha = Command::new("git") + .args(["rev-parse", "--short=12", "HEAD"]) + .output() + .ok() + .filter(|o| o.status.success()) + .and_then(|o| String::from_utf8(o.stdout).ok()) + .map(|s| s.trim().to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + let describe = Command::new("git") + .args(["describe", "--tags", "--always", "--dirty"]) + .output() + .ok() + .filter(|o| o.status.success()) + .and_then(|o| String::from_utf8(o.stdout).ok()) + .map(|s| s.trim().to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + let build_date = chrono::Utc::now().format("%Y-%m-%d").to_string(); + + println!("cargo:rustc-env=VERISIMISER_GIT_SHA={}", sha); + println!("cargo:rustc-env=VERISIMISER_GIT_DESCRIBE={}", describe); + println!("cargo:rustc-env=VERISIMISER_BUILD_DATE={}", build_date); + + // Re-run when HEAD moves or git ref changes. + println!("cargo:rerun-if-changed=.git/HEAD"); + println!("cargo:rerun-if-changed=.git/refs"); +} diff --git a/src/main.rs b/src/main.rs index 00c41fc..eea1f78 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,9 +19,19 @@ use anyhow::Result; use clap::{Parser, Subcommand}; use verisimiser::{abi, codegen, manifest}; +/// Long version string: ` (, built )`. +const LONG_VERSION: &str = concat!( + env!("CARGO_PKG_VERSION"), + " (", + env!("VERISIMISER_GIT_DESCRIBE"), + ", built ", + env!("VERISIMISER_BUILD_DATE"), + ")", +); + /// VeriSimiser — augment any database with VeriSimDB octad capabilities. #[derive(Parser)] -#[command(name = "verisimiser", version, about, long_about = None)] +#[command(name = "verisimiser", version = LONG_VERSION, about, long_about = None)] struct Cli { #[command(subcommand)] command: Commands, @@ -85,9 +95,18 @@ enum Commands { Status { #[arg(short, long, default_value = "verisimiser.toml")] manifest: String, + /// Emit a structured JSON report instead of human-readable text. + #[arg(long)] + json: bool, }, /// Show the octad modalities and which tiers they belong to. Octad, + /// Print version, git-sha, and build-date. + Version { + /// Emit JSON instead of human-readable text. + #[arg(long)] + json: bool, + }, } fn main() -> Result<()> { @@ -189,9 +208,14 @@ fn main() -> Result<()> { Ok(()) } - Commands::Status { manifest } => { + Commands::Status { manifest, json } => { let m = manifest::load_manifest(&manifest)?; - manifest::print_status(&m); + if json { + let report = manifest::status_report(&m); + println!("{}", serde_json::to_string_pretty(&report)?); + } else { + manifest::print_status(&m); + } Ok(()) } @@ -199,6 +223,21 @@ fn main() -> Result<()> { print_octad(); Ok(()) } + + Commands::Version { json } => { + if json { + let report = serde_json::json!({ + "version": env!("CARGO_PKG_VERSION"), + "git_sha": env!("VERISIMISER_GIT_SHA"), + "git_describe": env!("VERISIMISER_GIT_DESCRIBE"), + "build_date": env!("VERISIMISER_BUILD_DATE"), + }); + println!("{}", serde_json::to_string_pretty(&report)?); + } else { + println!("{}", LONG_VERSION); + } + Ok(()) + } } } diff --git a/src/manifest/mod.rs b/src/manifest/mod.rs index ff4ec88..aa5bf2b 100644 --- a/src/manifest/mod.rs +++ b/src/manifest/mod.rs @@ -466,6 +466,72 @@ mod init_template_tests { } } +/// Documented JSON schema returned by `verisimiser status --json`. +/// +/// Field stability: `name`, `backend`, `sidecar_path`, `sidecar_storage`, +/// and `octad` are part of the public schema. New fields may be added +/// in minor versions; existing fields will not be removed without a +/// major version bump. +#[derive(Debug, Clone, Serialize)] +pub struct StatusReport { + /// Project name (`[project].name` or legacy `[verisimiser].name`). + pub name: String, + /// Effective database backend after legacy field resolution. + pub backend: String, + /// Path to the sidecar storage file. + pub sidecar_path: String, + /// Sidecar storage technology. + pub sidecar_storage: String, + /// Per-dimension enablement. + pub octad: OctadStatus, +} + +/// Per-dimension boolean view used by `StatusReport`. +#[derive(Debug, Clone, Serialize)] +pub struct OctadStatus { + /// Number of enabled dimensions (always in `2..=8`). + pub enabled_count: usize, + /// Always `true`. + pub data: bool, + /// Always `true`. + pub metadata: bool, + pub provenance: bool, + pub lineage: bool, + pub constraints: bool, + pub access_control: bool, + pub temporal: bool, + pub simulation: bool, +} + +/// Build a [`StatusReport`] from a loaded manifest. +/// +/// Used by `verisimiser status --json`. The same content is rendered as +/// plain text by [`print_status`]. +pub fn status_report(manifest: &Manifest) -> StatusReport { + let name = if !manifest.project.name.is_empty() { + manifest.project.name.clone() + } else { + manifest.verisimiser.name.clone() + }; + StatusReport { + name, + backend: manifest.database.effective_backend().to_string(), + sidecar_path: manifest.sidecar.path.clone(), + sidecar_storage: manifest.sidecar.storage.clone(), + octad: OctadStatus { + enabled_count: manifest.octad.enabled_count(), + data: true, + metadata: true, + provenance: manifest.octad.enable_provenance, + lineage: manifest.octad.enable_lineage, + constraints: manifest.octad.enable_constraints, + access_control: manifest.octad.enable_access_control, + temporal: manifest.octad.enable_temporal, + simulation: manifest.octad.enable_simulation, + }, + } +} + /// Print a human-readable status summary of a loaded manifest. pub fn print_status(manifest: &Manifest) { let name = if !manifest.project.name.is_empty() {